Skip to content

Commit 72b44ee

Browse files
[Endpoint] Recursive resolver children (#61914)
1 parent 1a3d643 commit 72b44ee

File tree

35 files changed

+1293
-574
lines changed

35 files changed

+1293
-574
lines changed

x-pack/plugins/endpoint/common/generate_data.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ export class EndpointDocGenerator {
307307
process: {
308308
entity_id: options.entityID ? options.entityID : this.randomString(10),
309309
parent: options.parentEntityID ? { entity_id: options.parentEntityID } : undefined,
310-
name: options.processName ? options.processName : 'powershell.exe',
310+
name: options.processName ? options.processName : randomProcessName(),
311311
},
312312
};
313313
}
@@ -645,3 +645,16 @@ export class EndpointDocGenerator {
645645
return uuid.v4({ random: [...this.randomNGenerator(255, 16)] });
646646
}
647647
}
648+
649+
const fakeProcessNames = [
650+
'lsass.exe',
651+
'notepad.exe',
652+
'mimikatz.exe',
653+
'powershell.exe',
654+
'iexlorer.exe',
655+
'explorer.exe',
656+
];
657+
/** Return a random fake process name */
658+
function randomProcessName(): string {
659+
return fakeProcessNames[Math.floor(Math.random() * fakeProcessNames.length)];
660+
}

x-pack/plugins/endpoint/common/models/event.ts

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,45 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7-
import { EndpointEvent, LegacyEndpointEvent } from '../types';
7+
import { LegacyEndpointEvent, ResolverEvent } from '../types';
88

9-
export function isLegacyEvent(
10-
event: EndpointEvent | LegacyEndpointEvent
11-
): event is LegacyEndpointEvent {
9+
export function isLegacyEvent(event: ResolverEvent): event is LegacyEndpointEvent {
1210
return (event as LegacyEndpointEvent).endgame !== undefined;
1311
}
1412

15-
export function eventTimestamp(
16-
event: EndpointEvent | LegacyEndpointEvent
17-
): string | undefined | number {
13+
export function eventTimestamp(event: ResolverEvent): string | undefined | number {
1814
if (isLegacyEvent(event)) {
1915
return event.endgame.timestamp_utc;
2016
} else {
2117
return event['@timestamp'];
2218
}
2319
}
2420

25-
export function eventName(event: EndpointEvent | LegacyEndpointEvent): string {
21+
export function eventName(event: ResolverEvent): string {
2622
if (isLegacyEvent(event)) {
2723
return event.endgame.process_name ? event.endgame.process_name : '';
2824
} else {
2925
return event.process.name;
3026
}
3127
}
28+
29+
export function eventId(event: ResolverEvent): string {
30+
if (isLegacyEvent(event)) {
31+
return event.endgame.serial_event_id ? String(event.endgame.serial_event_id) : '';
32+
}
33+
return event.event.id;
34+
}
35+
36+
export function entityId(event: ResolverEvent): string {
37+
if (isLegacyEvent(event)) {
38+
return event.endgame.unique_pid ? String(event.endgame.unique_pid) : '';
39+
}
40+
return event.process.entity_id;
41+
}
42+
43+
export function parentEntityId(event: ResolverEvent): string | undefined {
44+
if (isLegacyEvent(event)) {
45+
return event.endgame.unique_ppid ? String(event.endgame.unique_ppid) : undefined;
46+
}
47+
return event.process.parent?.entity_id;
48+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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 { schema } from '@kbn/config-schema';
8+
9+
/**
10+
* Used to validate GET requests for a complete resolver tree.
11+
*/
12+
export const validateTree = {
13+
params: schema.object({ id: schema.string() }),
14+
query: schema.object({
15+
children: schema.number({ defaultValue: 10, min: 0, max: 100 }),
16+
generations: schema.number({ defaultValue: 3, min: 0, max: 3 }),
17+
ancestors: schema.number({ defaultValue: 3, min: 0, max: 5 }),
18+
events: schema.number({ defaultValue: 100, min: 0, max: 1000 }),
19+
afterEvent: schema.maybe(schema.string()),
20+
afterChild: schema.maybe(schema.string()),
21+
legacyEndpointID: schema.maybe(schema.string()),
22+
}),
23+
};
24+
25+
/**
26+
* Used to validate GET requests for non process events for a specific event.
27+
*/
28+
export const validateEvents = {
29+
params: schema.object({ id: schema.string() }),
30+
query: schema.object({
31+
events: schema.number({ defaultValue: 100, min: 1, max: 1000 }),
32+
afterEvent: schema.maybe(schema.string()),
33+
legacyEndpointID: schema.maybe(schema.string()),
34+
}),
35+
};
36+
37+
/**
38+
* Used to validate GET requests for the ancestors of a process event.
39+
*/
40+
export const validateAncestry = {
41+
params: schema.object({ id: schema.string() }),
42+
query: schema.object({
43+
ancestors: schema.number({ defaultValue: 0, min: 0, max: 10 }),
44+
legacyEndpointID: schema.maybe(schema.string()),
45+
}),
46+
};
47+
48+
/**
49+
* Used to validate GET requests for children of a specified process event.
50+
*/
51+
export const validateChildren = {
52+
params: schema.object({ id: schema.string() }),
53+
query: schema.object({
54+
children: schema.number({ defaultValue: 10, min: 10, max: 100 }),
55+
generations: schema.number({ defaultValue: 3, min: 0, max: 3 }),
56+
afterChild: schema.maybe(schema.string()),
57+
legacyEndpointID: schema.maybe(schema.string()),
58+
}),
59+
};

x-pack/plugins/endpoint/common/types.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,31 @@ type ImmutableObject<T> = { readonly [K in keyof T]: Immutable<T[K]> };
3535
*/
3636
export type AlertAPIOrdering = 'asc' | 'desc';
3737

38+
export interface ResolverNodeStats {
39+
totalEvents: number;
40+
totalAlerts: number;
41+
}
42+
43+
export interface ResolverNodePagination {
44+
nextChild?: string | null;
45+
nextEvent?: string | null;
46+
nextAncestor?: string | null;
47+
nextAlert?: string | null;
48+
}
49+
50+
/**
51+
* A node that contains pointers to other nodes, arrrays of resolver events, and any metadata associated with resolver specific data
52+
*/
53+
export interface ResolverNode {
54+
id: string;
55+
children: ResolverNode[];
56+
events: ResolverEvent[];
57+
lifecycle: ResolverEvent[];
58+
ancestors?: ResolverNode[];
59+
pagination: ResolverNodePagination;
60+
stats?: ResolverNodeStats;
61+
}
62+
3863
/**
3964
* Returned by 'api/endpoint/alerts'
4065
*/

x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/resolver.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,6 @@ export const AlertDetailResolver = styled(
3333
width: 100%;
3434
display: flex;
3535
flex-grow: 1;
36-
min-height: 500px;
36+
/* gross demo hack */
37+
min-height: calc(100vh - 505px);
3738
`;

x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,11 @@ import { ResolverEvent } from '../../../../../common/types';
88

99
interface ServerReturnedResolverData {
1010
readonly type: 'serverReturnedResolverData';
11-
readonly payload: {
12-
readonly data: {
13-
readonly result: {
14-
readonly search_results: readonly ResolverEvent[];
15-
};
16-
};
17-
};
11+
readonly payload: ResolverEvent[];
1812
}
1913

20-
export type DataAction = ServerReturnedResolverData;
14+
interface ServerFailedToReturnResolverData {
15+
readonly type: 'serverFailedToReturnResolverData';
16+
}
17+
18+
export type DataAction = ServerReturnedResolverData | ServerFailedToReturnResolverData;

x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts

Lines changed: 15 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { Store, createStore } from 'redux';
88
import { DataAction } from './action';
99
import { dataReducer } from './reducer';
1010
import { DataState } from '../../types';
11-
import { LegacyEndpointEvent } from '../../../../../common/types';
11+
import { LegacyEndpointEvent, ResolverEvent } from '../../../../../common/types';
1212
import { graphableProcesses, processNodePositionsAndEdgeLineSegments } from './selectors';
1313
import { mockProcessEvent } from '../../models/process_event_test_helpers';
1414

@@ -113,13 +113,7 @@ describe('resolver graph layout', () => {
113113
});
114114
describe('when rendering no nodes', () => {
115115
beforeEach(() => {
116-
const payload = {
117-
data: {
118-
result: {
119-
search_results: [],
120-
},
121-
},
122-
};
116+
const payload: ResolverEvent[] = [];
123117
const action: DataAction = { type: 'serverReturnedResolverData', payload };
124118
store.dispatch(action);
125119
});
@@ -133,13 +127,7 @@ describe('resolver graph layout', () => {
133127
});
134128
describe('when rendering one node', () => {
135129
beforeEach(() => {
136-
const payload = {
137-
data: {
138-
result: {
139-
search_results: [processA],
140-
},
141-
},
142-
};
130+
const payload = [processA];
143131
const action: DataAction = { type: 'serverReturnedResolverData', payload };
144132
store.dispatch(action);
145133
});
@@ -153,13 +141,7 @@ describe('resolver graph layout', () => {
153141
});
154142
describe('when rendering two nodes, one being the parent of the other', () => {
155143
beforeEach(() => {
156-
const payload = {
157-
data: {
158-
result: {
159-
search_results: [processA, processB],
160-
},
161-
},
162-
};
144+
const payload = [processA, processB];
163145
const action: DataAction = { type: 'serverReturnedResolverData', payload };
164146
store.dispatch(action);
165147
});
@@ -173,23 +155,17 @@ describe('resolver graph layout', () => {
173155
});
174156
describe('when rendering two forks, and one fork has an extra long tine', () => {
175157
beforeEach(() => {
176-
const payload = {
177-
data: {
178-
result: {
179-
search_results: [
180-
processA,
181-
processB,
182-
processC,
183-
processD,
184-
processE,
185-
processF,
186-
processG,
187-
processH,
188-
processI,
189-
],
190-
},
191-
},
192-
};
158+
const payload = [
159+
processA,
160+
processB,
161+
processC,
162+
processD,
163+
processE,
164+
processF,
165+
processG,
166+
processH,
167+
processI,
168+
];
193169
const action: DataAction = { type: 'serverReturnedResolverData', payload };
194170
store.dispatch(action);
195171
});

x-pack/plugins/endpoint/public/embeddables/resolver/store/data/reducer.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,28 @@ function initialState(): DataState {
1111
return {
1212
results: [],
1313
isLoading: false,
14+
hasError: false,
1415
};
1516
}
1617

1718
export const dataReducer: Reducer<DataState, ResolverAction> = (state = initialState(), action) => {
1819
if (action.type === 'serverReturnedResolverData') {
19-
const {
20-
data: {
21-
result: { search_results },
22-
},
23-
} = action.payload;
2420
return {
2521
...state,
26-
results: search_results,
22+
results: action.payload,
2723
isLoading: false,
24+
hasError: false,
2825
};
2926
} else if (action.type === 'appRequestedResolverData') {
3027
return {
3128
...state,
3229
isLoading: true,
30+
hasError: false,
31+
};
32+
} else if (action.type === 'serverFailedToReturnResolverData') {
33+
return {
34+
...state,
35+
hasError: true,
3336
};
3437
} else {
3538
return state;

x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ export function isLoading(state: DataState) {
3434
return state.isLoading;
3535
}
3636

37+
export function hasError(state: DataState) {
38+
return state.hasError;
39+
}
40+
3741
/**
3842
* An isometric projection is a method for representing three dimensional objects in 2 dimensions.
3943
* More information about isometric projections can be found here https://en.wikipedia.org/wiki/Isometric_projection.
@@ -293,7 +297,7 @@ function* levelOrderWithWidths(
293297
metadata.firstChildWidth = width;
294298
} else {
295299
const firstChildWidth = widths.get(siblings[0]);
296-
const lastChildWidth = widths.get(siblings[0]);
300+
const lastChildWidth = widths.get(siblings[siblings.length - 1]);
297301
if (firstChildWidth === undefined || lastChildWidth === undefined) {
298302
/**
299303
* All widths have been precalcluated, so this will not happen.

0 commit comments

Comments
 (0)