diff --git a/x-pack/plugins/endpoint/common/types.ts b/x-pack/plugins/endpoint/common/types.ts index d3df9722907598..6d904fda6f7478 100644 --- a/x-pack/plugins/endpoint/common/types.ts +++ b/x-pack/plugins/endpoint/common/types.ts @@ -115,6 +115,10 @@ export type AlertEvent = Immutable<{ score: number; }; }; + process?: { + unique_pid: number; + pid: number; + }; host: { hostname: string; ip: string; @@ -122,10 +126,9 @@ export type AlertEvent = Immutable<{ name: string; }; }; - process: { - pid: number; - }; thread: {}; + endpoint?: {}; + endgame?: {}; }>; /** @@ -186,22 +189,34 @@ export interface ESTotal { export type AlertHits = SearchResponse['hits']['hits']; export interface LegacyEndpointEvent { - '@timestamp': Date; + '@timestamp': number; endgame: { - event_type_full: string; - event_subtype_full: string; + pid?: number; + ppid?: number; + event_type_full?: string; + event_subtype_full?: string; + event_timestamp?: number; + event_type?: number; unique_pid: number; - unique_ppid: number; - serial_event_id: number; + unique_ppid?: number; + machine_id?: string; + process_name?: string; + process_path?: string; + timestamp_utc?: string; + serial_event_id?: number; }; agent: { id: string; type: string; + version: string; }; + process?: object; + rule?: object; + user?: object; } export interface EndpointEvent { - '@timestamp': Date; + '@timestamp': number; event: { category: string; type: string; @@ -216,6 +231,7 @@ export interface EndpointEvent { }; }; agent: { + id: string; type: string; }; } diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx index 7ab66817a08880..296587706e6acd 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx @@ -11,6 +11,7 @@ import { I18nProvider, FormattedMessage } from '@kbn/i18n/react'; import { Route, Switch, BrowserRouter } from 'react-router-dom'; import { Provider } from 'react-redux'; import { Store } from 'redux'; +import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { RouteCapture } from './view/route_capture'; import { appStoreFactory } from './store'; import { AlertIndex } from './view/alerts'; @@ -24,9 +25,7 @@ import { HeaderNavigation } from './components/header_nav'; export function renderApp(coreStart: CoreStart, { appBasePath, element }: AppMountParameters) { coreStart.http.get('/api/endpoint/hello-world'); const store = appStoreFactory(coreStart); - - ReactDOM.render(, element); - + ReactDOM.render(, element); return () => { ReactDOM.unmountComponentAtNode(element); }; @@ -35,35 +34,46 @@ export function renderApp(coreStart: CoreStart, { appBasePath, element }: AppMou interface RouterProps { basename: string; store: Store; + coreStart: CoreStart; } -const AppRoot: React.FunctionComponent = React.memo(({ basename, store }) => ( - - - - - - - ( -

- -

- )} - /> - - } /> - - ( - - )} - /> -
-
-
-
-
-)); +const AppRoot: React.FunctionComponent = React.memo( + ({ basename, store, coreStart: { http } }) => ( + + + + + + + + ( +

+ +

+ )} + /> + + + + ( + + )} + /> +
+
+
+
+
+
+ ) +); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/mock_alert_result_list.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/mock_alert_result_list.ts index b90f897ea2229c..8eadb3e7fb3dfd 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/mock_alert_result_list.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/mock_alert_result_list.ts @@ -43,6 +43,7 @@ export const mockAlertResultList: (options?: { }, process: { pid: 107, + unique_pid: 1, }, host: { hostname: 'HD-c15-bc09190a', 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 54add85f0fe04c..f217e3cda91913 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 @@ -9,13 +9,13 @@ import { createSelector, createStructuredSelector as createStructuredSelectorWithBadType, } from 'reselect'; -import { Immutable } from '../../../../../common/types'; import { AlertListState, AlertingIndexUIQueryParams, AlertsAPIQueryParams, CreateStructuredSelector, } from '../../types'; +import { Immutable, LegacyEndpointEvent } from '../../../../../common/types'; const createStructuredSelector: CreateStructuredSelector = createStructuredSelectorWithBadType; /** @@ -92,3 +92,24 @@ export const hasSelectedAlert: (state: AlertListState) => boolean = createSelect uiQueryParams, ({ selected_alert: selectedAlert }) => selectedAlert !== undefined ); + +/** + * Determine if the alert event is most likely compatible with LegacyEndpointEvent. + */ +function isAlertEventLegacyEndpointEvent(event: { endgame?: {} }): event is LegacyEndpointEvent { + return event.endgame !== undefined && 'unique_pid' in event.endgame; +} + +export const selectedEvent: ( + state: AlertListState +) => LegacyEndpointEvent | undefined = createSelector( + uiQueryParams, + alertListData, + ({ selected_alert: selectedAlert }, alertList) => { + const found = alertList.find(alert => alert.event.id === selectedAlert); + if (!found) { + return found; + } + return isAlertEventLegacyEndpointEvent(found) ? found : undefined; + } +); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.test.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.test.tsx index 37847553d512ad..fe362f21a178ea 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.test.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.test.tsx @@ -11,6 +11,7 @@ import { I18nProvider } from '@kbn/i18n/react'; import { AlertIndex } from './index'; import { appStoreFactory } from '../../store'; import { coreMock } from 'src/core/public/mocks'; +import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; import { fireEvent, waitForElement, act } from '@testing-library/react'; import { RouteCapture } from '../route_capture'; import { createMemoryHistory, MemoryHistory } from 'history'; @@ -44,6 +45,7 @@ describe('when on the alerting page', () => { * Create a store, with the middleware disabled. We don't want side effects being created by our code in this test. */ store = appStoreFactory(coreMock.createStart(), true); + /** * Render the test component, use this after setting up anything in `beforeEach`. */ @@ -56,13 +58,15 @@ describe('when on the alerting page', () => { */ return reactTestingLibrary.render( - - - - - - - + + + + + + + + + ); }; @@ -136,6 +140,9 @@ describe('when on the alerting page', () => { it('should show the flyout', async () => { await render().findByTestId('alertDetailFlyout'); }); + it('should render resolver', async () => { + await render().findByTestId('alertResolver'); + }); describe('when the user clicks the close button on the flyout', () => { let renderResult: reactTestingLibrary.RenderResult; beforeEach(async () => { diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.tsx index 6f88727575557d..3c229484ede4e0 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.tsx @@ -25,6 +25,7 @@ import { urlFromQueryParams } from './url_from_query_params'; import { AlertData } from '../../../../../common/types'; import * as selectors from '../../store/alerts/selectors'; import { useAlertListSelector } from './hooks/use_alerts_selector'; +import { AlertDetailResolver } from './resolver'; export const AlertIndex = memo(() => { const history = useHistory(); @@ -86,6 +87,7 @@ export const AlertIndex = memo(() => { const alertListData = useAlertListSelector(selectors.alertListData); const hasSelectedAlert = useAlertListSelector(selectors.hasSelectedAlert); const queryParams = useAlertListSelector(selectors.uiQueryParams); + const selectedEvent = useAlertListSelector(selectors.selectedEvent); const onChangeItemsPerPage = useCallback( newPageSize => { @@ -132,12 +134,11 @@ export const AlertIndex = memo(() => { } const row = alertListData[rowIndex % pageSize]; - if (columnId === 'alert_type') { return ( {i18n.translate( 'xpack.endpoint.application.endpoint.alerts.alertType.maliciousFileDescription', @@ -213,7 +214,9 @@ export const AlertIndex = memo(() => { - + + + )} 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 new file mode 100644 index 00000000000000..c7ef7f73dfe05b --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/resolver.tsx @@ -0,0 +1,35 @@ +/* + * 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 React from 'react'; +import styled from 'styled-components'; +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 { storeFactory } from '../../../../embeddables/resolver/store'; + +export const AlertDetailResolver = styled( + React.memo( + ({ className, selectedEvent }: { className?: string; selectedEvent?: LegacyEndpointEvent }) => { + const context = useKibana(); + const { store } = storeFactory(context); + return ( +
+ + + +
+ ); + } + ) +)` + height: 100%; + width: 100%; + display: flex; + flex-grow: 1; +`; 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 0eb3505096b4a5..6892bf11ecff2e 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 @@ -5,15 +5,16 @@ */ import { uniquePidForProcess, uniqueParentPidForProcess } from './process_event'; -import { IndexedProcessTree, ProcessEvent } from '../types'; +import { IndexedProcessTree } from '../types'; +import { LegacyEndpointEvent } from '../../../../common/types'; import { levelOrder as baseLevelOrder } from '../lib/tree_sequencers'; /** * Create a new IndexedProcessTree from an array of ProcessEvents */ -export function factory(processes: ProcessEvent[]): IndexedProcessTree { - const idToChildren = new Map(); - const idToValue = new Map(); +export function factory(processes: LegacyEndpointEvent[]): IndexedProcessTree { + const idToChildren = new Map(); + const idToValue = new Map(); for (const process of processes) { idToValue.set(uniquePidForProcess(process), process); @@ -35,7 +36,10 @@ export function factory(processes: ProcessEvent[]): IndexedProcessTree { /** * Returns an array with any children `ProcessEvent`s of the passed in `process` */ -export function children(tree: IndexedProcessTree, process: ProcessEvent): ProcessEvent[] { +export function children( + tree: IndexedProcessTree, + process: LegacyEndpointEvent +): LegacyEndpointEvent[] { const id = uniquePidForProcess(process); const processChildren = tree.idToChildren.get(id); return processChildren === undefined ? [] : processChildren; @@ -46,8 +50,8 @@ export function children(tree: IndexedProcessTree, process: ProcessEvent): Proce */ export function parent( tree: IndexedProcessTree, - childProcess: ProcessEvent -): ProcessEvent | undefined { + childProcess: LegacyEndpointEvent +): LegacyEndpointEvent | undefined { const uniqueParentPid = uniqueParentPidForProcess(childProcess); if (uniqueParentPid === undefined) { return undefined; @@ -70,7 +74,7 @@ export function root(tree: IndexedProcessTree) { if (size(tree) === 0) { return null; } - let current: ProcessEvent = tree.idToProcess.values().next().value; + let current: LegacyEndpointEvent = 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.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.test.ts index 3177671a30001d..3916396f7402ce 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.test.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.test.ts @@ -4,22 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ import { eventType } from './process_event'; -import { ProcessEvent } from '../types'; +import { LegacyEndpointEvent } from '../../../../common/types'; import { mockProcessEvent } from './process_event_test_helpers'; describe('process event', () => { describe('eventType', () => { - let event: ProcessEvent; + let event: LegacyEndpointEvent; beforeEach(() => { event = mockProcessEvent({ - data_buffer: { - node_id: 1, + endgame: { + unique_pid: 1, event_type_full: 'process_event', }, }); }); it("returns the right value when the subType is 'creation_event'", () => { - event.data_buffer.event_subtype_full = 'creation_event'; + event.endgame.event_subtype_full = 'creation_event'; expect(eventType(event)).toEqual('processCreated'); }); }); 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 c8496b8e6e7a5d..876168d2ed96ac 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,23 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ProcessEvent } from '../types'; +import { LegacyEndpointEvent } from '../../../../common/types'; /** * Returns true if the process's eventType is either 'processCreated' or 'processRan'. * Resolver will only render 'graphable' process events. */ -export function isGraphableProcess(event: ProcessEvent) { - return eventType(event) === 'processCreated' || eventType(event) === 'processRan'; +export function isGraphableProcess(passedEvent: LegacyEndpointEvent) { + return eventType(passedEvent) === 'processCreated' || eventType(passedEvent) === 'processRan'; } /** * Returns a custom event type for a process event based on the event's metadata. */ -export function eventType(event: ProcessEvent) { +export function eventType(passedEvent: LegacyEndpointEvent) { const { - data_buffer: { event_type_full: type, event_subtype_full: subType }, - } = event; + endgame: { event_type_full: type, event_subtype_full: subType }, + } = passedEvent; if (type === 'process_event') { if (subType === 'creation_event' || subType === 'fork_event' || subType === 'exec_event') { @@ -41,13 +41,13 @@ export function eventType(event: ProcessEvent) { /** * Returns the process event's pid */ -export function uniquePidForProcess(event: ProcessEvent) { - return event.data_buffer.node_id; +export function uniquePidForProcess(event: LegacyEndpointEvent) { + return event.endgame.unique_pid; } /** * Returns the process event's parent pid */ -export function uniqueParentPidForProcess(event: ProcessEvent) { - return event.data_buffer.source_id; +export function uniqueParentPidForProcess(event: LegacyEndpointEvent) { + return event.endgame.unique_ppid; } diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event_test_helpers.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event_test_helpers.ts index 9a6f19adcc1017..e88837d325108d 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event_test_helpers.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event_test_helpers.ts @@ -4,33 +4,46 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ProcessEvent } from '../types'; -type DeepPartial = { [K in keyof T]?: DeepPartial }; +import { LegacyEndpointEvent } from '../../../../common/types'; +type DeepPartial = { [K in keyof T]?: DeepPartial }; /** * Creates a mock process event given the 'parts' argument, which can * include all or some process event fields as determined by the ProcessEvent type. * The only field that must be provided is the event's 'node_id' field. * The other fields are populated by the function unless provided in 'parts' */ -export function mockProcessEvent( - parts: { - data_buffer: { node_id: ProcessEvent['data_buffer']['node_id'] }; - } & DeepPartial -): ProcessEvent { - const { data_buffer: dataBuffer } = parts; +export function mockProcessEvent(parts: { + endgame: { + unique_pid: LegacyEndpointEvent['endgame']['unique_pid']; + unique_ppid?: LegacyEndpointEvent['endgame']['unique_ppid']; + process_name?: LegacyEndpointEvent['endgame']['process_name']; + event_subtype_full?: LegacyEndpointEvent['endgame']['event_subtype_full']; + event_type_full?: LegacyEndpointEvent['endgame']['event_type_full']; + } & DeepPartial; +}): LegacyEndpointEvent { + const { endgame: dataBuffer } = parts; return { - event_timestamp: 1, - event_type: 1, - machine_id: '', - ...parts, - data_buffer: { - timestamp_utc: '2019-09-24 01:47:47Z', + endgame: { + ...dataBuffer, + event_timestamp: 1, + event_type: 1, + unique_ppid: 0, + unique_pid: 1, + machine_id: '', event_subtype_full: 'creation_event', event_type_full: 'process_event', process_name: '', process_path: '', - ...dataBuffer, + timestamp_utc: '', + serial_event_id: 1, + }, + '@timestamp': 1582233383000, + agent: { + type: '', + id: '', + version: '', }, + ...parts, }; } 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 25f196c76a2904..ecba0ec404d44d 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/actions.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/actions.ts @@ -3,9 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { ProcessEvent } from '../types'; import { CameraAction } from './camera'; import { DataAction } from './data'; +import { LegacyEndpointEvent } 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: ProcessEvent; + readonly process: LegacyEndpointEvent; /** * The time (since epoch in milliseconds) when the action was dispatched. */ @@ -24,4 +24,29 @@ interface UserBroughtProcessIntoView { }; } -export type ResolverAction = CameraAction | DataAction | UserBroughtProcessIntoView; +/** + * Used when the alert list selects an alert and the flyout shows resolver. + */ +interface UserChangedSelectedEvent { + readonly type: 'userChangedSelectedEvent'; + readonly payload: { + /** + * Optional because they could have unselected the event. + */ + selectedEvent?: LegacyEndpointEvent; + }; +} + +/** + * Triggered by middleware when the data for resolver needs to be loaded. Used to set state in redux to 'loading'. + */ +interface AppRequestedResolverData { + readonly type: 'appRequestedResolverData'; +} + +export type ResolverAction = + | CameraAction + | DataAction + | UserBroughtProcessIntoView + | UserChangedSelectedEvent + | AppRequestedResolverData; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/__snapshots__/graphing.test.ts.snap b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/__snapshots__/graphing.test.ts.snap index 1dc17054b9f47c..b88652097eb5c5 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/__snapshots__/graphing.test.ts.snap +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/__snapshots__/graphing.test.ts.snap @@ -12,17 +12,18 @@ Object { "edgeLineSegments": Array [], "processNodePositions": Map { Object { - "data_buffer": Object { + "@timestamp": 1582233383000, + "agent": Object { + "id": "", + "type": "", + "version": "", + }, + "endgame": Object { "event_subtype_full": "creation_event", "event_type_full": "process_event", - "node_id": 0, "process_name": "", - "process_path": "", - "timestamp_utc": "2019-09-24 01:47:47Z", + "unique_pid": 0, }, - "event_timestamp": 1, - "event_type": 1, - "machine_id": "", } => Array [ 0, -0.8164965809277259, @@ -167,136 +168,137 @@ Object { ], "processNodePositions": Map { Object { - "data_buffer": Object { + "@timestamp": 1582233383000, + "agent": Object { + "id": "", + "type": "", + "version": "", + }, + "endgame": Object { "event_subtype_full": "creation_event", "event_type_full": "process_event", - "node_id": 0, "process_name": "", - "process_path": "", - "timestamp_utc": "2019-09-24 01:47:47Z", + "unique_pid": 0, }, - "event_timestamp": 1, - "event_type": 1, - "machine_id": "", } => Array [ 0, -0.8164965809277259, ], Object { - "data_buffer": Object { + "@timestamp": 1582233383000, + "agent": Object { + "id": "", + "type": "", + "version": "", + }, + "endgame": Object { "event_subtype_full": "already_running", "event_type_full": "process_event", - "node_id": 1, - "process_name": "", - "process_path": "", - "source_id": 0, - "timestamp_utc": "2019-09-24 01:47:47Z", + "unique_pid": 1, + "unique_ppid": 0, }, - "event_timestamp": 1, - "event_type": 1, - "machine_id": "", } => Array [ 0, -82.46615467370032, ], Object { - "data_buffer": Object { + "@timestamp": 1582233383000, + "agent": Object { + "id": "", + "type": "", + "version": "", + }, + "endgame": Object { "event_subtype_full": "creation_event", "event_type_full": "process_event", - "node_id": 2, - "process_name": "", - "process_path": "", - "source_id": 0, - "timestamp_utc": "2019-09-24 01:47:47Z", + "unique_pid": 2, + "unique_ppid": 0, }, - "event_timestamp": 1, - "event_type": 1, - "machine_id": "", } => Array [ 141.4213562373095, -0.8164965809277259, ], Object { - "data_buffer": Object { + "@timestamp": 1582233383000, + "agent": Object { + "id": "", + "type": "", + "version": "", + }, + "endgame": Object { "event_subtype_full": "creation_event", "event_type_full": "process_event", - "node_id": 3, - "process_name": "", - "process_path": "", - "source_id": 1, - "timestamp_utc": "2019-09-24 01:47:47Z", + "unique_pid": 3, + "unique_ppid": 1, }, - "event_timestamp": 1, - "event_type": 1, - "machine_id": "", } => Array [ 35.35533905932738, -143.70339824327976, ], Object { - "data_buffer": Object { + "@timestamp": 1582233383000, + "agent": Object { + "id": "", + "type": "", + "version": "", + }, + "endgame": Object { "event_subtype_full": "creation_event", "event_type_full": "process_event", - "node_id": 4, - "process_name": "", - "process_path": "", - "source_id": 1, - "timestamp_utc": "2019-09-24 01:47:47Z", + "unique_pid": 4, + "unique_ppid": 1, }, - "event_timestamp": 1, - "event_type": 1, - "machine_id": "", } => Array [ 106.06601717798213, -102.87856919689347, ], Object { - "data_buffer": Object { + "@timestamp": 1582233383000, + "agent": Object { + "id": "", + "type": "", + "version": "", + }, + "endgame": Object { "event_subtype_full": "creation_event", "event_type_full": "process_event", - "node_id": 5, - "process_name": "", - "process_path": "", - "source_id": 2, - "timestamp_utc": "2019-09-24 01:47:47Z", + "unique_pid": 5, + "unique_ppid": 2, }, - "event_timestamp": 1, - "event_type": 1, - "machine_id": "", } => Array [ 176.7766952966369, -62.053740150507174, ], Object { - "data_buffer": Object { + "@timestamp": 1582233383000, + "agent": Object { + "id": "", + "type": "", + "version": "", + }, + "endgame": Object { "event_subtype_full": "creation_event", "event_type_full": "process_event", - "node_id": 6, - "process_name": "", - "process_path": "", - "source_id": 2, - "timestamp_utc": "2019-09-24 01:47:47Z", + "unique_pid": 6, + "unique_ppid": 2, }, - "event_timestamp": 1, - "event_type": 1, - "machine_id": "", } => Array [ 247.48737341529164, -21.228911104120883, ], Object { - "data_buffer": Object { + "@timestamp": 1582233383000, + "agent": Object { + "id": "", + "type": "", + "version": "", + }, + "endgame": Object { "event_subtype_full": "creation_event", "event_type_full": "process_event", - "node_id": 7, - "process_name": "", - "process_path": "", - "source_id": 6, - "timestamp_utc": "2019-09-24 01:47:47Z", + "unique_pid": 7, + "unique_ppid": 6, }, - "event_timestamp": 1, - "event_type": 1, - "machine_id": "", } => Array [ 318.1980515339464, -62.05374015050717, @@ -321,34 +323,35 @@ Object { ], "processNodePositions": Map { Object { - "data_buffer": Object { + "@timestamp": 1582233383000, + "agent": Object { + "id": "", + "type": "", + "version": "", + }, + "endgame": Object { "event_subtype_full": "creation_event", "event_type_full": "process_event", - "node_id": 0, "process_name": "", - "process_path": "", - "timestamp_utc": "2019-09-24 01:47:47Z", + "unique_pid": 0, }, - "event_timestamp": 1, - "event_type": 1, - "machine_id": "", } => Array [ 0, -0.8164965809277259, ], Object { - "data_buffer": Object { + "@timestamp": 1582233383000, + "agent": Object { + "id": "", + "type": "", + "version": "", + }, + "endgame": Object { "event_subtype_full": "already_running", "event_type_full": "process_event", - "node_id": 1, - "process_name": "", - "process_path": "", - "source_id": 0, - "timestamp_utc": "2019-09-24 01:47:47Z", + "unique_pid": 1, + "unique_ppid": 0, }, - "event_timestamp": 1, - "event_type": 1, - "machine_id": "", } => Array [ 70.71067811865476, -41.641325627314025, 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 900b9bda571dae..f34d7c08ce08cc 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 { ProcessEvent } from '../../types'; +import { LegacyEndpointEvent } from '../../../../../common/types'; interface ServerReturnedResolverData { readonly type: 'serverReturnedResolverData'; readonly payload: { readonly data: { readonly result: { - readonly search_results: readonly ProcessEvent[]; + readonly search_results: readonly LegacyEndpointEvent[]; }; }; }; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts index fac70433f14b29..f01136fe20ebf9 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts @@ -7,20 +7,21 @@ import { Store, createStore } from 'redux'; import { DataAction } from './action'; import { dataReducer } from './reducer'; -import { DataState, ProcessEvent } from '../../types'; +import { DataState } from '../../types'; +import { LegacyEndpointEvent } from '../../../../../common/types'; import { graphableProcesses, processNodePositionsAndEdgeLineSegments } from './selectors'; import { mockProcessEvent } from '../../models/process_event_test_helpers'; describe('resolver graph layout', () => { - let processA: ProcessEvent; - let processB: ProcessEvent; - let processC: ProcessEvent; - let processD: ProcessEvent; - let processE: ProcessEvent; - let processF: ProcessEvent; - let processG: ProcessEvent; - let processH: ProcessEvent; - let processI: ProcessEvent; + let processA: LegacyEndpointEvent; + let processB: LegacyEndpointEvent; + let processC: LegacyEndpointEvent; + let processD: LegacyEndpointEvent; + let processE: LegacyEndpointEvent; + let processF: LegacyEndpointEvent; + let processG: LegacyEndpointEvent; + let processH: LegacyEndpointEvent; + let processI: LegacyEndpointEvent; let store: Store; beforeEach(() => { @@ -37,75 +38,75 @@ describe('resolver graph layout', () => { * */ processA = mockProcessEvent({ - data_buffer: { + endgame: { process_name: '', event_type_full: 'process_event', event_subtype_full: 'creation_event', - node_id: 0, + unique_pid: 0, }, }); processB = mockProcessEvent({ - data_buffer: { + endgame: { event_type_full: 'process_event', event_subtype_full: 'already_running', - node_id: 1, - source_id: 0, + unique_pid: 1, + unique_ppid: 0, }, }); processC = mockProcessEvent({ - data_buffer: { + endgame: { event_type_full: 'process_event', event_subtype_full: 'creation_event', - node_id: 2, - source_id: 0, + unique_pid: 2, + unique_ppid: 0, }, }); processD = mockProcessEvent({ - data_buffer: { + endgame: { event_type_full: 'process_event', event_subtype_full: 'creation_event', - node_id: 3, - source_id: 1, + unique_pid: 3, + unique_ppid: 1, }, }); processE = mockProcessEvent({ - data_buffer: { + endgame: { event_type_full: 'process_event', event_subtype_full: 'creation_event', - node_id: 4, - source_id: 1, + unique_pid: 4, + unique_ppid: 1, }, }); processF = mockProcessEvent({ - data_buffer: { + endgame: { event_type_full: 'process_event', event_subtype_full: 'creation_event', - node_id: 5, - source_id: 2, + unique_pid: 5, + unique_ppid: 2, }, }); processG = mockProcessEvent({ - data_buffer: { + endgame: { event_type_full: 'process_event', event_subtype_full: 'creation_event', - node_id: 6, - source_id: 2, + unique_pid: 6, + unique_ppid: 2, }, }); processH = mockProcessEvent({ - data_buffer: { + endgame: { event_type_full: 'process_event', event_subtype_full: 'creation_event', - node_id: 7, - source_id: 6, + unique_pid: 7, + unique_ppid: 6, }, }); processI = mockProcessEvent({ - data_buffer: { + endgame: { event_type_full: 'process_event', event_subtype_full: 'termination_event', - node_id: 8, - source_id: 0, + unique_pid: 8, + unique_ppid: 0, }, }); store = createStore(dataReducer, undefined); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/reducer.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/reducer.ts index 848d814808bac4..a3184389a794e8 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/reducer.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/reducer.ts @@ -6,11 +6,11 @@ import { Reducer } from 'redux'; import { DataState, ResolverAction } from '../../types'; -import { sampleData } from './sample'; function initialState(): DataState { return { - results: sampleData.data.result.search_results, + results: [], + isLoading: false, }; } @@ -24,6 +24,12 @@ export const dataReducer: Reducer = (state = initialS return { ...state, results: search_results, + isLoading: false, + }; + } else if (action.type === 'appRequestedResolverData') { + return { + ...state, + isLoading: true, }; } else { return state; 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 75b477dd7c7fcd..304abbb06880b2 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 @@ -7,7 +7,6 @@ import { createSelector } from 'reselect'; import { DataState, - ProcessEvent, IndexedProcessTree, ProcessWidths, ProcessPositions, @@ -15,6 +14,7 @@ import { ProcessWithWidthMetadata, Matrix3, } from '../../types'; +import { LegacyEndpointEvent } from '../../../../../common/types'; import { Vector2 } from '../../types'; import { add as vector2Add, applyMatrix3 } from '../../lib/vector2'; import { isGraphableProcess } from '../../models/process_event'; @@ -29,6 +29,10 @@ import { const unit = 100; const distanceBetweenNodesInUnits = 1; +export function isLoading(state: DataState) { + return state.isLoading; +} + /** * An isometric projection is a method for representing three dimensional objects in 2 dimensions. * More information about isometric projections can be found here https://en.wikipedia.org/wiki/Isometric_projection. @@ -108,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; @@ -309,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: ProcessEvent | undefined; + let lastProcessedParentNode: LegacyEndpointEvent | undefined; /** * Nodes are positioned relative to their siblings. We walk this in level order, so we handle * children left -> right. @@ -420,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/index.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/index.ts index b17572bbc4ab46..2a20c73347348f 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/index.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/index.ts @@ -6,17 +6,21 @@ import { createStore, applyMiddleware, Store } from 'redux'; import { composeWithDevTools } from 'redux-devtools-extension/developmentOnly'; +import { KibanaReactContextValue } from '../../../../../../../src/plugins/kibana_react/public'; import { ResolverAction, ResolverState } from '../types'; +import { EndpointPluginServices } from '../../../plugin'; import { resolverReducer } from './reducer'; +import { resolverMiddlewareFactory } from './middleware'; -export const storeFactory = (): { store: Store } => { +export const storeFactory = ( + context?: KibanaReactContextValue +): { store: Store } => { const actionsBlacklist: Array = ['userMovedPointer']; const composeEnhancers = composeWithDevTools({ name: 'Resolver', actionsBlacklist, }); - - const middlewareEnhancer = applyMiddleware(); + const middlewareEnhancer = applyMiddleware(resolverMiddlewareFactory(context)); const store = createStore(resolverReducer, composeEnhancers(middlewareEnhancer)); return { 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 8808160c9c631a..9f06643626f500 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/methods.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/methods.ts @@ -6,7 +6,8 @@ import { animatePanning } from './camera/methods'; import { processNodePositionsAndEdgeLineSegments } from './selectors'; -import { ResolverState, ProcessEvent } from '../types'; +import { ResolverState } from '../types'; +import { LegacyEndpointEvent } from '../../../../common/types'; const animationDuration = 1000; @@ -16,7 +17,7 @@ const animationDuration = 1000; export function animateProcessIntoView( state: ResolverState, startTime: number, - process: ProcessEvent + process: LegacyEndpointEvent ): 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 new file mode 100644 index 00000000000000..900aece60618d3 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/middleware.ts @@ -0,0 +1,45 @@ +/* + * 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 { Dispatch, MiddlewareAPI } from 'redux'; +import { KibanaReactContextValue } from '../../../../../../../src/plugins/kibana_react/public'; +import { EndpointPluginServices } from '../../../plugin'; +import { ResolverState, ResolverAction } from '../types'; + +type MiddlewareFactory = ( + context?: KibanaReactContextValue +) => ( + api: MiddlewareAPI, S> +) => (next: Dispatch) => (action: ResolverAction) => unknown; + +export const resolverMiddlewareFactory: MiddlewareFactory = context => { + return api => next => async (action: ResolverAction) => { + next(action); + if (action.type === 'userChangedSelectedEvent') { + if (context?.services.http) { + 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]; + api.dispatch({ + type: 'serverReturnedResolverData', + payload: { data: { result: { search_results: response } } }, + }); + } + } + }; +}; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts index 25d08a8c347ed5..708eb684ebd3e6 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts @@ -68,6 +68,11 @@ function dataStateSelector(state: ResolverState) { return state.data; } +/** + * Whether or not the resolver is pending fetching data + */ +export const isLoading = composeSelectors(dataStateSelector, dataSelectors.isLoading); + /** * Calls the `secondSelector` with the result of the `selector`. Use this when re-exporting a * concern-specific selector. `selector` should return the concern-specific state. diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts index 6c6936d377deac..4c2a1ea5ac21f2 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts @@ -8,6 +8,7 @@ import { Store } from 'redux'; import { ResolverAction } from './store/actions'; export { ResolverAction } from './store/actions'; +import { LegacyEndpointEvent } from '../../../common/types'; /** * Redux state for the Resolver feature. Properties on this interface are populated via multiple reducers using redux's `combineReducers`. @@ -114,7 +115,8 @@ export type CameraState = { * State for `data` reducer which handles receiving Resolver data from the backend. */ export interface DataState { - readonly results: readonly ProcessEvent[]; + readonly results: readonly LegacyEndpointEvent[]; + isLoading: boolean; } export type Vector2 = readonly [number, number]; @@ -182,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. */ @@ -206,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: ProcessEvent; + process: LegacyEndpointEvent; width: number; } & ( | { - parent: ProcessEvent; + parent: LegacyEndpointEvent; parentWidth: number; isOnlyChild: boolean; firstChildWidth: number; 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 d71a4d87b7eab6..52a0872f269f5a 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -4,15 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { useSelector } from 'react-redux'; +import React, { useLayoutEffect } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; import styled from 'styled-components'; +import { EuiLoadingSpinner } from '@elastic/eui'; import * as selectors from '../store/selectors'; import { EdgeLine } from './edge_line'; import { Panel } from './panel'; 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'; const StyledPanel = styled(Panel)` position: absolute; @@ -31,35 +34,57 @@ const StyledGraphControls = styled(GraphControls)` `; export const Resolver = styled( - React.memo(function Resolver({ className }: { className?: string }) { + React.memo(function Resolver({ + className, + selectedEvent, + }: { + className?: string; + selectedEvent?: LegacyEndpointEvent; + }) { const { processNodePositions, edgeLineSegments } = useSelector( selectors.processNodePositionsAndEdgeLineSegments ); + const dispatch: (action: ResolverAction) => unknown = useDispatch(); const { projectionMatrix, ref, onMouseDown } = useCamera(); + const isLoading = useSelector(selectors.isLoading); + useLayoutEffect(() => { + dispatch({ + type: 'userChangedSelectedEvent', + payload: { selectedEvent }, + }); + }, [dispatch, selectedEvent]); return (
-
- {Array.from(processNodePositions).map(([processEvent, position], index) => ( - - ))} - {edgeLineSegments.map(([startPosition, endPosition], index) => ( - - ))} -
- - + {isLoading ? ( +
+ +
+ ) : ( + <> +
+ {Array.from(processNodePositions).map(([processEvent, position], index) => ( + + ))} + {edgeLineSegments.map(([startPosition, endPosition], index) => ( + + ))} +
+ + + + )}
); }) @@ -72,6 +97,12 @@ export const Resolver = styled( display: flex; flex-grow: 1; } + .loading-container { + display: flex; + align-items: center; + justify-content: center; + flex-grow: 1; + } /** * The placeholder components use absolute positioning. */ 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 c75b73b4bceafd..84c299698bb32c 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,7 @@ 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 { ProcessEvent } from '../types'; +import { LegacyEndpointEvent } from '../../../../common/types'; import { useResolverDispatch } from './use_resolver_dispatch'; import * as selectors from '../store/selectors'; @@ -38,7 +38,7 @@ export const Panel = memo(function Event({ className }: { className?: string }) interface ProcessTableView { name: string; timestamp?: Date; - event: ProcessEvent; + event: LegacyEndpointEvent; } const { processNodePositions } = useSelector(selectors.processNodePositionsAndEdgeLineSegments); @@ -47,11 +47,16 @@ export const Panel = memo(function Event({ className }: { className?: string }) const processTableView: ProcessTableView[] = useMemo( () => [...processNodePositions.keys()].map(processEvent => { - const { data_buffer } = processEvent; - const date = new Date(data_buffer.timestamp_utc); + let dateTime; + if (processEvent.endgame.timestamp_utc) { + const date = new Date(processEvent.endgame.timestamp_utc); + if (isFinite(date.getTime())) { + dateTime = date; + } + } return { - name: data_buffer.process_name, - timestamp: isFinite(date.getTime()) ? date : undefined, + name: processEvent.endgame.process_name ? processEvent.endgame.process_name : '', + timestamp: dateTime, event: processEvent, }; }), 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 384fbf90ed9847..034780c7ba14c8 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 @@ -7,7 +7,8 @@ import React from 'react'; import styled from 'styled-components'; import { applyMatrix3 } from '../lib/vector2'; -import { Vector2, ProcessEvent, Matrix3 } from '../types'; +import { Vector2, Matrix3 } from '../types'; +import { LegacyEndpointEvent } from '../../../../common/types'; /** * A placeholder view for a process node. @@ -31,7 +32,7 @@ export const ProcessEventDot = styled( /** * An event which contains details about the process node. */ - event: ProcessEvent; + event: LegacyEndpointEvent; /** * projectionMatrix which can be used to convert `position` to screen coordinates. */ @@ -48,7 +49,7 @@ export const ProcessEventDot = styled( }; return ( - name: {event.data_buffer.process_name} + name: {event.endgame.process_name}
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 f4abb51f062f2a..1948c6cae505bc 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 @@ -10,16 +10,12 @@ import { useCamera } from './use_camera'; import { Provider } from 'react-redux'; import * as selectors from '../store/selectors'; import { storeFactory } from '../store'; -import { - Matrix3, - ResolverAction, - ResolverStore, - ProcessEvent, - SideEffectSimulator, -} from '../types'; +import { Matrix3, ResolverAction, ResolverStore, SideEffectSimulator } from '../types'; +import { LegacyEndpointEvent } from '../../../../common/types'; import { SideEffectContext } from './side_effect_context'; import { applyMatrix3 } from '../lib/vector2'; import { sideEffectSimulator } from './side_effect_simulator'; +import { mockProcessEvent } from '../models/process_event_test_helpers'; describe('useCamera on an unpainted element', () => { let element: HTMLElement; @@ -28,6 +24,7 @@ describe('useCamera on an unpainted element', () => { let reactRenderResult: RenderResult; let store: ResolverStore; let simulator: SideEffectSimulator; + beforeEach(async () => { ({ store } = storeFactory()); @@ -136,17 +133,45 @@ describe('useCamera on an unpainted element', () => { expect(simulator.mock.requestAnimationFrame).not.toHaveBeenCalled(); }); describe('when the camera begins animation', () => { - let process: ProcessEvent; + let process: LegacyEndpointEvent; beforeEach(() => { - // At this time, processes are provided via mock data. In the future, this test will have to provide those mocks. - const processes: ProcessEvent[] = [ + const events: LegacyEndpointEvent[] = []; + const numberOfEvents: number = Math.floor(Math.random() * 10 + 1); + + for (let index = 0; index < numberOfEvents; index++) { + const uniquePpid = index === 0 ? undefined : index - 1; + events.push( + mockProcessEvent({ + endgame: { + unique_pid: index, + unique_ppid: uniquePpid, + event_type_full: 'process_event', + event_subtype_full: 'creation_event', + }, + }) + ); + } + const serverResponseAction: ResolverAction = { + type: 'serverReturnedResolverData', + payload: { + data: { + result: { + search_results: events, + }, + }, + }, + }; + act(() => { + store.dispatch(serverResponseAction); + }); + const processes: LegacyEndpointEvent[] = [ ...selectors .processNodePositionsAndEdgeLineSegments(store.getState()) .processNodePositions.keys(), ]; process = processes[processes.length - 1]; simulator.controls.time = 0; - const action: ResolverAction = { + const cameraAction: ResolverAction = { type: 'userBroughtProcessIntoView', payload: { time: simulator.controls.time, @@ -154,7 +179,7 @@ describe('useCamera on an unpainted element', () => { }, }; act(() => { - store.dispatch(action); + store.dispatch(cameraAction); }); }); diff --git a/x-pack/plugins/endpoint/public/plugin.ts b/x-pack/plugins/endpoint/public/plugin.ts index 355364253b2a52..0e10fe680e9f04 100644 --- a/x-pack/plugins/endpoint/public/plugin.ts +++ b/x-pack/plugins/endpoint/public/plugin.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Plugin, CoreSetup, AppMountParameters } from 'kibana/public'; +import { Plugin, CoreSetup, AppMountParameters, CoreStart } from 'kibana/public'; import { IEmbeddableSetup } from 'src/plugins/embeddable/public'; import { i18n } from '@kbn/i18n'; import { ResolverEmbeddableFactory } from './embeddables/resolver'; @@ -17,6 +17,15 @@ export interface EndpointPluginSetupDependencies { export interface EndpointPluginStartDependencies {} // eslint-disable-line @typescript-eslint/no-empty-interface +/** + * Functionality that the endpoint plugin uses from core. + */ +export interface EndpointPluginServices extends Partial { + http: CoreStart['http']; + overlays: CoreStart['overlays'] | undefined; + notifications: CoreStart['notifications'] | undefined; +} + export class EndpointPlugin implements Plugin< diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/children.test.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/children.test.ts index 2dd2e0c2d1d5f8..08a906e2884d68 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/queries/children.test.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/queries/children.test.ts @@ -8,7 +8,7 @@ import { EndpointAppConstants } from '../../../../common/types'; describe('children events query', () => { it('generates the correct legacy queries', () => { - const timestamp = new Date(); + const timestamp = new Date().getTime(); expect( new ChildrenQuery('awesome-id', { size: 1, timestamp, eventID: 'foo' }).build('5') ).toStrictEqual({ @@ -38,7 +38,7 @@ describe('children events query', () => { }, }, }, - search_after: [timestamp.getTime(), 'foo'], + search_after: [timestamp, 'foo'], size: 1, sort: [{ '@timestamp': 'asc' }, { 'endgame.serial_event_id': 'asc' }], }, @@ -47,7 +47,7 @@ describe('children events query', () => { }); it('generates the correct non-legacy queries', () => { - const timestamp = new Date(); + const timestamp = new Date().getTime(); expect( new ChildrenQuery(undefined, { size: 1, timestamp, eventID: 'bar' }).build('baz') @@ -84,7 +84,7 @@ describe('children events query', () => { }, }, }, - search_after: [timestamp.getTime(), 'bar'], + search_after: [timestamp, 'bar'], size: 1, sort: [{ '@timestamp': 'asc' }, { 'event.id': 'asc' }], }, diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/related_events.test.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/related_events.test.ts index 8ef680a168310d..a91c87274b8dd5 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/queries/related_events.test.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/queries/related_events.test.ts @@ -8,7 +8,7 @@ import { EndpointAppConstants } from '../../../../common/types'; describe('related events query', () => { it('generates the correct legacy queries', () => { - const timestamp = new Date(); + const timestamp = new Date().getTime(); expect( new RelatedEventsQuery('awesome-id', { size: 1, timestamp, eventID: 'foo' }).build('5') ).toStrictEqual({ @@ -39,7 +39,7 @@ describe('related events query', () => { }, }, }, - search_after: [timestamp.getTime(), 'foo'], + search_after: [timestamp, 'foo'], size: 1, sort: [{ '@timestamp': 'asc' }, { 'endgame.serial_event_id': 'asc' }], }, @@ -48,7 +48,7 @@ describe('related events query', () => { }); it('generates the correct non-legacy queries', () => { - const timestamp = new Date(); + const timestamp = new Date().getTime(); expect( new RelatedEventsQuery(undefined, { size: 1, timestamp, eventID: 'bar' }).build('baz') @@ -86,7 +86,7 @@ describe('related events query', () => { }, }, }, - search_after: [timestamp.getTime(), 'bar'], + search_after: [timestamp, 'bar'], size: 1, sort: [{ '@timestamp': 'asc' }, { 'event.id': 'asc' }], }, diff --git a/x-pack/plugins/endpoint/server/routes/resolver/utils/pagination.ts b/x-pack/plugins/endpoint/server/routes/resolver/utils/pagination.ts index 33eb6984793087..5a64f3ff9ddb61 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/utils/pagination.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/utils/pagination.ts @@ -11,12 +11,12 @@ import { JsonObject } from '../../../../../../../src/plugins/kibana_utils/public export interface PaginationParams { size: number; - timestamp?: Date; + timestamp?: number; eventID?: string; } interface PaginationCursor { - timestamp: Date; + timestamp: number; eventID: string; } @@ -35,7 +35,7 @@ function urlDecodeCursor(value: string): PaginationCursor { const { timestamp, eventID } = JSON.parse(data); // take some extra care to only grab the things we want // convert the timestamp string to date object - return { timestamp: new Date(timestamp), eventID }; + return { timestamp, eventID }; } export function getPaginationParams(limit: number, after?: string): PaginationParams { @@ -62,7 +62,7 @@ export function paginate(pagination: PaginationParams, field: string, query: Jso query.aggs = { total: { value_count: { field } } }; query.size = size; if (timestamp && eventID) { - query.search_after = [timestamp.getTime(), eventID] as Array; + query.search_after = [timestamp, eventID] as Array; } return query; }