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

[Endpoint] add resolver middleware #58288

Merged
Merged
Show file tree
Hide file tree
Changes from 10 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 @@ -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
74 changes: 42 additions & 32 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 @@ -23,9 +24,7 @@ import { PolicyList } from './view/policy';
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 @@ -34,34 +33,45 @@ export function renderApp(coreStart: CoreStart, { appBasePath, element }: AppMou
interface RouterProps {
basename: string;
store: Store;
coreStart: CoreStart;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kqualters-elastic - maybe not for this PR, but we should consider using the kibana-react components for sharing coreStart in the app. I think the use of hooks rather than to force coreStart down to each route's View would be better DX. If we did not want to use the kibana-react components, we could just create our own react Context and set of hooks.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed, updated to do just that.

}

const AppRoot: React.FunctionComponent<RouterProps> = React.memo(({ basename, store }) => (
<Provider store={store}>
<I18nProvider>
<BrowserRouter basename={basename}>
<RouteCapture>
<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>
<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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These will never equal each other based on how the rest of the details flyout is implemented. We use row.id for 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 })}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❔ row could resolve to alertListData[NaN] (Div by 0) and throw if the selector ever returned 0 for pageSize. Unlikely? Or maybe use the new chaining syntax?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kqualters-elastic We use row.id in the rest of the alert details flyout. We use row.id to fetch the details of the alert.

>
{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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❔ just flex: 1 in case it also needed a shrink?

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