Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[7.x] [Endpoint] add resolver middleware (#58288) #59180

Merged
merged 1 commit into from
Mar 4, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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;
Expand All @@ -214,6 +229,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