[Endpoint] add resolver middleware (#58288) (#59180)

Co-authored-by: kqualters-elastic <56408403+kqualters-elastic@users.noreply.github.com>
Co-authored-by: Robert Austin <robert.austin@elastic.co>
This commit is contained in:
Davis Plumlee 2020-03-04 13:23:55 -05:00 committed by GitHub
parent ce8df3b5cc
commit 77f023f36f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 578 additions and 301 deletions

View file

@ -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?: {};
}>;
/**
@ -184,22 +187,34 @@ export interface ESTotal {
export type AlertHits = SearchResponse<AlertEvent>['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;
@ -214,6 +229,7 @@ export interface EndpointEvent {
};
};
agent: {
id: string;
type: string;
};
}

View file

@ -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(<AppRoot basename={appBasePath} store={store} />, element);
ReactDOM.render(<AppRoot basename={appBasePath} store={store} coreStart={coreStart} />, 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<RouterProps> = React.memo(({ basename, store }) => (
<Provider store={store}>
<I18nProvider>
<BrowserRouter basename={basename}>
<RouteCapture>
<HeaderNavigation basename={basename} />
<Switch>
<Route
exact
path="/"
render={() => (
<h1 data-test-subj="welcomeTitle">
<FormattedMessage id="xpack.endpoint.welcomeTitle" defaultMessage="Hello World" />
</h1>
)}
/>
<Route path="/management" component={ManagementList} />
<Route path="/alerts" render={() => <AlertIndex />} />
<Route path="/policy" exact component={PolicyList} />
<Route
render={() => (
<FormattedMessage id="xpack.endpoint.notFound" defaultMessage="Page Not Found" />
)}
/>
</Switch>
</RouteCapture>
</BrowserRouter>
</I18nProvider>
</Provider>
));
const AppRoot: React.FunctionComponent<RouterProps> = React.memo(
({ basename, store, coreStart: { http } }) => (
<Provider store={store}>
<KibanaContextProvider services={{ http }}>
<I18nProvider>
<BrowserRouter basename={basename}>
<RouteCapture>
<HeaderNavigation basename={basename} />
<Switch>
<Route
exact
path="/"
render={() => (
<h1 data-test-subj="welcomeTitle">
<FormattedMessage
id="xpack.endpoint.welcomeTitle"
defaultMessage="Hello World"
/>
</h1>
)}
/>
<Route path="/management" component={ManagementList} />
<Route path="/alerts" component={AlertIndex} />
<Route path="/policy" exact component={PolicyList} />
<Route
render={() => (
<FormattedMessage
id="xpack.endpoint.notFound"
defaultMessage="Page Not Found"
/>
)}
/>
</Switch>
</RouteCapture>
</BrowserRouter>
</I18nProvider>
</KibanaContextProvider>
</Provider>
)
);

View file

@ -43,6 +43,7 @@ export const mockAlertResultList: (options?: {
},
process: {
pid: 107,
unique_pid: 1,
},
host: {
hostname: 'HD-c15-bc09190a',

View file

@ -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;
}
);

View file

@ -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(
<Provider store={store}>
<I18nProvider>
<Router history={history}>
<RouteCapture>
<AlertIndex />
</RouteCapture>
</Router>
</I18nProvider>
<KibanaContextProvider services={undefined}>
<I18nProvider>
<Router history={history}>
<RouteCapture>
<AlertIndex />
</RouteCapture>
</Router>
</I18nProvider>
</KibanaContextProvider>
</Provider>
);
};
@ -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 () => {

View file

@ -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 (
<Link
data-testid="alertTypeCellLink"
to={urlFromQueryParams({ ...queryParams, selected_alert: 'TODO' })}
to={urlFromQueryParams({ ...queryParams, selected_alert: row.event.id })}
>
{i18n.translate(
'xpack.endpoint.application.endpoint.alerts.alertType.maliciousFileDescription',
@ -213,7 +214,9 @@ export const AlertIndex = memo(() => {
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody />
<EuiFlyoutBody>
<AlertDetailResolver selectedEvent={selectedEvent} />
</EuiFlyoutBody>
</EuiFlyout>
)}
<EuiPage data-test-subj="alertListPage" data-testid="alertListPage">

View file

@ -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<EndpointPluginServices>();
const { store } = storeFactory(context);
return (
<div className={className} data-test-subj="alertResolver" data-testid="alertResolver">
<Provider store={store}>
<Resolver selectedEvent={selectedEvent} />
</Provider>
</div>
);
}
)
)`
height: 100%;
width: 100%;
display: flex;
flex-grow: 1;
`;

View file

@ -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<number | undefined, ProcessEvent[]>();
const idToValue = new Map<number, ProcessEvent>();
export function factory(processes: LegacyEndpointEvent[]): IndexedProcessTree {
const idToChildren = new Map<number | undefined, LegacyEndpointEvent[]>();
const idToValue = new Map<number, LegacyEndpointEvent>();
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)!;
}

View file

@ -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');
});
});

View file

@ -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;
}

View file

@ -4,33 +4,46 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { ProcessEvent } from '../types';
type DeepPartial<T> = { [K in keyof T]?: DeepPartial<T[K]> };
import { LegacyEndpointEvent } from '../../../../common/types';
type DeepPartial<T> = { [K in keyof T]?: DeepPartial<T[K]> };
/**
* 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>
): 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>;
}): 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,
};
}

View file

@ -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;

View file

@ -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,

View file

@ -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[];
};
};
};

View file

@ -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<DataState, DataAction>;
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);

View file

@ -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<DataState, ResolverAction> = (state = initialS
return {
...state,
results: search_results,
isLoading: false,
};
} else if (action.type === 'appRequestedResolverData') {
return {
...state,
isLoading: true,
};
} else {
return state;

View file

@ -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<ProcessEvent, number>();
const widths = new Map<LegacyEndpointEvent, number>();
if (size(indexedProcessTree) === 0) {
return widths;
@ -309,13 +313,13 @@ function processPositions(
indexedProcessTree: IndexedProcessTree,
widths: ProcessWidths
): ProcessPositions {
const positions = new Map<ProcessEvent, Vector2>();
const positions = new Map<LegacyEndpointEvent, Vector2>();
/**
* 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<ProcessEvent, Vector2>();
const transformedPositions = new Map<LegacyEndpointEvent, Vector2>();
for (const [processEvent, position] of positions) {
transformedPositions.set(processEvent, applyMatrix3(position, isometricTransformMatrix));

View file

@ -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<ResolverState, ResolverAction> } => {
export const storeFactory = (
context?: KibanaReactContextValue<EndpointPluginServices>
): { store: Store<ResolverState, ResolverAction> } => {
const actionsBlacklist: Array<ResolverAction['type']> = ['userMovedPointer'];
const composeEnhancers = composeWithDevTools({
name: 'Resolver',
actionsBlacklist,
});
const middlewareEnhancer = applyMiddleware();
const middlewareEnhancer = applyMiddleware(resolverMiddlewareFactory(context));
const store = createStore(resolverReducer, composeEnhancers(middlewareEnhancer));
return {

View file

@ -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);

View file

@ -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<S = ResolverState> = (
context?: KibanaReactContextValue<EndpointPluginServices>
) => (
api: MiddlewareAPI<Dispatch<ResolverAction>, S>
) => (next: Dispatch<ResolverAction>) => (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 } } },
});
}
}
};
};

View file

@ -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.

View file

@ -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<number | undefined, ProcessEvent[]>;
idToChildren: Map<number | undefined, LegacyEndpointEvent[]>;
/**
* Map of ID to process
*/
idToProcess: Map<number, ProcessEvent>;
idToProcess: Map<number, LegacyEndpointEvent>;
}
/**
* A map of ProcessEvents (representing process nodes) to the 'width' of their subtrees as calculated by `widthsOfProcessSubtrees`
*/
export type ProcessWidths = Map<ProcessEvent, number>;
export type ProcessWidths = Map<LegacyEndpointEvent, number>;
/**
* Map of ProcessEvents (representing process nodes) to their positions. Calculated by `processPositions`
*/
export type ProcessPositions = Map<ProcessEvent, Vector2>;
export type ProcessPositions = Map<LegacyEndpointEvent, Vector2>;
/**
* 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;

View file

@ -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 (
<div data-test-subj="resolverEmbeddable" className={className}>
<div className="resolver-graph" onMouseDown={onMouseDown} ref={ref}>
{Array.from(processNodePositions).map(([processEvent, position], index) => (
<ProcessEventDot
key={index}
position={position}
projectionMatrix={projectionMatrix}
event={processEvent}
/>
))}
{edgeLineSegments.map(([startPosition, endPosition], index) => (
<EdgeLine
key={index}
startPosition={startPosition}
endPosition={endPosition}
projectionMatrix={projectionMatrix}
/>
))}
</div>
<StyledPanel />
<StyledGraphControls />
{isLoading ? (
<div className="loading-container">
<EuiLoadingSpinner size="xl" />
</div>
) : (
<>
<div className="resolver-graph" onMouseDown={onMouseDown} ref={ref}>
{Array.from(processNodePositions).map(([processEvent, position], index) => (
<ProcessEventDot
key={index}
position={position}
projectionMatrix={projectionMatrix}
event={processEvent}
/>
))}
{edgeLineSegments.map(([startPosition, endPosition], index) => (
<EdgeLine
key={index}
startPosition={startPosition}
endPosition={endPosition}
projectionMatrix={projectionMatrix}
/>
))}
</div>
<StyledPanel />
<StyledGraphControls />
</>
)}
</div>
);
})
@ -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.
*/

View file

@ -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,
};
}),

View file

@ -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 (
<span className={className} style={style}>
name: {event.data_buffer.process_name}
name: {event.endgame.process_name}
<br />
x: {position[0]}
<br />

View file

@ -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);
});
});

View file

@ -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<CoreStart> {
http: CoreStart['http'];
overlays: CoreStart['overlays'] | undefined;
notifications: CoreStart['notifications'] | undefined;
}
export class EndpointPlugin
implements
Plugin<

View file

@ -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' }],
},

View file

@ -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' }],
},

View file

@ -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<number | string>;
query.search_after = [timestamp, eventID] as Array<number | string>;
}
return query;
}