Skip to content

Commit

Permalink
[Endpoint] add resolver middleware (#58288)
Browse files Browse the repository at this point in the history
* Add resolver middleware

* Update types to match events, use sample events in useCamera tests

* add predicate to convert alertdata to legacy endpoint event

* Use mock event generator in tests

* Guard against events not having agent or endpoint fields

Co-authored-by: Robert Austin <robert.austin@elastic.co>
  • Loading branch information
kqualters-elastic and Robert Austin authored Mar 2, 2020
1 parent b5dd99c commit a3be4e2
Show file tree
Hide file tree
Showing 30 changed files with 578 additions and 301 deletions.
34 changes: 25 additions & 9 deletions x-pack/plugins/endpoint/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,17 +115,20 @@ export type AlertEvent = Immutable<{
score: number;
};
};
process?: {
unique_pid: number;
pid: number;
};
host: {
hostname: string;
ip: string;
os: {
name: string;
};
};
process: {
pid: number;
};
thread: {};
endpoint?: {};
endgame?: {};
}>;

/**
Expand Down Expand Up @@ -186,22 +189,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;
Expand All @@ -216,6 +231,7 @@ export interface EndpointEvent {
};
};
agent: {
id: string;
type: string;
};
}
Expand Down
76 changes: 43 additions & 33 deletions x-pack/plugins/endpoint/public/applications/endpoint/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);
};
Expand All @@ -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>
)
);
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const mockAlertResultList: (options?: {
},
process: {
pid: 107,
unique_pid: 1,
},
host: {
hostname: 'HD-c15-bc09190a',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
/**
Expand Down Expand Up @@ -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;
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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`.
*/
Expand All @@ -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>
);
};
Expand Down Expand Up @@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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 => {
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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">
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
`;
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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)!;
}
Expand Down
Loading

0 comments on commit a3be4e2

Please sign in to comment.