Skip to content

Commit fea3bfc

Browse files
author
Robert Austin
authored
[Resolver] simulator and click through tests (#73310)
Write a few jest tests for resolver's react code.
1 parent 78aa24d commit fea3bfc

22 files changed

+1347
-307
lines changed
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { KibanaReactContextValue } from '../../../../../../src/plugins/kibana_react/public';
8+
import { StartServices } from '../../types';
9+
import { DataAccessLayer } from '../types';
10+
import {
11+
ResolverRelatedEvents,
12+
ResolverTree,
13+
ResolverEntityIndex,
14+
} from '../../../common/endpoint/types';
15+
import { DEFAULT_INDEX_KEY as defaultIndexKey } from '../../../common/constants';
16+
17+
/**
18+
* The data access layer for resolver. All communication with the Kibana server is done through this object. This object is provided to Resolver. In tests, a mock data access layer can be used instead.
19+
*/
20+
export function dataAccessLayerFactory(
21+
context: KibanaReactContextValue<StartServices>
22+
): DataAccessLayer {
23+
const dataAccessLayer: DataAccessLayer = {
24+
/**
25+
* Used to get non-process related events for a node.
26+
*/
27+
async relatedEvents(entityID: string): Promise<ResolverRelatedEvents> {
28+
return context.services.http.get(`/api/endpoint/resolver/${entityID}/events`, {
29+
query: { events: 100 },
30+
});
31+
},
32+
/**
33+
* Used to get descendant and ancestor process events for a node.
34+
*/
35+
async resolverTree(entityID: string, signal: AbortSignal): Promise<ResolverTree> {
36+
return context.services.http.get(`/api/endpoint/resolver/${entityID}`, {
37+
signal,
38+
});
39+
},
40+
41+
/**
42+
* Used to get the default index pattern from the SIEM application.
43+
*/
44+
indexPatterns(): string[] {
45+
return context.services.uiSettings.get(defaultIndexKey);
46+
},
47+
48+
/**
49+
* Used to get the entity_id for an _id.
50+
*/
51+
async entities({
52+
_id,
53+
indices,
54+
signal,
55+
}: {
56+
_id: string;
57+
indices: string[];
58+
signal: AbortSignal;
59+
}): Promise<ResolverEntityIndex> {
60+
return context.services.http.get('/api/endpoint/resolver/entity', {
61+
signal,
62+
query: {
63+
_id,
64+
indices,
65+
},
66+
});
67+
},
68+
};
69+
return dataAccessLayer;
70+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import {
8+
ResolverRelatedEvents,
9+
ResolverTree,
10+
ResolverEntityIndex,
11+
} from '../../../../common/endpoint/types';
12+
import { mockEndpointEvent } from '../../store/mocks/endpoint_event';
13+
import { mockTreeWithNoAncestorsAnd2Children } from '../../store/mocks/resolver_tree';
14+
import { DataAccessLayer } from '../../types';
15+
16+
interface Metadata {
17+
/**
18+
* The `_id` of the document being analyzed.
19+
*/
20+
databaseDocumentID: string;
21+
/**
22+
* A record of entityIDs to be used in tests assertions.
23+
*/
24+
entityIDs: {
25+
/**
26+
* The entityID of the node related to the document being analyzed.
27+
*/
28+
origin: 'origin';
29+
/**
30+
* The entityID of the first child of the origin.
31+
*/
32+
firstChild: 'firstChild';
33+
/**
34+
* The entityID of the second child of the origin.
35+
*/
36+
secondChild: 'secondChild';
37+
};
38+
}
39+
40+
/**
41+
* A simple mock dataAccessLayer possible that returns a tree with 0 ancestors and 2 direct children. 1 related event is returned. The parameter to `entities` is ignored.
42+
*/
43+
export function oneAncestorTwoChildren(): { dataAccessLayer: DataAccessLayer; metadata: Metadata } {
44+
const metadata: Metadata = {
45+
databaseDocumentID: '_id',
46+
entityIDs: { origin: 'origin', firstChild: 'firstChild', secondChild: 'secondChild' },
47+
};
48+
return {
49+
metadata,
50+
dataAccessLayer: {
51+
/**
52+
* Fetch related events for an entity ID
53+
*/
54+
relatedEvents(entityID: string): Promise<ResolverRelatedEvents> {
55+
return Promise.resolve({
56+
entityID,
57+
events: [
58+
mockEndpointEvent({
59+
entityID,
60+
name: 'event',
61+
timestamp: 0,
62+
}),
63+
],
64+
nextEvent: null,
65+
});
66+
},
67+
68+
/**
69+
* Fetch a ResolverTree for a entityID
70+
*/
71+
resolverTree(): Promise<ResolverTree> {
72+
return Promise.resolve(
73+
mockTreeWithNoAncestorsAnd2Children({
74+
originID: metadata.entityIDs.origin,
75+
firstChildID: metadata.entityIDs.firstChild,
76+
secondChildID: metadata.entityIDs.secondChild,
77+
})
78+
);
79+
},
80+
81+
/**
82+
* Get an array of index patterns that contain events.
83+
*/
84+
indexPatterns(): string[] {
85+
return ['index pattern'];
86+
},
87+
88+
/**
89+
* Get entities matching a document.
90+
*/
91+
entities(): Promise<ResolverEntityIndex> {
92+
return Promise.resolve([{ entity_id: metadata.entityIDs.origin }]);
93+
},
94+
},
95+
};
96+
}

x-pack/plugins/security_solution/public/resolver/models/process_event.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export function isTerminatedProcess(passedEvent: ResolverEvent) {
2929
}
3030

3131
/**
32-
* ms since unix epoc, based on timestamp.
32+
* ms since Unix epoc, based on timestamp.
3333
* may return NaN if the timestamp wasn't present or was invalid.
3434
*/
3535
export function datetime(passedEvent: ResolverEvent): number | null {
@@ -85,7 +85,7 @@ export function eventType(passedEvent: ResolverEvent): ResolverProcessType {
8585
}
8686

8787
/**
88-
* Returns the process event's pid
88+
* Returns the process event's PID
8989
*/
9090
export function uniquePidForProcess(passedEvent: ResolverEvent): string {
9191
if (event.isLegacyEvent(passedEvent)) {
@@ -96,7 +96,7 @@ export function uniquePidForProcess(passedEvent: ResolverEvent): string {
9696
}
9797

9898
/**
99-
* Returns the pid for the process on the host
99+
* Returns the PID for the process on the host
100100
*/
101101
export function processPid(passedEvent: ResolverEvent): number | undefined {
102102
if (event.isLegacyEvent(passedEvent)) {
@@ -107,7 +107,7 @@ export function processPid(passedEvent: ResolverEvent): number | undefined {
107107
}
108108

109109
/**
110-
* Returns the process event's parent pid
110+
* Returns the process event's parent PID
111111
*/
112112
export function uniqueParentPidForProcess(passedEvent: ResolverEvent): string | undefined {
113113
if (event.isLegacyEvent(passedEvent)) {
@@ -118,7 +118,7 @@ export function uniqueParentPidForProcess(passedEvent: ResolverEvent): string |
118118
}
119119

120120
/**
121-
* Returns the process event's parent pid
121+
* Returns the process event's parent PID
122122
*/
123123
export function processParentPid(passedEvent: ResolverEvent): number | undefined {
124124
if (event.isLegacyEvent(passedEvent)) {
@@ -144,12 +144,12 @@ export function processPath(passedEvent: ResolverEvent): string | undefined {
144144
*/
145145
export function userInfoForProcess(
146146
passedEvent: ResolverEvent
147-
): { user?: string; domain?: string } | undefined {
147+
): { name?: string; domain?: string } | undefined {
148148
return passedEvent.user;
149149
}
150150

151151
/**
152-
* Returns the MD5 hash for the `passedEvent` param, or undefined if it can't be located
152+
* Returns the MD5 hash for the `passedEvent` parameter, or undefined if it can't be located
153153
* @param {ResolverEvent} passedEvent The `ResolverEvent` to get the MD5 value for
154154
* @returns {string | undefined} The MD5 string for the event
155155
*/
@@ -164,7 +164,7 @@ export function md5HashForProcess(passedEvent: ResolverEvent): string | undefine
164164
/**
165165
* Returns the command line path and arguments used to run the `passedEvent` if any
166166
*
167-
* @param {ResolverEvent} passedEvent The `ResolverEvent` to get the arguemnts value for
167+
* @param {ResolverEvent} passedEvent The `ResolverEvent` to get the arguments value for
168168
* @returns {string | undefined} The arguments (including the path) used to run the process
169169
*/
170170
export function argsForProcess(passedEvent: ResolverEvent): string | undefined {

x-pack/plugins/security_solution/public/resolver/store/index.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,20 @@
66

77
import { createStore, applyMiddleware, Store } from 'redux';
88
import { composeWithDevTools } from 'redux-devtools-extension/developmentOnly';
9-
import { KibanaReactContextValue } from '../../../../../../src/plugins/kibana_react/public';
10-
import { ResolverState } from '../types';
11-
import { StartServices } from '../../types';
9+
import { ResolverState, DataAccessLayer } from '../types';
1210
import { resolverReducer } from './reducer';
1311
import { resolverMiddlewareFactory } from './middleware';
1412
import { ResolverAction } from './actions';
1513

1614
export const storeFactory = (
17-
context?: KibanaReactContextValue<StartServices>
15+
dataAccessLayer: DataAccessLayer
1816
): Store<ResolverState, ResolverAction> => {
1917
const actionsDenylist: Array<ResolverAction['type']> = ['userMovedPointer'];
2018
const composeEnhancers = composeWithDevTools({
2119
name: 'Resolver',
2220
actionsBlacklist: actionsDenylist,
2321
});
24-
const middlewareEnhancer = applyMiddleware(resolverMiddlewareFactory(context));
22+
const middlewareEnhancer = applyMiddleware(resolverMiddlewareFactory(dataAccessLayer));
2523

2624
return createStore(resolverReducer, composeEnhancers(middlewareEnhancer));
2725
};

x-pack/plugins/security_solution/public/resolver/store/middleware/index.ts

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,34 +5,26 @@
55
*/
66

77
import { Dispatch, MiddlewareAPI } from 'redux';
8-
import { KibanaReactContextValue } from '../../../../../../../src/plugins/kibana_react/public';
9-
import { StartServices } from '../../../types';
10-
import { ResolverState } from '../../types';
8+
import { ResolverState, DataAccessLayer } from '../../types';
119
import { ResolverRelatedEvents } from '../../../../common/endpoint/types';
1210
import { ResolverTreeFetcher } from './resolver_tree_fetcher';
1311
import { ResolverAction } from '../actions';
1412

1513
type MiddlewareFactory<S = ResolverState> = (
16-
context?: KibanaReactContextValue<StartServices>
14+
dataAccessLayer: DataAccessLayer
1715
) => (
1816
api: MiddlewareAPI<Dispatch<ResolverAction>, S>
1917
) => (next: Dispatch<ResolverAction>) => (action: ResolverAction) => unknown;
2018

2119
/**
22-
* The redux middleware that the app uses to trigger side effects.
20+
* The `redux` middleware that the application uses to trigger side effects.
2321
* All data fetching should be done here.
24-
* For actions that the app triggers directly, use `app` as a prefix for the type.
22+
* For actions that the application triggers directly, use `app` as a prefix for the type.
2523
* For actions that are triggered as a result of server interaction, use `server` as a prefix for the type.
2624
*/
27-
export const resolverMiddlewareFactory: MiddlewareFactory = (context) => {
25+
export const resolverMiddlewareFactory: MiddlewareFactory = (dataAccessLayer: DataAccessLayer) => {
2826
return (api) => (next) => {
29-
// This cannot work w/o `context`.
30-
if (!context) {
31-
return async (action: ResolverAction) => {
32-
next(action);
33-
};
34-
}
35-
const resolverTreeFetcher = ResolverTreeFetcher(context, api);
27+
const resolverTreeFetcher = ResolverTreeFetcher(dataAccessLayer, api);
3628
return async (action: ResolverAction) => {
3729
next(action);
3830

@@ -45,12 +37,7 @@ export const resolverMiddlewareFactory: MiddlewareFactory = (context) => {
4537
const entityIdToFetchFor = action.payload;
4638
let result: ResolverRelatedEvents | undefined;
4739
try {
48-
result = await context.services.http.get(
49-
`/api/endpoint/resolver/${entityIdToFetchFor}/events`,
50-
{
51-
query: { events: 100 },
52-
}
53-
);
40+
result = await dataAccessLayer.relatedEvents(entityIdToFetchFor);
5441
} catch {
5542
api.dispatch({
5643
type: 'serverFailedToReturnRelatedEventData',

x-pack/plugins/security_solution/public/resolver/store/middleware/resolver_tree_fetcher.ts

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,8 @@
99
import { Dispatch, MiddlewareAPI } from 'redux';
1010
import { ResolverTree, ResolverEntityIndex } from '../../../../common/endpoint/types';
1111

12-
import { KibanaReactContextValue } from '../../../../../../../src/plugins/kibana_react/public';
13-
import { ResolverState } from '../../types';
12+
import { ResolverState, DataAccessLayer } from '../../types';
1413
import * as selectors from '../selectors';
15-
import { StartServices } from '../../../types';
16-
import { DEFAULT_INDEX_KEY as defaultIndexKey } from '../../../../common/constants';
1714
import { ResolverAction } from '../actions';
1815
/**
1916
* A function that handles syncing ResolverTree data w/ the current entity ID.
@@ -23,7 +20,7 @@ import { ResolverAction } from '../actions';
2320
* This is a factory because it is stateful and keeps that state in closure.
2421
*/
2522
export function ResolverTreeFetcher(
26-
context: KibanaReactContextValue<StartServices>,
23+
dataAccessLayer: DataAccessLayer,
2724
api: MiddlewareAPI<Dispatch<ResolverAction>, ResolverState>
2825
): () => void {
2926
let lastRequestAbortController: AbortController | undefined;
@@ -48,17 +45,12 @@ export function ResolverTreeFetcher(
4845
payload: databaseDocumentIDToFetch,
4946
});
5047
try {
51-
const indices: string[] = context.services.uiSettings.get(defaultIndexKey);
52-
const matchingEntities: ResolverEntityIndex = await context.services.http.get(
53-
'/api/endpoint/resolver/entity',
54-
{
55-
signal: lastRequestAbortController.signal,
56-
query: {
57-
_id: databaseDocumentIDToFetch,
58-
indices,
59-
},
60-
}
61-
);
48+
const indices: string[] = dataAccessLayer.indexPatterns();
49+
const matchingEntities: ResolverEntityIndex = await dataAccessLayer.entities({
50+
_id: databaseDocumentIDToFetch,
51+
indices,
52+
signal: lastRequestAbortController.signal,
53+
});
6254
if (matchingEntities.length < 1) {
6355
// If no entity_id could be found for the _id, bail out with a failure.
6456
api.dispatch({
@@ -68,9 +60,10 @@ export function ResolverTreeFetcher(
6860
return;
6961
}
7062
const entityIDToFetch = matchingEntities[0].entity_id;
71-
result = await context.services.http.get(`/api/endpoint/resolver/${entityIDToFetch}`, {
72-
signal: lastRequestAbortController.signal,
73-
});
63+
result = await dataAccessLayer.resolverTree(
64+
entityIDToFetch,
65+
lastRequestAbortController.signal
66+
);
7467
} catch (error) {
7568
// https://developer.mozilla.org/en-US/docs/Web/API/DOMException#exception-AbortError
7669
if (error instanceof DOMException && error.name === 'AbortError') {

x-pack/plugins/security_solution/public/resolver/store/mocks/endpoint_event.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export function mockEndpointEvent({
1818
}: {
1919
entityID: string;
2020
name: string;
21-
parentEntityId: string | undefined;
21+
parentEntityId?: string;
2222
timestamp: number;
2323
lifecycleType?: string;
2424
}): EndpointEvent {

0 commit comments

Comments
 (0)