mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
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:
parent
ce8df3b5cc
commit
77f023f36f
30 changed files with 578 additions and 301 deletions
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
);
|
||||
|
|
|
@ -43,6 +43,7 @@ export const mockAlertResultList: (options?: {
|
|||
},
|
||||
process: {
|
||||
pid: 107,
|
||||
unique_pid: 1,
|
||||
},
|
||||
host: {
|
||||
hostname: 'HD-c15-bc09190a',
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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;
|
||||
`;
|
|
@ -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)!;
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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[];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 } } },
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}),
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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<
|
||||
|
|
|
@ -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' }],
|
||||
},
|
||||
|
|
|
@ -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' }],
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue