diff --git a/x-pack/plugins/endpoint/common/models/event.ts b/x-pack/plugins/endpoint/common/models/event.ts new file mode 100644 index 0000000000000..650486f3c3858 --- /dev/null +++ b/x-pack/plugins/endpoint/common/models/event.ts @@ -0,0 +1,31 @@ +/* + * 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 { EndpointEvent, LegacyEndpointEvent } from '../types'; + +export function isLegacyEvent( + event: EndpointEvent | LegacyEndpointEvent +): event is LegacyEndpointEvent { + return (event as LegacyEndpointEvent).endgame !== undefined; +} + +export function eventTimestamp( + event: EndpointEvent | LegacyEndpointEvent +): string | undefined | number { + if (isLegacyEvent(event)) { + return event.endgame.timestamp_utc; + } else { + return event['@timestamp']; + } +} + +export function eventName(event: EndpointEvent | LegacyEndpointEvent): string { + if (isLegacyEvent(event)) { + return event.endgame.process_name ? event.endgame.process_name : ''; + } else { + return event.process.name; + } +} diff --git a/x-pack/plugins/endpoint/common/types.ts b/x-pack/plugins/endpoint/common/types.ts index aa326c663965d..92bec4f72a387 100644 --- a/x-pack/plugins/endpoint/common/types.ts +++ b/x-pack/plugins/endpoint/common/types.ts @@ -311,8 +311,8 @@ export interface EndpointEvent { version: string; }; event: { - category: string; - type: string; + category: string | string[]; + type: string | string[]; id: string; kind: string; }; @@ -328,6 +328,7 @@ export interface EndpointEvent { name: string; parent?: { entity_id: string; + name?: string; }; }; } diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts index 68731bb3f307f..5e9b08c09c2c7 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts @@ -165,15 +165,3 @@ export const hasSelectedAlert: (state: AlertListState) => boolean = createSelect uiQueryParams, ({ selected_alert: selectedAlert }) => selectedAlert !== undefined ); - -/** - * Determine if the alert event is most likely compatible with LegacyEndpointEvent. - */ -export const selectedAlertIsLegacyEndpointEvent: ( - state: AlertListState -) => boolean = createSelector(selectedAlertDetailsData, function(event) { - if (event === undefined) { - return false; - } - return 'endgame' in event; -}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/overview/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/overview/index.tsx index 82a4bc00a4396..0ec5a855c8615 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/overview/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/overview/index.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { memo, useMemo } from 'react'; +import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -19,87 +20,104 @@ import * as selectors from '../../../../store/alerts/selectors'; import { MetadataPanel } from './metadata_panel'; import { FormattedDate } from '../../formatted_date'; import { AlertDetailResolver } from '../../resolver'; +import { ResolverEvent } from '../../../../../../../common/types'; import { TakeActionDropdown } from './take_action_dropdown'; -export const AlertDetailsOverview = memo(() => { - const alertDetailsData = useAlertListSelector(selectors.selectedAlertDetailsData); - if (alertDetailsData === undefined) { - return null; - } - const selectedAlertIsLegacyEndpointEvent = useAlertListSelector( - selectors.selectedAlertIsLegacyEndpointEvent - ); +export const AlertDetailsOverview = styled( + memo(() => { + const alertDetailsData = useAlertListSelector(selectors.selectedAlertDetailsData); + if (alertDetailsData === undefined) { + return null; + } - const tabs: EuiTabbedContentTab[] = useMemo(() => { - return [ - { - id: 'overviewMetadata', - 'data-test-subj': 'overviewMetadata', - name: i18n.translate( - 'xpack.endpoint.application.endpoint.alertDetails.overview.tabs.overview', - { - defaultMessage: 'Overview', - } - ), - content: ( - <> - - - - ), - }, - { - id: 'overviewResolver', - name: i18n.translate( - 'xpack.endpoint.application.endpoint.alertDetails.overview.tabs.resolver', - { - defaultMessage: 'Resolver', - } - ), - content: ( - <> - - {selectedAlertIsLegacyEndpointEvent && } - - ), - }, - ]; - }, [selectedAlertIsLegacyEndpointEvent]); + const tabs: EuiTabbedContentTab[] = useMemo(() => { + return [ + { + id: 'overviewMetadata', + 'data-test-subj': 'overviewMetadata', + name: i18n.translate( + 'xpack.endpoint.application.endpoint.alertDetails.overview.tabs.overview', + { + defaultMessage: 'Overview', + } + ), + content: ( + <> + + + + ), + }, + { + id: 'overviewResolver', + 'data-test-subj': 'overviewResolverTab', + name: i18n.translate( + 'xpack.endpoint.application.endpoint.alertDetails.overview.tabs.resolver', + { + defaultMessage: 'Resolver', + } + ), + content: ( + <> + + + + ), + }, + ]; + }, [alertDetailsData]); - return ( - <> -
- -

+ return ( + <> +
+ +

+ +

+
+ + +

+ , + }} + /> +

+
+ + + Endpoint Status:{' '} + + {' '} + + + + + {' '} -

-
- - -

- , - }} - /> -

-
- - - Endpoint Status: Online - - Alert Status: Open - - - -
- - - ); -}); + + + + + + + + ); + }) +)` + height: 100%; + width: 100%; +`; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/resolver.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/resolver.tsx index 52ef480bbb930..d18bc59a35f52 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/resolver.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/resolver.tsx @@ -10,12 +10,12 @@ import { Provider } from 'react-redux'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { Resolver } from '../../../../embeddables/resolver/view'; import { EndpointPluginServices } from '../../../../plugin'; -import { LegacyEndpointEvent } from '../../../../../common/types'; +import { ResolverEvent } from '../../../../../common/types'; import { storeFactory } from '../../../../embeddables/resolver/store'; export const AlertDetailResolver = styled( React.memo( - ({ className, selectedEvent }: { className?: string; selectedEvent?: LegacyEndpointEvent }) => { + ({ className, selectedEvent }: { className?: string; selectedEvent?: ResolverEvent }) => { const context = useKibana(); const { store } = storeFactory(context); @@ -33,4 +33,5 @@ export const AlertDetailResolver = styled( width: 100%; display: flex; flex-grow: 1; + min-height: 500px; `; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/models/indexed_process_tree.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/models/indexed_process_tree.ts index 6892bf11ecff2..c9a03f0a47968 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/models/indexed_process_tree.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/models/indexed_process_tree.ts @@ -6,15 +6,15 @@ import { uniquePidForProcess, uniqueParentPidForProcess } from './process_event'; import { IndexedProcessTree } from '../types'; -import { LegacyEndpointEvent } from '../../../../common/types'; +import { ResolverEvent } from '../../../../common/types'; import { levelOrder as baseLevelOrder } from '../lib/tree_sequencers'; /** * Create a new IndexedProcessTree from an array of ProcessEvents */ -export function factory(processes: LegacyEndpointEvent[]): IndexedProcessTree { - const idToChildren = new Map(); - const idToValue = new Map(); +export function factory(processes: ResolverEvent[]): IndexedProcessTree { + const idToChildren = new Map(); + const idToValue = new Map(); for (const process of processes) { idToValue.set(uniquePidForProcess(process), process); @@ -36,10 +36,7 @@ export function factory(processes: LegacyEndpointEvent[]): IndexedProcessTree { /** * Returns an array with any children `ProcessEvent`s of the passed in `process` */ -export function children( - tree: IndexedProcessTree, - process: LegacyEndpointEvent -): LegacyEndpointEvent[] { +export function children(tree: IndexedProcessTree, process: ResolverEvent): ResolverEvent[] { const id = uniquePidForProcess(process); const processChildren = tree.idToChildren.get(id); return processChildren === undefined ? [] : processChildren; @@ -50,8 +47,8 @@ export function children( */ export function parent( tree: IndexedProcessTree, - childProcess: LegacyEndpointEvent -): LegacyEndpointEvent | undefined { + childProcess: ResolverEvent +): ResolverEvent | undefined { const uniqueParentPid = uniqueParentPidForProcess(childProcess); if (uniqueParentPid === undefined) { return undefined; @@ -74,7 +71,7 @@ export function root(tree: IndexedProcessTree) { if (size(tree) === 0) { return null; } - let current: LegacyEndpointEvent = tree.idToProcess.values().next().value; + let current: ResolverEvent = tree.idToProcess.values().next().value; while (parent(tree, current) !== undefined) { current = parent(tree, current)!; } diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.ts index 876168d2ed96a..a709d6caf46cb 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.ts @@ -4,36 +4,65 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LegacyEndpointEvent } from '../../../../common/types'; +import { ResolverEvent } from '../../../../common/types'; +import * as event from '../../../../common/models/event'; +import { ResolverProcessType } from '../types'; /** * Returns true if the process's eventType is either 'processCreated' or 'processRan'. * Resolver will only render 'graphable' process events. */ -export function isGraphableProcess(passedEvent: LegacyEndpointEvent) { +export function isGraphableProcess(passedEvent: ResolverEvent) { return eventType(passedEvent) === 'processCreated' || eventType(passedEvent) === 'processRan'; } +function isValue(field: string | string[], value: string) { + if (field instanceof Array) { + return field.length === 1 && field[0] === value; + } else { + return field === value; + } +} + /** * Returns a custom event type for a process event based on the event's metadata. */ -export function eventType(passedEvent: LegacyEndpointEvent) { - const { - endgame: { event_type_full: type, event_subtype_full: subType }, - } = passedEvent; +export function eventType(passedEvent: ResolverEvent): ResolverProcessType { + if (event.isLegacyEvent(passedEvent)) { + const { + endgame: { event_type_full: type, event_subtype_full: subType }, + } = passedEvent; - if (type === 'process_event') { - if (subType === 'creation_event' || subType === 'fork_event' || subType === 'exec_event') { - return 'processCreated'; - } else if (subType === 'already_running') { - return 'processRan'; - } else if (subType === 'termination_event') { - return 'processTerminated'; - } else { - return 'unknownProcessEvent'; + if (type === 'process_event') { + if (subType === 'creation_event' || subType === 'fork_event' || subType === 'exec_event') { + return 'processCreated'; + } else if (subType === 'already_running') { + return 'processRan'; + } else if (subType === 'termination_event') { + return 'processTerminated'; + } else { + return 'unknownProcessEvent'; + } + } else if (type === 'alert_event') { + return 'processCausedAlert'; + } + } else { + const { + event: { type, category, kind }, + } = passedEvent; + if (isValue(category, 'process')) { + if (isValue(type, 'start') || isValue(type, 'change') || isValue(type, 'creation')) { + return 'processCreated'; + } else if (isValue(type, 'info')) { + return 'processRan'; + } else if (isValue(type, 'end')) { + return 'processTerminated'; + } else { + return 'unknownProcessEvent'; + } + } else if (kind === 'alert') { + return 'processCausedAlert'; } - } else if (type === 'alert_event') { - return 'processCausedAlert'; } return 'unknownEvent'; } @@ -41,13 +70,21 @@ export function eventType(passedEvent: LegacyEndpointEvent) { /** * Returns the process event's pid */ -export function uniquePidForProcess(event: LegacyEndpointEvent) { - return event.endgame.unique_pid; +export function uniquePidForProcess(passedEvent: ResolverEvent): string { + if (event.isLegacyEvent(passedEvent)) { + return String(passedEvent.endgame.unique_pid); + } else { + return passedEvent.process.entity_id; + } } /** * Returns the process event's parent pid */ -export function uniqueParentPidForProcess(event: LegacyEndpointEvent) { - return event.endgame.unique_ppid; +export function uniqueParentPidForProcess(passedEvent: ResolverEvent): string | undefined { + if (event.isLegacyEvent(passedEvent)) { + return String(passedEvent.endgame.unique_ppid); + } else { + return passedEvent.process.parent?.entity_id; + } } diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/actions.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/actions.ts index ecba0ec404d44..fec2078cc60c9 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/actions.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/actions.ts @@ -5,7 +5,7 @@ */ import { CameraAction } from './camera'; import { DataAction } from './data'; -import { LegacyEndpointEvent } from '../../../../common/types'; +import { ResolverEvent } from '../../../../common/types'; /** * When the user wants to bring a process node front-and-center on the map. @@ -16,7 +16,7 @@ interface UserBroughtProcessIntoView { /** * Used to identify the process node that should be brought into view. */ - readonly process: LegacyEndpointEvent; + readonly process: ResolverEvent; /** * The time (since epoch in milliseconds) when the action was dispatched. */ @@ -33,7 +33,7 @@ interface UserChangedSelectedEvent { /** * Optional because they could have unselected the event. */ - selectedEvent?: LegacyEndpointEvent; + readonly selectedEvent?: ResolverEvent; }; } diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts index f34d7c08ce08c..373afa89921dc 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LegacyEndpointEvent } from '../../../../../common/types'; +import { ResolverEvent } from '../../../../../common/types'; interface ServerReturnedResolverData { readonly type: 'serverReturnedResolverData'; readonly payload: { readonly data: { readonly result: { - readonly search_results: readonly LegacyEndpointEvent[]; + readonly search_results: readonly ResolverEvent[]; }; }; }; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts index 304abbb06880b..e8007f82e30c2 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts @@ -14,7 +14,7 @@ import { ProcessWithWidthMetadata, Matrix3, } from '../../types'; -import { LegacyEndpointEvent } from '../../../../../common/types'; +import { ResolverEvent } from '../../../../../common/types'; import { Vector2 } from '../../types'; import { add as vector2Add, applyMatrix3 } from '../../lib/vector2'; import { isGraphableProcess } from '../../models/process_event'; @@ -112,7 +112,7 @@ export const graphableProcesses = createSelector( * */ function widthsOfProcessSubtrees(indexedProcessTree: IndexedProcessTree): ProcessWidths { - const widths = new Map(); + const widths = new Map(); if (size(indexedProcessTree) === 0) { return widths; @@ -313,13 +313,13 @@ function processPositions( indexedProcessTree: IndexedProcessTree, widths: ProcessWidths ): ProcessPositions { - const positions = new Map(); + const positions = new Map(); /** * This algorithm iterates the tree in level order. It keeps counters that are reset for each parent. * By keeping track of the last parent node, we can know when we are dealing with a new set of siblings and * reset the counters. */ - let lastProcessedParentNode: LegacyEndpointEvent | undefined; + let lastProcessedParentNode: ResolverEvent | undefined; /** * Nodes are positioned relative to their siblings. We walk this in level order, so we handle * children left -> right. @@ -424,7 +424,7 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( * Transform the positions of nodes and edges so they seem like they are on an isometric grid. */ const transformedEdgeLineSegments: EdgeLineSegment[] = []; - const transformedPositions = new Map(); + const transformedPositions = new Map(); for (const [processEvent, position] of positions) { transformedPositions.set(processEvent, applyMatrix3(position, isometricTransformMatrix)); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/methods.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/methods.ts index 9f06643626f50..f15307a662388 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/methods.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/methods.ts @@ -7,7 +7,7 @@ import { animatePanning } from './camera/methods'; import { processNodePositionsAndEdgeLineSegments } from './selectors'; import { ResolverState } from '../types'; -import { LegacyEndpointEvent } from '../../../../common/types'; +import { ResolverEvent } from '../../../../common/types'; const animationDuration = 1000; @@ -17,7 +17,7 @@ const animationDuration = 1000; export function animateProcessIntoView( state: ResolverState, startTime: number, - process: LegacyEndpointEvent + process: ResolverEvent ): ResolverState { const { processNodePositions } = processNodePositionsAndEdgeLineSegments(state); const position = processNodePositions.get(process); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/middleware.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/middleware.ts index 900aece60618d..23e4a4fe7d7ed 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/middleware.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/middleware.ts @@ -8,6 +8,8 @@ import { Dispatch, MiddlewareAPI } from 'redux'; import { KibanaReactContextValue } from '../../../../../../../src/plugins/kibana_react/public'; import { EndpointPluginServices } from '../../../plugin'; import { ResolverState, ResolverAction } from '../types'; +import { ResolverEvent } from '../../../../common/types'; +import * as event from '../../../../common/models/event'; type MiddlewareFactory = ( context?: KibanaReactContextValue @@ -19,22 +21,54 @@ export const resolverMiddlewareFactory: MiddlewareFactory = context => { return api => next => async (action: ResolverAction) => { next(action); if (action.type === 'userChangedSelectedEvent') { - if (context?.services.http) { + /** + * concurrently fetches a process's details, its ancestors, and its related events. + */ + if (context?.services.http && action.payload.selectedEvent) { api.dispatch({ type: 'appRequestedResolverData' }); - const uniquePid = action.payload.selectedEvent?.endgame?.unique_pid; - const legacyEndpointID = action.payload.selectedEvent?.agent?.id; - const [{ lifecycle }, { children }, { events: relatedEvents }] = await Promise.all([ - context.services.http.get(`/api/endpoint/resolver/${uniquePid}`, { - query: { legacyEndpointID }, - }), - context.services.http.get(`/api/endpoint/resolver/${uniquePid}/children`, { - query: { legacyEndpointID }, - }), - context.services.http.get(`/api/endpoint/resolver/${uniquePid}/related`, { - query: { legacyEndpointID }, - }), - ]); - const response = [...lifecycle, ...children, ...relatedEvents]; + let response = []; + let lifecycle: ResolverEvent[]; + let childEvents: ResolverEvent[]; + let relatedEvents: ResolverEvent[]; + let children = []; + const ancestors: ResolverEvent[] = []; + const maxAncestors = 5; + if (event.isLegacyEvent(action.payload.selectedEvent)) { + const uniquePid = action.payload.selectedEvent?.endgame?.unique_pid; + const legacyEndpointID = action.payload.selectedEvent?.agent?.id; + [{ lifecycle }, { children }, { events: relatedEvents }] = await Promise.all([ + context.services.http.get(`/api/endpoint/resolver/${uniquePid}`, { + query: { legacyEndpointID }, + }), + context.services.http.get(`/api/endpoint/resolver/${uniquePid}/children`, { + query: { legacyEndpointID }, + }), + context.services.http.get(`/api/endpoint/resolver/${uniquePid}/related`, { + query: { legacyEndpointID }, + }), + ]); + childEvents = children.length > 0 ? children.map((child: any) => child.lifecycle) : []; + } else { + const uniquePid = action.payload.selectedEvent.process.entity_id; + const ppid = action.payload.selectedEvent.process.parent?.entity_id; + async function getAncestors(pid: string | undefined) { + if (ancestors.length < maxAncestors && pid !== undefined) { + const parent = await context?.services.http.get(`/api/endpoint/resolver/${pid}`); + ancestors.push(parent.lifecycle[0]); + if (parent.lifecycle[0].process?.parent?.entity_id) { + await getAncestors(parent.lifecycle[0].process.parent.entity_id); + } + } + } + [{ lifecycle }, { children }, { events: relatedEvents }] = await Promise.all([ + context.services.http.get(`/api/endpoint/resolver/${uniquePid}`), + context.services.http.get(`/api/endpoint/resolver/${uniquePid}/children`), + context.services.http.get(`/api/endpoint/resolver/${uniquePid}/related`), + getAncestors(ppid), + ]); + } + childEvents = children.length > 0 ? children.map((child: any) => child.lifecycle) : []; + response = [...lifecycle, ...childEvents, ...relatedEvents, ...ancestors]; api.dispatch({ type: 'serverReturnedResolverData', payload: { data: { result: { search_results: response } } }, diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts index 4c2a1ea5ac21f..4380d3ab98999 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts @@ -8,7 +8,7 @@ import { Store } from 'redux'; import { ResolverAction } from './store/actions'; export { ResolverAction } from './store/actions'; -import { LegacyEndpointEvent } from '../../../common/types'; +import { ResolverEvent } from '../../../common/types'; /** * Redux state for the Resolver feature. Properties on this interface are populated via multiple reducers using redux's `combineReducers`. @@ -115,7 +115,7 @@ export type CameraState = { * State for `data` reducer which handles receiving Resolver data from the backend. */ export interface DataState { - readonly results: readonly LegacyEndpointEvent[]; + readonly results: readonly ResolverEvent[]; isLoading: boolean; } @@ -184,21 +184,21 @@ export interface IndexedProcessTree { /** * Map of ID to a process's children */ - idToChildren: Map; + idToChildren: Map; /** * Map of ID to process */ - idToProcess: Map; + idToProcess: Map; } /** * A map of ProcessEvents (representing process nodes) to the 'width' of their subtrees as calculated by `widthsOfProcessSubtrees` */ -export type ProcessWidths = Map; +export type ProcessWidths = Map; /** * Map of ProcessEvents (representing process nodes) to their positions. Calculated by `processPositions` */ -export type ProcessPositions = Map; +export type ProcessPositions = Map; /** * An array of vectors2 forming an polyline. Used to connect process nodes in the graph. */ @@ -208,11 +208,11 @@ export type EdgeLineSegment = Vector2[]; * Used to provide precalculated info from `widthsOfProcessSubtrees`. These 'width' values are used in the layout of the graph. */ export type ProcessWithWidthMetadata = { - process: LegacyEndpointEvent; + process: ResolverEvent; width: number; } & ( | { - parent: LegacyEndpointEvent; + parent: ResolverEvent; parentWidth: number; isOnlyChild: boolean; firstChildWidth: number; @@ -275,4 +275,15 @@ export interface SideEffectSimulator { mock: jest.Mocked> & Pick; } +/** + * The internal types of process events used by resolver, mapped from v0 and v1 events. + */ +export type ResolverProcessType = + | 'processCreated' + | 'processRan' + | 'processTerminated' + | 'unknownProcessEvent' + | 'processCausedAlert' + | 'unknownEvent'; + export type ResolverStore = Store; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx index 52a0872f269f5..eab22f993d0a8 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -15,7 +15,7 @@ import { GraphControls } from './graph_controls'; import { ProcessEventDot } from './process_event_dot'; import { useCamera } from './use_camera'; import { ResolverAction } from '../types'; -import { LegacyEndpointEvent } from '../../../../common/types'; +import { ResolverEvent } from '../../../../common/types'; const StyledPanel = styled(Panel)` position: absolute; @@ -39,7 +39,7 @@ export const Resolver = styled( selectedEvent, }: { className?: string; - selectedEvent?: LegacyEndpointEvent; + selectedEvent?: ResolverEvent; }) { const { processNodePositions, edgeLineSegments } = useSelector( selectors.processNodePositionsAndEdgeLineSegments diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/panel.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/panel.tsx index 84c299698bb32..1250c1106b355 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/panel.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/panel.tsx @@ -11,7 +11,8 @@ import euiVars from '@elastic/eui/dist/eui_theme_light.json'; import { useSelector } from 'react-redux'; import { i18n } from '@kbn/i18n'; import { SideEffectContext } from './side_effect_context'; -import { LegacyEndpointEvent } from '../../../../common/types'; +import { ResolverEvent } from '../../../../common/types'; +import * as event from '../../../../common/models/event'; import { useResolverDispatch } from './use_resolver_dispatch'; import * as selectors from '../store/selectors'; @@ -38,7 +39,7 @@ export const Panel = memo(function Event({ className }: { className?: string }) interface ProcessTableView { name: string; timestamp?: Date; - event: LegacyEndpointEvent; + event: ResolverEvent; } const { processNodePositions } = useSelector(selectors.processNodePositionsAndEdgeLineSegments); @@ -48,14 +49,16 @@ export const Panel = memo(function Event({ className }: { className?: string }) () => [...processNodePositions.keys()].map(processEvent => { let dateTime; - if (processEvent.endgame.timestamp_utc) { - const date = new Date(processEvent.endgame.timestamp_utc); + const eventTime = event.eventTimestamp(processEvent); + const name = event.eventName(processEvent); + if (eventTime) { + const date = new Date(eventTime); if (isFinite(date.getTime())) { dateTime = date; } } return { - name: processEvent.endgame.process_name ? processEvent.endgame.process_name : '', + name, timestamp: dateTime, event: processEvent, }; @@ -115,9 +118,9 @@ export const Panel = memo(function Event({ className }: { className?: string }) }), dataType: 'date', sortable: true, - render(eventTimestamp?: Date) { - return eventTimestamp ? ( - formatter.format(eventTimestamp) + render(eventDate?: Date) { + return eventDate ? ( + formatter.format(eventDate) ) : ( {i18n.translate('xpack.endpoint.resolver.panel.tabel.row.timestampInvalidLabel', { diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx index 034780c7ba14c..2241df97291ae 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx @@ -8,7 +8,8 @@ import React from 'react'; import styled from 'styled-components'; import { applyMatrix3 } from '../lib/vector2'; import { Vector2, Matrix3 } from '../types'; -import { LegacyEndpointEvent } from '../../../../common/types'; +import { ResolverEvent } from '../../../../common/types'; +import * as eventModel from '../../../../common/models/event'; /** * A placeholder view for a process node. @@ -32,7 +33,7 @@ export const ProcessEventDot = styled( /** * An event which contains details about the process node. */ - event: LegacyEndpointEvent; + event: ResolverEvent; /** * projectionMatrix which can be used to convert `position` to screen coordinates. */ @@ -42,14 +43,13 @@ export const ProcessEventDot = styled( * Convert the position, which is in 'world' coordinates, to screen coordinates. */ const [left, top] = applyMatrix3(position, projectionMatrix); - const style = { left: (left - 20).toString() + 'px', top: (top - 20).toString() + 'px', }; return ( - - name: {event.endgame.process_name} + + name: {eventModel.eventName(event)}
x: {position[0]}
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.test.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.test.tsx index 711e4f9a5c537..6e83fc19a922e 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.test.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.test.tsx @@ -11,7 +11,7 @@ import { Provider } from 'react-redux'; import * as selectors from '../store/selectors'; import { storeFactory } from '../store'; import { Matrix3, ResolverAction, ResolverStore, SideEffectSimulator } from '../types'; -import { LegacyEndpointEvent } from '../../../../common/types'; +import { ResolverEvent } from '../../../../common/types'; import { SideEffectContext } from './side_effect_context'; import { applyMatrix3 } from '../lib/vector2'; import { sideEffectSimulator } from './side_effect_simulator'; @@ -133,9 +133,9 @@ describe('useCamera on an unpainted element', () => { expect(simulator.mock.requestAnimationFrame).not.toHaveBeenCalled(); }); describe('when the camera begins animation', () => { - let process: LegacyEndpointEvent; + let process: ResolverEvent; beforeEach(() => { - const events: LegacyEndpointEvent[] = []; + const events: ResolverEvent[] = []; const numberOfEvents: number = Math.floor(Math.random() * 10 + 1); for (let index = 0; index < numberOfEvents; index++) { @@ -164,7 +164,7 @@ describe('useCamera on an unpainted element', () => { act(() => { store.dispatch(serverResponseAction); }); - const processes: LegacyEndpointEvent[] = [ + const processes: ResolverEvent[] = [ ...selectors .processNodePositionsAndEdgeLineSegments(store.getState()) .processNodePositions.keys(), diff --git a/x-pack/test/functional/apps/endpoint/alerts.ts b/x-pack/test/functional/apps/endpoint/alerts.ts index 1ce7eb41e6690..759574702c0f1 100644 --- a/x-pack/test/functional/apps/endpoint/alerts.ts +++ b/x-pack/test/functional/apps/endpoint/alerts.ts @@ -18,8 +18,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await esArchiver.load('endpoint/alerts/api_feature'); await pageObjects.common.navigateToUrlWithBrowserHistory('endpoint', '/alerts'); }); - - it('loads in the browser', async () => { + it('loads the Alert List Page', async () => { await testSubjects.existOrFail('alertListPage'); }); it('contains the Alert List Page title', async () => { @@ -57,6 +56,12 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('loads the Alert List Flyout correctly', async () => { await testSubjects.existOrFail('alertDetailFlyout'); }); + + it('loads the resolver component and renders at least a single node', async () => { + await testSubjects.click('overviewResolverTab'); + await testSubjects.existOrFail('alertResolver'); + await testSubjects.existOrFail('resolverNode'); + }); }); after(async () => {