diff --git a/x-pack/plugins/security_solution/package.json b/x-pack/plugins/security_solution/package.json index 73347e00e6b3..108ed6695885 100644 --- a/x-pack/plugins/security_solution/package.json +++ b/x-pack/plugins/security_solution/package.json @@ -16,9 +16,11 @@ "@types/lodash": "^4.14.110" }, "dependencies": { + "@types/rbush": "^3.0.0", + "@types/seedrandom": ">=2.0.0 <4.0.0", "lodash": "^4.17.15", "querystring": "^0.2.0", - "redux-devtools-extension": "^2.13.8", - "@types/seedrandom": ">=2.0.0 <4.0.0" + "rbush": "^3.0.1", + "redux-devtools-extension": "^2.13.8" } } diff --git a/x-pack/plugins/security_solution/public/resolver/lib/aabb.ts b/x-pack/plugins/security_solution/public/resolver/lib/aabb.ts new file mode 100644 index 000000000000..0937d10c24d8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/lib/aabb.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import * as vector2 from './vector2'; +import { AABB } from '../types'; + +/** + * Return a boolean indicating if 2 vector objects are equal. + */ +export function isEqual(a: AABB, b: AABB): boolean { + return vector2.isEqual(a.minimum, b.minimum) && vector2.isEqual(a.maximum, b.maximum); +} diff --git a/x-pack/plugins/security_solution/public/resolver/lib/vector2.ts b/x-pack/plugins/security_solution/public/resolver/lib/vector2.ts index 898ce6f6bacd..35f17c9460f8 100644 --- a/x-pack/plugins/security_solution/public/resolver/lib/vector2.ts +++ b/x-pack/plugins/security_solution/public/resolver/lib/vector2.ts @@ -40,6 +40,13 @@ export function applyMatrix3([x, y]: Vector2, [m11, m12, m13, m21, m22, m23]: Ma return [x * m11 + y * m12 + m13, x * m21 + y * m22 + m23]; } +/** + * Returns a boolean indicating equality of two vectors. + */ +export function isEqual([x1, y1]: Vector2, [x2, y2]: Vector2): boolean { + return x1 === x2 && y1 === y2; +} + /** * Returns the distance between two vectors */ diff --git a/x-pack/plugins/security_solution/public/resolver/models/process_event.ts b/x-pack/plugins/security_solution/public/resolver/models/process_event.ts index 1094fee6da24..0286cca93b43 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/process_event.ts +++ b/x-pack/plugins/security_solution/public/resolver/models/process_event.ts @@ -24,6 +24,10 @@ function isValue(field: string | string[], value: string) { } } +export function isTerminatedProcess(passedEvent: ResolverEvent) { + return eventType(passedEvent) === 'processTerminated'; +} + /** * Returns a custom event type for a process event based on the event's metadata. */ diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/__snapshots__/graphing.test.ts.snap b/x-pack/plugins/security_solution/public/resolver/store/data/__snapshots__/graphing.test.ts.snap index f21d3b210681..8525ccd7b154 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/__snapshots__/graphing.test.ts.snap +++ b/x-pack/plugins/security_solution/public/resolver/store/data/__snapshots__/graphing.test.ts.snap @@ -36,6 +36,9 @@ exports[`resolver graph layout when rendering two forks, and one fork has an ext Object { "edgeLineSegments": Array [ Object { + "metadata": Object { + "uniqueId": "parentToMid", + }, "points": Array [ Array [ 0, @@ -48,6 +51,9 @@ Object { ], }, Object { + "metadata": Object { + "uniqueId": "midway", + }, "points": Array [ Array [ 0, @@ -60,7 +66,9 @@ Object { ], }, Object { - "metadata": Object {}, + "metadata": Object { + "uniqueId": "", + }, "points": Array [ Array [ 0, @@ -73,7 +81,9 @@ Object { ], }, Object { - "metadata": Object {}, + "metadata": Object { + "uniqueId": "", + }, "points": Array [ Array [ 395.9797974644666, @@ -86,6 +96,9 @@ Object { ], }, Object { + "metadata": Object { + "uniqueId": "parentToMid13", + }, "points": Array [ Array [ 197.9898987322333, @@ -98,6 +111,9 @@ Object { ], }, Object { + "metadata": Object { + "uniqueId": "midway13", + }, "points": Array [ Array [ 296.98484809834997, @@ -110,7 +126,9 @@ Object { ], }, Object { - "metadata": Object {}, + "metadata": Object { + "uniqueId": "13", + }, "points": Array [ Array [ 296.98484809834997, @@ -123,7 +141,9 @@ Object { ], }, Object { - "metadata": Object {}, + "metadata": Object { + "uniqueId": "14", + }, "points": Array [ Array [ 494.9747468305833, @@ -136,6 +156,9 @@ Object { ], }, Object { + "metadata": Object { + "uniqueId": "parentToMid25", + }, "points": Array [ Array [ 593.9696961966999, @@ -148,6 +171,9 @@ Object { ], }, Object { + "metadata": Object { + "uniqueId": "midway25", + }, "points": Array [ Array [ 692.9646455628166, @@ -160,7 +186,9 @@ Object { ], }, Object { - "metadata": Object {}, + "metadata": Object { + "uniqueId": "25", + }, "points": Array [ Array [ 692.9646455628166, @@ -173,7 +201,9 @@ Object { ], }, Object { - "metadata": Object {}, + "metadata": Object { + "uniqueId": "26", + }, "points": Array [ Array [ 890.9545442950499, @@ -186,7 +216,9 @@ Object { ], }, Object { - "metadata": Object {}, + "metadata": Object { + "uniqueId": "67", + }, "points": Array [ Array [ 1088.9444430272833, @@ -344,7 +376,9 @@ exports[`resolver graph layout when rendering two nodes, one being the parent of Object { "edgeLineSegments": Array [ Object { - "metadata": Object {}, + "metadata": Object { + "uniqueId": "", + }, "points": Array [ Array [ 0, diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts index ba415e6d83c8..5654f1ca423f 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import rbush from 'rbush'; import { createSelector } from 'reselect'; import { DataState, @@ -16,11 +17,20 @@ import { AdjacentProcessMap, Vector2, EdgeLineMetadata, + IndexedEntity, + IndexedEdgeLineSegment, + IndexedProcessNode, + AABB, + VisibleEntites, } from '../../types'; import { ResolverEvent } from '../../../../common/endpoint/types'; -import { eventTimestamp } from '../../../../common/endpoint/models/event'; +import * as event from '../../../../common/endpoint/models/event'; import { add as vector2Add, applyMatrix3 } from '../../lib/vector2'; -import { isGraphableProcess, uniquePidForProcess } from '../../models/process_event'; +import { + isGraphableProcess, + isTerminatedProcess, + uniquePidForProcess, +} from '../../models/process_event'; import { factory as indexedProcessTreeFactory, children as indexedProcessTreeChildren, @@ -29,6 +39,7 @@ import { levelOrder, } from '../../models/indexed_process_tree'; import { getFriendlyElapsedTime } from '../../lib/date'; +import { isEqual } from '../../lib/aabb'; const unit = 140; const distanceBetweenNodesInUnits = 2; @@ -81,6 +92,20 @@ export const graphableProcesses = createSelector( } ); +/** + * Process events that will be displayed as terminated. + */ +export const terminatedProcesses = createSelector( + ({ results }: DataState) => results, + function (results: DataState['results']) { + return new Set( + results.filter(isTerminatedProcess).map((terminatedEvent) => { + return uniquePidForProcess(terminatedEvent); + }) + ); + } +); + /** * In laying out the graph, we precalculate the 'width' of each subtree. The 'width' of the subtree is determined by its * descedants and the rule that each process node must be at least 1 unit apart. Enforcing that all nodes are at least @@ -157,7 +182,7 @@ function processEdgeLineSegments( ): EdgeLineSegment[] { const edgeLineSegments: EdgeLineSegment[] = []; for (const metadata of levelOrderWithWidths(indexedProcessTree, widths)) { - const edgeLineMetadata: EdgeLineMetadata = {}; + const edgeLineMetadata: EdgeLineMetadata = { uniqueId: '' }; /** * We only handle children, drawing lines back to their parents. The root has no parent, so we skip it */ @@ -168,6 +193,9 @@ function processEdgeLineSegments( const { process, parent, parentWidth } = metadata; const position = positions.get(process); const parentPosition = positions.get(parent); + const parentId = event.entityId(parent); + const processEntityId = event.entityId(process); + const edgeLineId = parentId ? parentId + processEntityId : parentId; if (position === undefined || parentPosition === undefined) { /** @@ -176,12 +204,13 @@ function processEdgeLineSegments( throw new Error(); } - const parentTime = eventTimestamp(parent); - const processTime = eventTimestamp(process); + const parentTime = event.eventTimestamp(parent); + const processTime = event.eventTimestamp(process); if (parentTime && processTime) { const elapsedTime = getFriendlyElapsedTime(parentTime, processTime); if (elapsedTime) edgeLineMetadata.elapsedTime = elapsedTime; } + edgeLineMetadata.uniqueId = edgeLineId; /** * The point halfway between the parent and child on the y axis, we sometimes have a hard angle here in the edge line @@ -226,6 +255,7 @@ function processEdgeLineSegments( const lineFromParentToMidwayLine: EdgeLineSegment = { points: [parentPosition, [parentPosition[0], midwayY]], + metadata: { uniqueId: `parentToMid${edgeLineId}` }, }; const widthOfMidline = parentWidth - firstChildWidth / 2 - lastChildWidth / 2; @@ -246,6 +276,7 @@ function processEdgeLineSegments( midwayY, ], ], + metadata: { uniqueId: `midway${edgeLineId}` }, }; edgeLineSegments.push( @@ -508,18 +539,16 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( for (const edgeLineSegment of edgeLineSegments) { const { points: [startPoint, endPoint], - metadata, } = edgeLineSegment; const transformedSegment: EdgeLineSegment = { + ...edgeLineSegment, points: [ applyMatrix3(startPoint, isometricTransformMatrix), applyMatrix3(endPoint, isometricTransformMatrix), ], }; - if (metadata) transformedSegment.metadata = metadata; - transformedEdgeLineSegments.push(transformedSegment); } @@ -530,6 +559,96 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( } ); +const indexedProcessNodePositionsAndEdgeLineSegments = createSelector( + processNodePositionsAndEdgeLineSegments, + function visibleProcessNodePositionsAndEdgeLineSegments({ + /* eslint-disable no-shadow */ + processNodePositions, + edgeLineSegments, + /* eslint-enable no-shadow */ + }) { + const tree: rbush = new rbush(); + const processesToIndex: IndexedProcessNode[] = []; + const edgeLineSegmentsToIndex: IndexedEdgeLineSegment[] = []; + + // Make sure these numbers are big enough to cover the process nodes at all zoom levels. + // The process nodes don't extend equally in all directions from their center point. + const processNodeViewWidth = 720; + const processNodeViewHeight = 240; + const lineSegmentPadding = 30; + for (const [processEvent, position] of processNodePositions) { + const [nodeX, nodeY] = position; + const indexedEvent: IndexedProcessNode = { + minX: nodeX - 0.5 * processNodeViewWidth, + minY: nodeY - 0.5 * processNodeViewHeight, + maxX: nodeX + 0.5 * processNodeViewWidth, + maxY: nodeY + 0.5 * processNodeViewHeight, + position, + entity: processEvent, + type: 'processNode', + }; + processesToIndex.push(indexedEvent); + } + for (const edgeLineSegment of edgeLineSegments) { + const { + points: [[x1, y1], [x2, y2]], + } = edgeLineSegment; + const indexedLineSegment: IndexedEdgeLineSegment = { + minX: Math.min(x1, x2) - lineSegmentPadding, + minY: Math.min(y1, y2) - lineSegmentPadding, + maxX: Math.max(x1, x2) + lineSegmentPadding, + maxY: Math.max(y1, y2) + lineSegmentPadding, + entity: edgeLineSegment, + type: 'edgeLine', + }; + edgeLineSegmentsToIndex.push(indexedLineSegment); + } + tree.load([...processesToIndex, ...edgeLineSegmentsToIndex]); + return tree; + } +); + +export const visibleProcessNodePositionsAndEdgeLineSegments = createSelector( + indexedProcessNodePositionsAndEdgeLineSegments, + function visibleProcessNodePositionsAndEdgeLineSegments(tree) { + // memoize the results of this call to avoid unnecessarily rerunning + let lastBoundingBox: AABB | null = null; + let currentlyVisible: VisibleEntites = { + processNodePositions: new Map(), + connectingEdgeLineSegments: [], + }; + return (boundingBox: AABB) => { + if (lastBoundingBox !== null && isEqual(lastBoundingBox, boundingBox)) { + return currentlyVisible; + } else { + const { + minimum: [minX, minY], + maximum: [maxX, maxY], + } = boundingBox; + const entities = tree.search({ + minX, + minY, + maxX, + maxY, + }); + const visibleProcessNodePositions = new Map( + entities + .filter((entity): entity is IndexedProcessNode => entity.type === 'processNode') + .map((node) => [node.entity, node.position]) + ); + const connectingEdgeLineSegments = entities + .filter((entity): entity is IndexedEdgeLineSegment => entity.type === 'edgeLine') + .map((node) => node.entity); + currentlyVisible = { + processNodePositions: visibleProcessNodePositions, + connectingEdgeLineSegments, + }; + lastBoundingBox = boundingBox; + return currentlyVisible; + } + }; + } +); /** * Returns the `children` and `ancestors` limits for the current graph, if any. * diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/visible_entities.test.ts b/x-pack/plugins/security_solution/public/resolver/store/data/visible_entities.test.ts new file mode 100644 index 000000000000..f10cfe0ba466 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/store/data/visible_entities.test.ts @@ -0,0 +1,165 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Store, createStore } from 'redux'; +import { ResolverAction } from '../actions'; +import { resolverReducer } from '../reducer'; +import { ResolverState } from '../../types'; +import { LegacyEndpointEvent, ResolverEvent } from '../../../../common/endpoint/types'; +import { visibleProcessNodePositionsAndEdgeLineSegments } from '../selectors'; +import { mockProcessEvent } from '../../models/process_event_test_helpers'; + +describe('resolver visible entities', () => { + let processA: LegacyEndpointEvent; + let processB: LegacyEndpointEvent; + let processC: LegacyEndpointEvent; + let processD: LegacyEndpointEvent; + let processE: LegacyEndpointEvent; + let processF: LegacyEndpointEvent; + let processG: LegacyEndpointEvent; + let store: Store; + + beforeEach(() => { + /* + * A + * | + * B + * | + * C + * | + * D etc + */ + processA = mockProcessEvent({ + endgame: { + process_name: '', + event_type_full: 'process_event', + event_subtype_full: 'creation_event', + unique_pid: 0, + }, + }); + processB = mockProcessEvent({ + endgame: { + event_type_full: 'process_event', + event_subtype_full: 'already_running', + unique_pid: 1, + unique_ppid: 0, + }, + }); + processC = mockProcessEvent({ + endgame: { + event_type_full: 'process_event', + event_subtype_full: 'creation_event', + unique_pid: 2, + unique_ppid: 1, + }, + }); + processD = mockProcessEvent({ + endgame: { + event_type_full: 'process_event', + event_subtype_full: 'creation_event', + unique_pid: 3, + unique_ppid: 2, + }, + }); + processE = mockProcessEvent({ + endgame: { + event_type_full: 'process_event', + event_subtype_full: 'creation_event', + unique_pid: 4, + unique_ppid: 3, + }, + }); + processF = mockProcessEvent({ + endgame: { + event_type_full: 'process_event', + event_subtype_full: 'creation_event', + unique_pid: 5, + unique_ppid: 4, + }, + }); + processF = mockProcessEvent({ + endgame: { + event_type_full: 'process_event', + event_subtype_full: 'creation_event', + unique_pid: 6, + unique_ppid: 5, + }, + }); + processG = mockProcessEvent({ + endgame: { + event_type_full: 'process_event', + event_subtype_full: 'creation_event', + unique_pid: 7, + unique_ppid: 6, + }, + }); + store = createStore(resolverReducer, undefined); + }); + describe('when rendering a large tree with a small viewport', () => { + beforeEach(() => { + const events: ResolverEvent[] = [ + processA, + processB, + processC, + processD, + processE, + processF, + processG, + ]; + const action: ResolverAction = { + type: 'serverReturnedResolverData', + payload: { events, stats: new Map(), lineageLimits: { children: '', ancestors: '' } }, + }; + const cameraAction: ResolverAction = { type: 'userSetRasterSize', payload: [300, 200] }; + store.dispatch(action); + store.dispatch(cameraAction); + }); + it('the visibleProcessNodePositions list should only include 2 nodes', () => { + const { processNodePositions } = visibleProcessNodePositionsAndEdgeLineSegments( + store.getState() + )(0); + expect([...processNodePositions.keys()].length).toEqual(2); + }); + it('the visibleEdgeLineSegments list should only include one edge line', () => { + const { connectingEdgeLineSegments } = visibleProcessNodePositionsAndEdgeLineSegments( + store.getState() + )(0); + expect(connectingEdgeLineSegments.length).toEqual(1); + }); + }); + describe('when rendering a large tree with a large viewport', () => { + beforeEach(() => { + const events: ResolverEvent[] = [ + processA, + processB, + processC, + processD, + processE, + processF, + processG, + ]; + const action: ResolverAction = { + type: 'serverReturnedResolverData', + payload: { events, stats: new Map(), lineageLimits: { children: '', ancestors: '' } }, + }; + const cameraAction: ResolverAction = { type: 'userSetRasterSize', payload: [2000, 2000] }; + store.dispatch(action); + store.dispatch(cameraAction); + }); + it('the visibleProcessNodePositions list should include all process nodes', () => { + const { processNodePositions } = visibleProcessNodePositionsAndEdgeLineSegments( + store.getState() + )(0); + expect([...processNodePositions.keys()].length).toEqual(5); + }); + it('the visibleEdgeLineSegments list include all lines', () => { + const { connectingEdgeLineSegments } = visibleProcessNodePositionsAndEdgeLineSegments( + store.getState() + )(0); + expect(connectingEdgeLineSegments.length).toEqual(4); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware.ts index 343b4e1a1447..a1807255b5ea 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/middleware.ts @@ -112,7 +112,6 @@ export const resolverMiddlewareFactory: MiddlewareFactory = (context) => { query: { events: 100 }, } ); - api.dispatch({ type: 'serverReturnedRelatedEventData', payload: result, diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts index 3a5c48009e5b..5599b7e8ab61 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { createSelector } from 'reselect'; import * as cameraSelectors from './camera/selectors'; import * as dataSelectors from './data/selectors'; import * as uiSelectors from './ui/selectors'; @@ -60,6 +61,11 @@ export const processAdjacencies = composeSelectors( dataSelectors.processAdjacencies ); +export const terminatedProcesses = composeSelectors( + dataStateSelector, + dataSelectors.terminatedProcesses +); + /** * Returns a map of `ResolverEvent` entity_id to their related event and alert statistics */ @@ -171,3 +177,27 @@ function composeSelectors( ): (state: OuterState) => ReturnValue { return (state) => secondSelector(selector(state)); } + +const boundingBox = composeSelectors(cameraStateSelector, cameraSelectors.viewableBoundingBox); +const indexedProcessNodesAndEdgeLineSegments = composeSelectors( + dataStateSelector, + dataSelectors.visibleProcessNodePositionsAndEdgeLineSegments +); + +/** + * Return the visible edge lines and process nodes based on the camera position at `time`. + * The bounding box represents what the camera can see. The camera position is a function of time because it can be + * animated. So in order to get the currently visible entities, we need to pass in time. + */ +export const visibleProcessNodePositionsAndEdgeLineSegments = createSelector( + indexedProcessNodesAndEdgeLineSegments, + boundingBox, + function ( + /* eslint-disable no-shadow */ + indexedProcessNodesAndEdgeLineSegments, + boundingBox + /* eslint-enable no-shadow */ + ) { + return (time: number) => indexedProcessNodesAndEdgeLineSegments(boundingBox(time)); + } +); diff --git a/x-pack/plugins/security_solution/public/resolver/types.ts b/x-pack/plugins/security_solution/public/resolver/types.ts index f0e401dd2e89..0742fa2e3056 100644 --- a/x-pack/plugins/security_solution/public/resolver/types.ts +++ b/x-pack/plugins/security_solution/public/resolver/types.ts @@ -5,7 +5,7 @@ */ import { Store } from 'redux'; - +import { BBox } from 'rbush'; import { ResolverAction } from './store/actions'; export { ResolverAction } from './store/actions'; import { @@ -142,6 +142,36 @@ export type CameraState = { } ); +/** + * Wrappers around our internal types that make them compatible with `rbush`. + */ +export type IndexedEntity = IndexedEdgeLineSegment | IndexedProcessNode; + +/** + * The entity stored in rbush for resolver edge lines. + */ +export interface IndexedEdgeLineSegment extends BBox { + type: 'edgeLine'; + entity: EdgeLineSegment; +} + +/** + * The entity store in rbush for resolver process nodes. + */ +export interface IndexedProcessNode extends BBox { + type: 'processNode'; + entity: ResolverEvent; + position: Vector2; +} + +/** + * A type containing all things to actually be rendered to the DOM. + */ +export interface VisibleEntites { + processNodePositions: ProcessPositions; + connectingEdgeLineSegments: EdgeLineSegment[]; +} + /** * State for `data` reducer which handles receiving Resolver data from the backend. */ @@ -287,6 +317,8 @@ export interface DurationDetails { */ export interface EdgeLineMetadata { elapsedTime?: DurationDetails; + // A string of the two joined process nodes concatted together. + uniqueId: string; } /** * A tuple of 2 vector2 points forming a polyline. Used to connect process nodes in the graph. @@ -298,7 +330,7 @@ export type EdgeLinePoints = Vector2[]; */ export interface EdgeLineSegment { points: EdgeLinePoints; - metadata?: EdgeLineMetadata; + metadata: EdgeLineMetadata; } /** diff --git a/x-pack/plugins/security_solution/public/resolver/view/assets.tsx b/x-pack/plugins/security_solution/public/resolver/view/assets.tsx index 82f969b755b2..442a90f0a575 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/assets.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/assets.tsx @@ -12,8 +12,6 @@ import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; import { useUiSetting } from '../../common/lib/kibana'; import { DEFAULT_DARK_MODE } from '../../../common/constants'; -import { ResolverEvent } from '../../../common/endpoint/types'; -import * as processModel from '../models/process_event'; import { ResolverProcessType } from '../types'; type ResolverColorNames = @@ -417,27 +415,13 @@ const processTypeToCube: Record = { unknownEvent: 'runningProcessCube', }; -/** - * This will return which type the ResolverEvent will display as in the Node component - * it will be something like 'runningProcessCube' or 'terminatedProcessCube' - * - * @param processEvent {ResolverEvent} the event to get the Resolver Component Node type of - */ -export function nodeType(processEvent: ResolverEvent): keyof NodeStyleMap { - const processType = processModel.eventType(processEvent); - if (processType in processTypeToCube) { - return processTypeToCube[processType]; - } - return 'runningProcessCube'; -} - /** * A hook to bring Resolver theming information into components. */ export const useResolverTheme = (): { colorMap: ColorMap; nodeAssets: NodeStyleMap; - cubeAssetsForNode: (arg0: ResolverEvent) => NodeStyleConfig; + cubeAssetsForNode: (isProcessTerimnated: boolean, isProcessOrigin: boolean) => NodeStyleConfig; } => { const isDarkMode = useUiSetting(DEFAULT_DARK_MODE); const theme = isDarkMode ? euiThemeAmsterdamDark : euiThemeAmsterdamLight; @@ -511,12 +495,14 @@ export const useResolverTheme = (): { }, }; - /** - * Export assets to reuse symbols/icons in other places in the app (e.g. tables, etc.) - * @param processEvent : The process event to fetch node assets for - */ - function cubeAssetsForNode(processEvent: ResolverEvent) { - return nodeAssets[nodeType(processEvent)]; + function cubeAssetsForNode(isProcessTerminated: boolean, isProcessOrigin: boolean) { + if (isProcessTerminated) { + return nodeAssets[processTypeToCube.processTerminated]; + } else if (isProcessOrigin) { + return nodeAssets[processTypeToCube.processCausedAlert]; + } else { + return nodeAssets[processTypeToCube.processRan]; + } } return { colorMap, nodeAssets, cubeAssetsForNode }; diff --git a/x-pack/plugins/security_solution/public/resolver/view/index.tsx b/x-pack/plugins/security_solution/public/resolver/view/index.tsx index 9dfc9a45fafe..9b7114b56495 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useLayoutEffect } from 'react'; +import React, { useLayoutEffect, useContext } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import styled from 'styled-components'; import { EuiLoadingSpinner } from '@elastic/eui'; @@ -16,9 +16,10 @@ import { GraphControls } from './graph_controls'; import { ProcessEventDot } from './process_event_dot'; import { useCamera } from './use_camera'; import { SymbolDefinitions, useResolverTheme } from './assets'; +import { entityId } from '../../../common/endpoint/models/event'; import { ResolverAction } from '../types'; import { ResolverEvent } from '../../../common/endpoint/types'; -import * as eventModel from '../../../common/endpoint/models/event'; +import { SideEffectContext } from './side_effect_context'; interface StyledResolver { backgroundColor: string; @@ -74,17 +75,20 @@ export const Resolver = React.memo(function Resolver({ className?: string; selectedEvent?: ResolverEvent; }) { - const { processNodePositions, edgeLineSegments } = useSelector( - selectors.processNodePositionsAndEdgeLineSegments - ); + const { timestamp } = useContext(SideEffectContext); + + const { processNodePositions, connectingEdgeLineSegments } = useSelector( + selectors.visibleProcessNodePositionsAndEdgeLineSegments + )(timestamp()); const dispatch: (action: ResolverAction) => unknown = useDispatch(); const { processToAdjacencyMap } = useSelector(selectors.processAdjacencies); - const relatedEventsStats = useSelector(selectors.relatedEventsStats); const { projectionMatrix, ref, onMouseDown } = useCamera(); const isLoading = useSelector(selectors.isLoading); const hasError = useSelector(selectors.hasError); + const relatedEventsStats = useSelector(selectors.relatedEventsStats); const activeDescendantId = useSelector(selectors.uiActiveDescendantId); + const terminatedProcesses = useSelector(selectors.terminatedProcesses); const { colorMap } = useResolverTheme(); useLayoutEffect(() => { @@ -123,10 +127,10 @@ export const Resolver = React.memo(function Resolver({ tabIndex={0} aria-activedescendant={activeDescendantId || undefined} > - {edgeLineSegments.map(({ points: [startPosition, endPosition], metadata }, index) => ( + {connectingEdgeLineSegments.map(({ points: [startPosition, endPosition], metadata }) => ( { const adjacentNodeMap = processToAdjacencyMap.get(processEvent); + const processEntityId = entityId(processEvent); if (!adjacentNodeMap) { // This should never happen throw new Error('Issue calculating adjacency node map.'); @@ -145,7 +150,9 @@ export const Resolver = React.memo(function Resolver({ projectionMatrix={projectionMatrix} event={processEvent} adjacentNodeMap={adjacentNodeMap} - relatedEventsStats={relatedEventsStats.get(eventModel.entityId(processEvent))} + relatedEventsStats={relatedEventsStats.get(entityId(processEvent))} + isProcessTerminated={terminatedProcesses.has(processEntityId)} + isProcessOrigin={false} /> ); })} diff --git a/x-pack/plugins/security_solution/public/resolver/view/panel.tsx b/x-pack/plugins/security_solution/public/resolver/view/panel.tsx index 4bef2f4d2a10..c8f6512077a6 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panel.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panel.tsx @@ -51,6 +51,7 @@ const PanelContent = memo(function PanelContent() { const urlSearch = history.location.search; const dispatch = useResolverDispatch(); + const { timestamp } = useContext(SideEffectContext); const queryParams: CrumbInfo = useMemo(() => { return { crumbId: '', crumbEvent: '', ...querystring.parse(urlSearch.slice(1)) }; }, [urlSearch]); @@ -84,7 +85,7 @@ const PanelContent = memo(function PanelContent() { const paramsSelectedEvent = useMemo(() => { return graphableProcesses.find((evt) => event.entityId(evt) === idFromParams); }, [graphableProcesses, idFromParams]); - const { timestamp } = useContext(SideEffectContext); + const [lastUpdatedProcess, setLastUpdatedProcess] = useState(null); /** @@ -218,11 +219,19 @@ const PanelContent = memo(function PanelContent() { }, [panelToShow, dispatch]); const currentPanelView = useSelector(selectors.currentPanelView); + const terminatedProcesses = useSelector(selectors.terminatedProcesses); + const processEntityId = uiSelectedEvent ? event.entityId(uiSelectedEvent) : undefined; + const isProcessTerminated = processEntityId ? terminatedProcesses.has(processEntityId) : false; const panelInstance = useMemo(() => { if (currentPanelView === 'processDetails') { return ( - + ); } @@ -261,7 +270,13 @@ const PanelContent = memo(function PanelContent() { ); } // The default 'Event List' / 'List of all processes' view - return ; + return ( + + ); }, [ uiSelectedEvent, crumbEvent, @@ -269,6 +284,7 @@ const PanelContent = memo(function PanelContent() { pushToQueryParams, relatedStatsForIdFromParams, currentPanelView, + isProcessTerminated, ]); return <>{panelInstance}; diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_detail.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_detail.tsx index fcb7bf1d12e1..3127c7132df3 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_detail.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_detail.tsx @@ -41,10 +41,14 @@ const StyledDescriptionList = styled(EuiDescriptionList)` */ export const ProcessDetails = memo(function ProcessDetails({ processEvent, + isProcessTerminated, + isProcessOrigin, pushToQueryParams, }: { processEvent: ResolverEvent; - pushToQueryParams: (arg0: CrumbInfo) => unknown; + isProcessTerminated: boolean; + isProcessOrigin: boolean; + pushToQueryParams: (queryStringKeyValuePair: CrumbInfo) => unknown; }) { const processName = event.eventName(processEvent); const processInfoEntry = useMemo(() => { @@ -178,8 +182,8 @@ export const ProcessDetails = memo(function ProcessDetails({ if (!processEvent) { return { descriptionText: '' }; } - return cubeAssetsForNode(processEvent); - }, [processEvent, cubeAssetsForNode]); + return cubeAssetsForNode(isProcessTerminated, isProcessOrigin); + }, [processEvent, cubeAssetsForNode, isProcessTerminated, isProcessOrigin]); const titleId = useMemo(() => htmlIdGenerator('resolverTable')(), []); return ( @@ -188,7 +192,10 @@ export const ProcessDetails = memo(function ProcessDetails({

- + {processName}

diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_list.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_list.tsx index 86ae10b3b38c..9152649c07ab 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_list.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_list.tsx @@ -28,8 +28,12 @@ import { ResolverEvent } from '../../../../common/endpoint/types'; */ export const ProcessListWithCounts = memo(function ProcessListWithCounts({ pushToQueryParams, + isProcessTerminated, + isProcessOrigin, }: { - pushToQueryParams: (arg0: CrumbInfo) => unknown; + pushToQueryParams: (queryStringKeyValuePair: CrumbInfo) => unknown; + isProcessTerminated: boolean; + isProcessOrigin: boolean; }) { interface ProcessTableView { name: string; @@ -82,7 +86,10 @@ export const ProcessListWithCounts = memo(function ProcessListWithCounts({ pushToQueryParams({ crumbId: event.entityId(item.event), crumbEvent: '' }); }} > - + {name} ); @@ -114,7 +121,7 @@ export const ProcessListWithCounts = memo(function ProcessListWithCounts({ }, }, ], - [pushToQueryParams, handleBringIntoViewClick] + [pushToQueryParams, handleBringIntoViewClick, isProcessOrigin, isProcessTerminated] ); const { processNodePositions } = useSelector(selectors.processNodePositionsAndEdgeLineSegments); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_related_counts.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_related_counts.tsx index 2e4211f568ff..880ee1dc7a10 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_related_counts.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_related_counts.tsx @@ -30,7 +30,7 @@ export const EventCountsForProcess = memo(function EventCountsForProcess({ relatedStats, }: { processEvent: ResolverEvent; - pushToQueryParams: (arg0: CrumbInfo) => unknown; + pushToQueryParams: (queryStringKeyValuePair: CrumbInfo) => unknown; relatedStats: ResolverNodeStats; }) { interface EventCountsTableView { diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_related_detail.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_related_detail.tsx index 1fe6599e0829..f27ec56fef69 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_related_detail.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_related_detail.tsx @@ -96,7 +96,7 @@ export const RelatedEventDetail = memo(function RelatedEventDetail({ }: { relatedEventId: string; parentEvent: ResolverEvent; - pushToQueryParams: (arg0: CrumbInfo) => unknown; + pushToQueryParams: (queryStringKeyValuePair: CrumbInfo) => unknown; countForParent: number | undefined; }) { const processName = (parentEvent && event.eventName(parentEvent)) || '*'; diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/process_cube_icon.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/process_cube_icon.tsx index 29ffe154d571..98eea51a011b 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/process_cube_icon.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/process_cube_icon.tsx @@ -5,7 +5,6 @@ */ import React, { memo } from 'react'; -import { ResolverEvent } from '../../../../common/endpoint/types'; import { useResolverTheme } from '../assets'; /** @@ -13,12 +12,14 @@ import { useResolverTheme } from '../assets'; * Nodes on the graph and what's in the table. Using the same symbol in both places (as below) could help with that. */ export const CubeForProcess = memo(function CubeForProcess({ - processEvent, + isProcessTerminated, + isProcessOrigin, }: { - processEvent: ResolverEvent; + isProcessTerminated: boolean; + isProcessOrigin: boolean; }) { const { cubeAssetsForNode } = useResolverTheme(); - const { cubeSymbol, descriptionText } = cubeAssetsForNode(processEvent); + const { cubeSymbol, descriptionText } = cubeAssetsForNode(isProcessTerminated, isProcessOrigin); return ( <> diff --git a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx index 78b70611a697..e7c9960f7805 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx @@ -15,7 +15,7 @@ import querystring from 'querystring'; import { NodeSubMenu, subMenuAssets } from './submenu'; import { applyMatrix3 } from '../lib/vector2'; import { Vector2, Matrix3, AdjacentProcessMap } from '../types'; -import { SymbolIds, useResolverTheme, calculateResolverFontSize, nodeType } from './assets'; +import { SymbolIds, useResolverTheme, calculateResolverFontSize } from './assets'; import { ResolverEvent, ResolverNodeStats } from '../../../common/endpoint/types'; import { useResolverDispatch } from './use_resolver_dispatch'; import * as eventModel from '../../../common/endpoint/models/event'; @@ -239,6 +239,8 @@ const ProcessEventDotComponents = React.memo( event, projectionMatrix, adjacentNodeMap, + isProcessTerminated, + isProcessOrigin, relatedEventsStats, }: { /** @@ -262,6 +264,16 @@ const ProcessEventDotComponents = React.memo( */ adjacentNodeMap: AdjacentProcessMap; /** + * Whether or not to show the process as terminated. + */ + isProcessTerminated: boolean; + /** + * Whether or not to show the process as the originating event. + */ + isProcessOrigin: boolean; + /** + * A collection of events related to the current node and statistics (e.g. counts indexed by event type) + * to provide the user some visibility regarding the contents thereof. * Statistics for the number of related events and alerts for this process node */ relatedEventsStats?: ResolverNodeStats; @@ -363,7 +375,7 @@ const ProcessEventDotComponents = React.memo( }) | null; } = React.createRef(); - const { colorMap, nodeAssets } = useResolverTheme(); + const { colorMap, cubeAssetsForNode } = useResolverTheme(); const { backingFill, cubeSymbol, @@ -371,7 +383,8 @@ const ProcessEventDotComponents = React.memo( isLabelFilled, labelButtonFill, strokeColor, - } = nodeAssets[nodeType(event)]; + } = cubeAssetsForNode(isProcessTerminated, isProcessOrigin); + const resolverNodeIdGenerator = useMemo(() => htmlIdGenerator('resolverNode'), []); const nodeId = useMemo(() => resolverNodeIdGenerator(selfId), [ diff --git a/yarn.lock b/yarn.lock index 60122f8b8cde..53fef40b44c9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5616,6 +5616,11 @@ resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8" integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw== +"@types/rbush@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/rbush/-/rbush-3.0.0.tgz#b6887d99b159e87ae23cd14eceff34f139842aa6" + integrity sha512-W3ue/GYWXBOpkRm0VSoifrP3HV0Ni47aVJWvXyWMcbtpBy/l/K/smBRiJ+fI8f7shXRjZBiux+iJzYbh7VmcZg== + "@types/reach__router@^1.2.3", "@types/reach__router@^1.2.6": version "1.2.6" resolved "https://registry.yarnpkg.com/@types/reach__router/-/reach__router-1.2.6.tgz#b14cf1adbd1a365d204bbf6605cd9dd7b8816c87" @@ -25291,6 +25296,13 @@ raw-loader@~0.5.1: resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-0.5.1.tgz#0c3d0beaed8a01c966d9787bf778281252a979aa" integrity sha1-DD0L6u2KAclm2Xh793goElKpeao= +rbush@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/rbush/-/rbush-3.0.1.tgz#5fafa8a79b3b9afdfe5008403a720cc1de882ecf" + integrity sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w== + dependencies: + quickselect "^2.0.0" + rc@^1.0.1, rc@^1.1.6, rc@^1.2.7, rc@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"