From e7b02d06cc023528ab868891fca8e523bb390917 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Thu, 10 Sep 2020 14:26:35 -0400 Subject: [PATCH] [Security Solution] Use safe type in resolver backend (#76969) * Moving generator to safe type version * Finished generator and alert * Gzipping again * Finishing type conversions for backend * Trying to cast front end tests back to unsafe type for now * Working reducer tests * Adding more comments and fixing alert type * Restoring resolver test data * Updating snapshot with timestamp info * Removing todo and fixing test Co-authored-by: Elastic Machine --- .../common/endpoint/generate_data.test.ts | 142 ++++---- .../common/endpoint/generate_data.ts | 119 ++++--- .../common/endpoint/index_data.ts | 3 +- .../endpoint/models/ecs_safety_helpers.ts | 4 +- .../common/endpoint/models/event.test.ts | 41 ++- .../common/endpoint/models/event.ts | 55 +-- .../common/endpoint/types/index.ts | 322 +++++++++++------- .../isometric_taxi_layout.test.ts.snap | 36 ++ .../resolver/store/data/reducer.test.ts | 34 +- .../routes/resolver/queries/alerts.ts | 6 +- .../endpoint/routes/resolver/queries/base.ts | 8 +- .../routes/resolver/queries/children.ts | 6 +- .../routes/resolver/queries/events.ts | 6 +- .../routes/resolver/queries/lifecycle.ts | 6 +- .../routes/resolver/queries/multi_searcher.ts | 6 +- .../endpoint/routes/resolver/queries/stats.ts | 4 +- .../resolver/utils/alerts_query_handler.ts | 4 +- .../resolver/utils/ancestry_query_handler.ts | 49 +-- .../resolver/utils/children_helper.test.ts | 10 +- .../routes/resolver/utils/children_helper.ts | 37 +- .../utils/children_lifecycle_query_handler.ts | 10 +- .../resolver/utils/children_pagination.ts | 8 +- .../utils/children_start_query_handler.ts | 4 +- .../resolver/utils/events_query_handler.ts | 8 +- .../endpoint/routes/resolver/utils/fetch.ts | 16 +- .../resolver/utils/lifecycle_query_handler.ts | 10 +- .../endpoint/routes/resolver/utils/node.ts | 34 +- .../routes/resolver/utils/pagination.test.ts | 10 +- .../routes/resolver/utils/pagination.ts | 19 +- .../routes/resolver/utils/tree.test.ts | 24 +- .../endpoint/routes/resolver/utils/tree.ts | 28 +- .../apis/resolver/alerts.ts | 19 +- .../apis/resolver/children.ts | 98 +++--- .../apis/resolver/common.ts | 55 ++- .../apis/resolver/entity_id.ts | 46 +-- .../apis/resolver/events.ts | 28 +- .../apis/resolver/tree.ts | 58 ++-- .../services/resolver.ts | 3 +- 38 files changed, 794 insertions(+), 582 deletions(-) diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts index be3a1e82356c8..7e3b3d125fb5d 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts @@ -13,6 +13,12 @@ import { ECSCategory, ANCESTRY_LIMIT, } from './generate_data'; +import { firstNonNullValue, values } from './models/ecs_safety_helpers'; +import { + entityIDSafeVersion, + parentEntityIDSafeVersion, + timestampSafeVersion, +} from './models/event'; interface Node { events: Event[]; @@ -30,7 +36,7 @@ describe('data generator', () => { const event1 = generator.generateEvent(); const event2 = generator.generateEvent(); - expect(event2.event.sequence).toBe(event1.event.sequence + 1); + expect(event2.event?.sequence).toBe((firstNonNullValue(event1.event?.sequence) ?? 0) + 1); }); it('creates the same documents with same random seed', () => { @@ -76,37 +82,37 @@ describe('data generator', () => { const timestamp = new Date().getTime(); const alert = generator.generateAlert(timestamp); expect(alert['@timestamp']).toEqual(timestamp); - expect(alert.event.action).not.toBeNull(); + expect(alert.event?.action).not.toBeNull(); expect(alert.Endpoint).not.toBeNull(); expect(alert.agent).not.toBeNull(); expect(alert.host).not.toBeNull(); - expect(alert.process.entity_id).not.toBeNull(); + expect(alert.process?.entity_id).not.toBeNull(); }); it('creates process event documents', () => { const timestamp = new Date().getTime(); const processEvent = generator.generateEvent({ timestamp }); expect(processEvent['@timestamp']).toEqual(timestamp); - expect(processEvent.event.category).toEqual(['process']); - expect(processEvent.event.kind).toEqual('event'); - expect(processEvent.event.type).toEqual(['start']); + expect(processEvent.event?.category).toEqual(['process']); + expect(processEvent.event?.kind).toEqual('event'); + expect(processEvent.event?.type).toEqual(['start']); expect(processEvent.agent).not.toBeNull(); expect(processEvent.host).not.toBeNull(); - expect(processEvent.process.entity_id).not.toBeNull(); - expect(processEvent.process.name).not.toBeNull(); + expect(processEvent.process?.entity_id).not.toBeNull(); + expect(processEvent.process?.name).not.toBeNull(); }); it('creates other event documents', () => { const timestamp = new Date().getTime(); const processEvent = generator.generateEvent({ timestamp, eventCategory: 'dns' }); expect(processEvent['@timestamp']).toEqual(timestamp); - expect(processEvent.event.category).toEqual('dns'); - expect(processEvent.event.kind).toEqual('event'); - expect(processEvent.event.type).toEqual(['start']); + expect(processEvent.event?.category).toEqual('dns'); + expect(processEvent.event?.kind).toEqual('event'); + expect(processEvent.event?.type).toEqual(['start']); expect(processEvent.agent).not.toBeNull(); expect(processEvent.host).not.toBeNull(); - expect(processEvent.process.entity_id).not.toBeNull(); - expect(processEvent.process.name).not.toBeNull(); + expect(processEvent.process?.entity_id).not.toBeNull(); + expect(processEvent.process?.name).not.toBeNull(); }); describe('creates events with an empty ancestry array', () => { @@ -128,7 +134,7 @@ describe('data generator', () => { it('creates all events with an empty ancestry array', () => { for (const event of tree.allEvents) { - expect(event.process.Ext!.ancestry!.length).toEqual(0); + expect(event.process?.Ext?.ancestry?.length).toEqual(0); } }); }); @@ -194,22 +200,23 @@ describe('data generator', () => { const inRelated = node.relatedEvents.includes(event); const inRelatedAlerts = node.relatedAlerts.includes(event); - return (inRelated || inRelatedAlerts || inLifecycle) && event.process.entity_id === node.id; + return (inRelated || inRelatedAlerts || inLifecycle) && event.process?.entity_id === node.id; }; const verifyAncestry = (event: Event, genTree: Tree) => { - if (event.process.Ext!.ancestry!.length > 0) { - expect(event.process.parent?.entity_id).toBe(event.process.Ext!.ancestry![0]); + const ancestry = values(event.process?.Ext?.ancestry); + if (ancestry.length > 0) { + expect(event.process?.parent?.entity_id).toBe(ancestry[0]); } - for (let i = 0; i < event.process.Ext!.ancestry!.length; i++) { - const ancestor = event.process.Ext!.ancestry![i]; + for (let i = 0; i < ancestry.length; i++) { + const ancestor = ancestry[i]; const parent = genTree.children.get(ancestor) || genTree.ancestry.get(ancestor); - expect(ancestor).toBe(parent?.lifecycle[0].process.entity_id); + expect(ancestor).toBe(parent?.lifecycle[0].process?.entity_id); // the next ancestor should be the grandparent - if (i + 1 < event.process.Ext!.ancestry!.length) { - const grandparent = event.process.Ext!.ancestry![i + 1]; - expect(grandparent).toBe(parent?.lifecycle[0].process.parent?.entity_id); + if (i + 1 < ancestry.length) { + const grandparent = ancestry[i + 1]; + expect(grandparent).toBe(parent?.lifecycle[0].process?.parent?.entity_id); } } }; @@ -217,13 +224,14 @@ describe('data generator', () => { it('creates related events in ascending order', () => { // the order should not change since it should already be in ascending order const relatedEventsAsc = _.cloneDeep(tree.origin.relatedEvents).sort( - (event1, event2) => event1['@timestamp'] - event2['@timestamp'] + (event1, event2) => + (timestampSafeVersion(event1) ?? 0) - (timestampSafeVersion(event2) ?? 0) ); expect(tree.origin.relatedEvents).toStrictEqual(relatedEventsAsc); }); it('has ancestry array defined', () => { - expect(tree.origin.lifecycle[0].process.Ext!.ancestry!.length).toBe(ANCESTRY_LIMIT); + expect(values(tree.origin.lifecycle[0].process?.Ext?.ancestry).length).toBe(ANCESTRY_LIMIT); for (const event of tree.allEvents) { verifyAncestry(event, tree); } @@ -252,12 +260,9 @@ describe('data generator', () => { const counts: Record = {}; for (const event of node.relatedEvents) { - if (Array.isArray(event.event.category)) { - for (const cat of event.event.category) { - counts[cat] = counts[cat] + 1 || 1; - } - } else { - counts[event.event.category] = counts[event.event.category] + 1 || 1; + const categories = values(event.event?.category); + for (const cat of categories) { + counts[cat] = counts[cat] + 1 || 1; } } expect(counts[ECSCategory.Driver]).toEqual(1); @@ -316,15 +321,18 @@ describe('data generator', () => { expect(tree.allEvents.length).toBeGreaterThan(0); tree.allEvents.forEach((event) => { - const ancestor = tree.ancestry.get(event.process.entity_id); - if (ancestor) { - expect(eventInNode(event, ancestor)).toBeTruthy(); - return; - } + const entityID = entityIDSafeVersion(event); + if (entityID) { + const ancestor = tree.ancestry.get(entityID); + if (ancestor) { + expect(eventInNode(event, ancestor)).toBeTruthy(); + return; + } - const children = tree.children.get(event.process.entity_id); - if (children) { - expect(eventInNode(event, children)).toBeTruthy(); + const children = tree.children.get(entityID); + if (children) { + expect(eventInNode(event, children)).toBeTruthy(); + } } }); }); @@ -351,9 +359,8 @@ describe('data generator', () => { let events: Event[]; const isCategoryProcess = (event: Event) => { - return ( - _.isEqual(event.event.category, ['process']) || _.isEqual(event.event.category, 'process') - ); + const category = values(event.event?.category); + return _.isEqual(category, ['process']); }; beforeEach(() => { @@ -366,12 +373,16 @@ describe('data generator', () => { it('with n-1 process events', () => { for (let i = events.length - 2; i > 0; ) { - const parentEntityIdOfChild = events[i].process.parent?.entity_id; - for (; --i >= -1 && (events[i].event.kind !== 'event' || !isCategoryProcess(events[i])); ) { + const parentEntityIdOfChild = parentEntityIDSafeVersion(events[i]); + for ( + ; + --i >= -1 && (events[i].event?.kind !== 'event' || !isCategoryProcess(events[i])); + + ) { // related event - skip it } expect(i).toBeGreaterThanOrEqual(0); - expect(parentEntityIdOfChild).toEqual(events[i].process.entity_id); + expect(parentEntityIdOfChild).toEqual(entityIDSafeVersion(events[i])); } }); @@ -380,7 +391,7 @@ describe('data generator', () => { for ( ; previousProcessEventIndex >= -1 && - (events[previousProcessEventIndex].event.kind !== 'event' || + (events[previousProcessEventIndex].event?.kind !== 'event' || !isCategoryProcess(events[previousProcessEventIndex])); previousProcessEventIndex-- ) { @@ -388,14 +399,14 @@ describe('data generator', () => { } expect(previousProcessEventIndex).toBeGreaterThanOrEqual(0); // The alert should be last and have the same entity_id as the previous process event - expect(events[events.length - 1].process.entity_id).toEqual( - events[previousProcessEventIndex].process.entity_id + expect(events[events.length - 1].process?.entity_id).toEqual( + events[previousProcessEventIndex].process?.entity_id ); - expect(events[events.length - 1].process.parent?.entity_id).toEqual( - events[previousProcessEventIndex].process.parent?.entity_id + expect(events[events.length - 1].process?.parent?.entity_id).toEqual( + events[previousProcessEventIndex].process?.parent?.entity_id ); - expect(events[events.length - 1].event.kind).toEqual('alert'); - expect(events[events.length - 1].event.category).toEqual('malware'); + expect(events[events.length - 1].event?.kind).toEqual('alert'); + expect(events[events.length - 1].event?.category).toEqual('malware'); }); }); @@ -403,14 +414,17 @@ describe('data generator', () => { // First pass we gather up all the events by entity_id const tree: Record = {}; events.forEach((event) => { - if (event.process.entity_id in tree) { - tree[event.process.entity_id].events.push(event); - } else { - tree[event.process.entity_id] = { - events: [event], - children: [], - parent_entity_id: event.process.parent?.entity_id, - }; + const entityID = entityIDSafeVersion(event); + if (entityID) { + if (entityID in tree) { + tree[entityID].events.push(event); + } else { + tree[entityID] = { + events: [event], + children: [], + parent_entity_id: parentEntityIDSafeVersion(event), + }; + } } }); // Second pass add child references to each node @@ -419,8 +433,14 @@ describe('data generator', () => { tree[value.parent_entity_id].children.push(value); } } + + const entityID = entityIDSafeVersion(events[0]); + if (!entityID) { + throw new Error('entity id was invalid'); + } + // The root node must be first in the array or this fails - return tree[events[0].process.entity_id]; + return tree[entityID]; } function countResolverEvents(rootNode: Node, generations: number): number { diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index e1ff34463d215..7f31c71fe712b 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -7,7 +7,6 @@ import uuid from 'uuid'; import seedrandom from 'seedrandom'; import { AlertEvent, - EndpointEvent, EndpointStatus, Host, HostMetadata, @@ -15,9 +14,15 @@ import { HostPolicyResponseActionStatus, OSFields, PolicyData, + SafeEndpointEvent, } from './types'; import { factory as policyFactory } from './models/policy_config'; -import { parentEntityId } from './models/event'; +import { + ancestryArray, + entityIDSafeVersion, + parentEntityIDSafeVersion, + timestampSafeVersion, +} from './models/event'; import { GetAgentPoliciesResponseItem, GetPackagesResponse, @@ -28,8 +33,9 @@ import { InstallationStatus, KibanaAssetReference, } from '../../../ingest_manager/common/types/models'; +import { firstNonNullValue } from './models/ecs_safety_helpers'; -export type Event = AlertEvent | EndpointEvent; +export type Event = AlertEvent | SafeEndpointEvent; /** * This value indicates the limit for the size of the ancestry array. The endpoint currently saves up to 20 values * in its messages. To simulate a limit on the array size I'm using 2 here so that we can't rely on there being a large @@ -426,13 +432,13 @@ export class EndpointDocGenerator { * @param ts - Timestamp to put in the event * @param entityID - entityID of the originating process * @param parentEntityID - optional entityID of the parent process, if it exists - * @param ancestryArray - an array of ancestors for the generated alert + * @param ancestry - an array of ancestors for the generated alert */ public generateAlert( ts = new Date().getTime(), entityID = this.randomString(10), parentEntityID?: string, - ancestryArray: string[] = [] + ancestry: string[] = [] ): AlertEvent { return { ...this.commonInfo, @@ -493,7 +499,7 @@ export class EndpointDocGenerator { sha256: 'fake sha256', }, Ext: { - ancestry: ancestryArray, + ancestry, code_signature: [ { trusted: false, @@ -555,7 +561,7 @@ export class EndpointDocGenerator { * Creates an event, customized by the options parameter * @param options - Allows event field values to be specified */ - public generateEvent(options: EventOptions = {}): EndpointEvent { + public generateEvent(options: EventOptions = {}): Event { // this will default to an empty array for the ancestry field if options.ancestry isn't included const ancestry: string[] = options.ancestry?.slice(0, options?.ancestryArrayLimit ?? ANCESTRY_LIMIT) ?? []; @@ -643,7 +649,11 @@ export class EndpointDocGenerator { public generateTree(options: TreeOptions = {}): Tree { const optionsWithDef = getTreeOptionsWithDef(options); const addEventToMap = (nodeMap: Map, event: Event) => { - const nodeId = event.process.entity_id; + const nodeId = entityIDSafeVersion(event); + if (!nodeId) { + return nodeMap; + } + // if a node already exists for the entity_id we'll use that one, otherwise let's create a new empty node // and add the event to the right array. let node = nodeMap.get(nodeId); @@ -652,18 +662,13 @@ export class EndpointDocGenerator { } // place the event in the right array depending on its category - if (event.event.kind === 'event') { - if ( - (Array.isArray(event.event.category) && - event.event.category.length === 1 && - event.event.category[0] === 'process') || - event.event.category === 'process' - ) { + if (firstNonNullValue(event.event?.kind) === 'event') { + if (firstNonNullValue(event.event?.category) === 'process') { node.lifecycle.push(event); } else { node.relatedEvents.push(event); } - } else if (event.event.kind === 'alert') { + } else if (firstNonNullValue(event.event?.kind) === 'alert') { node.relatedAlerts.push(event); } @@ -673,7 +678,7 @@ export class EndpointDocGenerator { const groupNodesByParent = (children: Map) => { const nodesByParent: Map> = new Map(); for (const node of children.values()) { - const parentID = parentEntityId(node.lifecycle[0]); + const parentID = parentEntityIDSafeVersion(node.lifecycle[0]); if (parentID) { let groupedNodes = nodesByParent.get(parentID); @@ -715,9 +720,13 @@ export class EndpointDocGenerator { const ancestryNodes: Map = ancestry.reduce(addEventToMap, new Map()); const alert = ancestry[ancestry.length - 1]; - const origin = ancestryNodes.get(alert.process.entity_id); + const alertEntityID = entityIDSafeVersion(alert); + if (!alertEntityID) { + throw Error("could not find the originating alert's entity id"); + } + const origin = ancestryNodes.get(alertEntityID); if (!origin) { - throw Error(`could not find origin while building tree: ${alert.process.entity_id}`); + throw Error(`could not find origin while building tree: ${alertEntityID}`); } const children = Array.from(this.descendantsTreeGenerator(alert, optionsWithDef)); @@ -799,7 +808,7 @@ export class EndpointDocGenerator { }); events.push(root); let ancestor = root; - let timestamp = root['@timestamp'] + 1000; + let timestamp = (timestampSafeVersion(root) ?? 0) + 1000; const addRelatedAlerts = ( node: Event, @@ -836,8 +845,8 @@ export class EndpointDocGenerator { events.push( this.generateEvent({ timestamp: timestamp + termProcessDuration * 1000, - entityID: root.process.entity_id, - parentEntityID: root.process.parent?.entity_id, + entityID: entityIDSafeVersion(root), + parentEntityID: parentEntityIDSafeVersion(root), eventCategory: ['process'], eventType: ['end'], }) @@ -845,13 +854,20 @@ export class EndpointDocGenerator { } for (let i = 0; i < opts.ancestors; i++) { + const ancestorEntityID = entityIDSafeVersion(ancestor); + const ancestry: string[] = []; + if (ancestorEntityID) { + ancestry.push(ancestorEntityID); + } + + ancestry.push(...(ancestryArray(ancestor) ?? [])); ancestor = this.generateEvent({ timestamp, - parentEntityID: ancestor.process.entity_id, + parentEntityID: entityIDSafeVersion(ancestor), // add the parent to the ancestry array - ancestry: [ancestor.process.entity_id, ...(ancestor.process.Ext?.ancestry ?? [])], + ancestry, ancestryArrayLimit: opts.ancestryArraySize, - parentPid: ancestor.process.pid, + parentPid: firstNonNullValue(ancestor.process?.pid), pid: this.randomN(5000), }); events.push(ancestor); @@ -862,11 +878,11 @@ export class EndpointDocGenerator { events.push( this.generateEvent({ timestamp: timestamp + termProcessDuration * 1000, - entityID: ancestor.process.entity_id, - parentEntityID: ancestor.process.parent?.entity_id, + entityID: entityIDSafeVersion(ancestor), + parentEntityID: parentEntityIDSafeVersion(ancestor), eventCategory: ['process'], eventType: ['end'], - ancestry: ancestor.process.Ext?.ancestry, + ancestry: ancestryArray(ancestor), ancestryArrayLimit: opts.ancestryArraySize, }) ); @@ -890,9 +906,9 @@ export class EndpointDocGenerator { events.push( this.generateAlert( timestamp, - ancestor.process.entity_id, - ancestor.process.parent?.entity_id, - ancestor.process.Ext?.ancestry + entityIDSafeVersion(ancestor), + parentEntityIDSafeVersion(ancestor), + ancestryArray(ancestor) ) ); return events; @@ -922,7 +938,7 @@ export class EndpointDocGenerator { maxChildren, }; const lineage: NodeState[] = [rootState]; - let timestamp = root['@timestamp']; + let timestamp = timestampSafeVersion(root) ?? 0; while (lineage.length > 0) { const currentState = lineage[lineage.length - 1]; // If we get to a state node and it has made all the children, move back up a level @@ -937,13 +953,17 @@ export class EndpointDocGenerator { // Otherwise, add a child and any nodes associated with it currentState.childrenCreated++; timestamp = timestamp + 1000; + const currentStateEntityID = entityIDSafeVersion(currentState.event); + const ancestry: string[] = []; + if (currentStateEntityID) { + ancestry.push(currentStateEntityID); + } + ancestry.push(...(ancestryArray(currentState.event) ?? [])); + const child = this.generateEvent({ timestamp, - parentEntityID: currentState.event.process.entity_id, - ancestry: [ - currentState.event.process.entity_id, - ...(currentState.event.process.Ext?.ancestry ?? []), - ], + parentEntityID: currentStateEntityID, + ancestry, ancestryArrayLimit: opts.ancestryArraySize, }); @@ -962,11 +982,11 @@ export class EndpointDocGenerator { processDuration = this.randomN(1000000); // This lets termination events be up to 1 million seconds after the creation event (~11 days) yield this.generateEvent({ timestamp: timestamp + processDuration * 1000, - entityID: child.process.entity_id, - parentEntityID: child.process.parent?.entity_id, + entityID: entityIDSafeVersion(child), + parentEntityID: parentEntityIDSafeVersion(child), eventCategory: ['process'], eventType: ['end'], - ancestry: child.process.Ext?.ancestry, + ancestry, ancestryArrayLimit: opts.ancestryArraySize, }); } @@ -998,7 +1018,8 @@ export class EndpointDocGenerator { ordered: boolean = false ) { let relatedEventsInfo: RelatedEventInfo[]; - let ts = node['@timestamp'] + 1; + const nodeTimestamp = timestampSafeVersion(node) ?? 0; + let ts = nodeTimestamp + 1; if (typeof relatedEvents === 'number') { relatedEventsInfo = [{ category: RelatedEventCategory.Random, count: relatedEvents }]; } else { @@ -1017,16 +1038,16 @@ export class EndpointDocGenerator { if (ordered) { ts += this.randomN(processDuration) * 1000; } else { - ts = node['@timestamp'] + this.randomN(processDuration) * 1000; + ts = nodeTimestamp + this.randomN(processDuration) * 1000; } yield this.generateEvent({ timestamp: ts, - entityID: node.process.entity_id, - parentEntityID: node.process.parent?.entity_id, + entityID: entityIDSafeVersion(node), + parentEntityID: parentEntityIDSafeVersion(node), eventCategory: eventInfo.category, eventType: eventInfo.creationType, - ancestry: node.process.Ext?.ancestry, + ancestry: ancestryArray(node), }); } } @@ -1044,12 +1065,12 @@ export class EndpointDocGenerator { alertCreationTime: number = 6 * 3600 ) { for (let i = 0; i < relatedAlerts; i++) { - const ts = node['@timestamp'] + this.randomN(alertCreationTime) * 1000; + const ts = (timestampSafeVersion(node) ?? 0) + this.randomN(alertCreationTime) * 1000; yield this.generateAlert( ts, - node.process.entity_id, - node.process.parent?.entity_id, - node.process.Ext?.ancestry + entityIDSafeVersion(node), + parentEntityIDSafeVersion(node), + ancestryArray(node) ); } } diff --git a/x-pack/plugins/security_solution/common/endpoint/index_data.ts b/x-pack/plugins/security_solution/common/endpoint/index_data.ts index 9a61738cd84b4..b8c2fdbe65f1e 100644 --- a/x-pack/plugins/security_solution/common/endpoint/index_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/index_data.ts @@ -7,6 +7,7 @@ import { Client } from '@elastic/elasticsearch'; import seedrandom from 'seedrandom'; import { EndpointDocGenerator, TreeOptions, Event } from './generate_data'; +import { firstNonNullValue } from './models/ecs_safety_helpers'; export async function indexHostsAndAlerts( client: Client, @@ -86,7 +87,7 @@ async function indexAlerts( // eslint-disable-next-line @typescript-eslint/no-explicit-any (array: Array>, doc) => { let index = eventIndex; - if (doc.event.kind === 'alert') { + if (firstNonNullValue(doc.event?.kind) === 'alert') { index = alertIndex; } array.push({ create: { _index: index } }, doc); diff --git a/x-pack/plugins/security_solution/common/endpoint/models/ecs_safety_helpers.ts b/x-pack/plugins/security_solution/common/endpoint/models/ecs_safety_helpers.ts index 8b419e90a6ee9..5dc75bb707d0e 100644 --- a/x-pack/plugins/security_solution/common/endpoint/models/ecs_safety_helpers.ts +++ b/x-pack/plugins/security_solution/common/endpoint/models/ecs_safety_helpers.ts @@ -46,12 +46,12 @@ export function values(valueOrCollection: ECSField): T[] { if (Array.isArray(valueOrCollection)) { const nonNullValues: T[] = []; for (const value of valueOrCollection) { - if (value !== null) { + if (value !== null && value !== undefined) { nonNullValues.push(value); } } return nonNullValues; - } else if (valueOrCollection !== null) { + } else if (valueOrCollection !== null && valueOrCollection !== undefined) { // if there is a single non-null value, wrap it in an array and return it. return [valueOrCollection]; } else { diff --git a/x-pack/plugins/security_solution/common/endpoint/models/event.test.ts b/x-pack/plugins/security_solution/common/endpoint/models/event.test.ts index 6e6e0f443015b..2b0aa1601ab37 100644 --- a/x-pack/plugins/security_solution/common/endpoint/models/event.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/models/event.test.ts @@ -5,7 +5,7 @@ */ import { EndpointDocGenerator } from '../generate_data'; import { descriptiveName, isProcessRunning } from './event'; -import { ResolverEvent } from '../types'; +import { ResolverEvent, SafeResolverEvent } from '../types'; describe('Generated documents', () => { let generator: EndpointDocGenerator; @@ -17,20 +17,31 @@ describe('Generated documents', () => { it('returns the right name for a registry event', () => { const extensions = { registry: { key: `HKLM/Windows/Software/abc` } }; const event = generator.generateEvent({ eventCategory: 'registry', extensions }); - expect(descriptiveName(event)).toEqual({ subject: `HKLM/Windows/Software/abc` }); + // casting to ResolverEvent here because the `descriptiveName` function is used by the frontend is still relies + // on the unsafe ResolverEvent type. Once it's switched over to the safe version we can remove this cast. + expect(descriptiveName(event as ResolverEvent)).toEqual({ + subject: `HKLM/Windows/Software/abc`, + }); }); it('returns the right name for a network event', () => { const randomIP = `${generator.randomIP()}`; const extensions = { network: { direction: 'outbound', forwarded_ip: randomIP } }; const event = generator.generateEvent({ eventCategory: 'network', extensions }); - expect(descriptiveName(event)).toEqual({ subject: `${randomIP}`, descriptor: 'outbound' }); + // casting to ResolverEvent here because the `descriptiveName` function is used by the frontend is still relies + // on the unsafe ResolverEvent type. Once it's switched over to the safe version we can remove this cast. + expect(descriptiveName(event as ResolverEvent)).toEqual({ + subject: `${randomIP}`, + descriptor: 'outbound', + }); }); it('returns the right name for a file event', () => { const extensions = { file: { path: 'C:\\My Documents\\business\\January\\processName' } }; const event = generator.generateEvent({ eventCategory: 'file', extensions }); - expect(descriptiveName(event)).toEqual({ + // casting to ResolverEvent here because the `descriptiveName` function is used by the frontend is still relies + // on the unsafe ResolverEvent type. Once it's switched over to the safe version we can remove this cast. + expect(descriptiveName(event as ResolverEvent)).toEqual({ subject: 'C:\\My Documents\\business\\January\\processName', }); }); @@ -38,27 +49,31 @@ describe('Generated documents', () => { it('returns the right name for a dns event', () => { const extensions = { dns: { question: { name: `${generator.randomIP()}` } } }; const event = generator.generateEvent({ eventCategory: 'dns', extensions }); - expect(descriptiveName(event)).toEqual({ subject: extensions.dns.question.name }); + // casting to ResolverEvent here because the `descriptiveName` function is used by the frontend is still relies + // on the unsafe ResolverEvent type. Once it's switched over to the safe version we can remove this cast. + expect(descriptiveName(event as ResolverEvent)).toEqual({ + subject: extensions.dns.question.name, + }); }); }); describe('Process running events', () => { it('is a running event when event.type is a string', () => { - const event: ResolverEvent = generator.generateEvent({ + const event: SafeResolverEvent = generator.generateEvent({ eventType: 'start', }); expect(isProcessRunning(event)).toBeTruthy(); }); it('is a running event when event.type is an array of strings', () => { - const event: ResolverEvent = generator.generateEvent({ + const event: SafeResolverEvent = generator.generateEvent({ eventType: ['start'], }); expect(isProcessRunning(event)).toBeTruthy(); }); it('is a running event when event.type is an array of strings and contains start', () => { - let event: ResolverEvent = generator.generateEvent({ + let event: SafeResolverEvent = generator.generateEvent({ eventType: ['bogus', 'start', 'creation'], }); expect(isProcessRunning(event)).toBeTruthy(); @@ -70,35 +85,35 @@ describe('Generated documents', () => { }); it('is not a running event when event.type is only and end type', () => { - const event: ResolverEvent = generator.generateEvent({ + const event: SafeResolverEvent = generator.generateEvent({ eventType: ['end'], }); expect(isProcessRunning(event)).toBeFalsy(); }); it('is not a running event when event.type is empty', () => { - const event: ResolverEvent = generator.generateEvent({ + const event: SafeResolverEvent = generator.generateEvent({ eventType: [], }); expect(isProcessRunning(event)).toBeFalsy(); }); it('is not a running event when event.type is bogus', () => { - const event: ResolverEvent = generator.generateEvent({ + const event: SafeResolverEvent = generator.generateEvent({ eventType: ['bogus'], }); expect(isProcessRunning(event)).toBeFalsy(); }); it('is a running event when event.type contains info', () => { - const event: ResolverEvent = generator.generateEvent({ + const event: SafeResolverEvent = generator.generateEvent({ eventType: ['info'], }); expect(isProcessRunning(event)).toBeTruthy(); }); it('is a running event when event.type contains change', () => { - const event: ResolverEvent = generator.generateEvent({ + const event: SafeResolverEvent = generator.generateEvent({ eventType: ['bogus', 'change'], }); expect(isProcessRunning(event)).toBeTruthy(); diff --git a/x-pack/plugins/security_solution/common/endpoint/models/event.ts b/x-pack/plugins/security_solution/common/endpoint/models/event.ts index a0e9be58911c6..07208214a641a 100644 --- a/x-pack/plugins/security_solution/common/endpoint/models/event.ts +++ b/x-pack/plugins/security_solution/common/endpoint/models/event.ts @@ -9,7 +9,7 @@ import { SafeResolverEvent, SafeLegacyEndpointEvent, } from '../types'; -import { firstNonNullValue } from './ecs_safety_helpers'; +import { firstNonNullValue, hasValue, values } from './ecs_safety_helpers'; /* * Determine if a `ResolverEvent` is the legacy variety. Can be used to narrow `ResolverEvent` to `LegacyEndpointEvent`. @@ -27,32 +27,24 @@ export function isLegacyEvent(event: ResolverEvent): event is LegacyEndpointEven return (event as LegacyEndpointEvent).endgame !== undefined; } -export function isProcessRunning(event: ResolverEvent): boolean { - if (isLegacyEvent(event)) { - return ( - event.event?.type === 'process_start' || - event.event?.action === 'fork_event' || - event.event?.type === 'already_running' - ); - } - - if (Array.isArray(event.event.type)) { +export function isProcessRunning(event: SafeResolverEvent): boolean { + if (isLegacyEventSafeVersion(event)) { return ( - event.event.type.includes('start') || - event.event.type.includes('change') || - event.event.type.includes('info') + hasValue(event.event?.type, 'process_start') || + hasValue(event.event?.action, 'fork_event') || + hasValue(event.event?.type, 'already_running') ); } return ( - event.event.type === 'start' || event.event.type === 'change' || event.event.type === 'info' + hasValue(event.event?.type, 'start') || + hasValue(event.event?.type, 'change') || + hasValue(event.event?.type, 'info') ); } -export function timestampSafeVersion(event: SafeResolverEvent): string | undefined | number { - return isLegacyEventSafeVersion(event) - ? firstNonNullValue(event.endgame?.timestamp_utc) - : firstNonNullValue(event?.['@timestamp']); +export function timestampSafeVersion(event: SafeResolverEvent): undefined | number { + return firstNonNullValue(event?.['@timestamp']); } /** @@ -75,11 +67,7 @@ export function timestampAsDateSafeVersion(event: SafeResolverEvent): Date | und } export function eventTimestamp(event: ResolverEvent): string | undefined | number { - if (isLegacyEvent(event)) { - return event.endgame.timestamp_utc; - } else { - return event['@timestamp']; - } + return event['@timestamp']; } export function eventName(event: ResolverEvent): string { @@ -105,14 +93,7 @@ export function eventId(event: ResolverEvent): number | undefined | string { return event.event.id; } -export function eventSequence(event: ResolverEvent): number | undefined { - if (isLegacyEvent(event)) { - return firstNonNullValue(event.endgame.serial_event_id); - } - return firstNonNullValue(event.event?.sequence); -} - -export function eventSequenceSafeVersion(event: SafeResolverEvent): number | undefined { +export function eventSequence(event: SafeResolverEvent): number | undefined { if (isLegacyEventSafeVersion(event)) { return firstNonNullValue(event.endgame.serial_event_id); } @@ -156,16 +137,16 @@ export function parentEntityIDSafeVersion(event: SafeResolverEvent): string | un return firstNonNullValue(event.process?.parent?.entity_id); } -export function ancestryArray(event: ResolverEvent): string[] | undefined { - if (isLegacyEvent(event)) { +export function ancestryArray(event: SafeResolverEvent): string[] | undefined { + if (isLegacyEventSafeVersion(event)) { return undefined; } // this is to guard against the endpoint accidentally not sending the ancestry array // otherwise the request will fail when really we should just try using the parent entity id - return event.process.Ext?.ancestry; + return values(event.process?.Ext?.ancestry); } -export function getAncestryAsArray(event: ResolverEvent | undefined): string[] { +export function getAncestryAsArray(event: SafeResolverEvent | undefined): string[] { if (!event) { return []; } @@ -175,7 +156,7 @@ export function getAncestryAsArray(event: ResolverEvent | undefined): string[] { return ancestors; } - const parentID = parentEntityId(event); + const parentID = parentEntityIDSafeVersion(event); if (parentID) { return [parentID]; } diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts index e0bd916103a28..cc40225ec1a10 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -112,6 +112,27 @@ export interface ResolverChildNode extends ResolverLifecycleNode { nextChild?: string | null; } +/** + * Safe version of `ResolverChildNode`. + */ +export interface SafeResolverChildNode extends SafeResolverLifecycleNode { + /** + * nextChild can have 3 different states: + * + * undefined: This indicates that you should not use this node for additional queries. It does not mean that node does + * not have any more direct children. The node could have more direct children but to determine that, use the + * ResolverChildren node's nextChild. + * + * null: Indicates that we have received all the children of the node. There may be more descendants though. + * + * string: Indicates this is a leaf node and it can be used to continue querying for additional descendants + * using this node's entity_id + * + * For more information see the resolver docs on pagination [here](../../server/endpoint/routes/resolver/docs/README.md#L129) + */ + nextChild?: string | null; +} + /** * The response structure for the children route. The structure is an array of nodes where each node * has an array of lifecycle events. @@ -131,6 +152,24 @@ export interface ResolverChildren { nextChild: string | null; } +/** + * Safe version of `ResolverChildren`. + */ +export interface SafeResolverChildren { + childNodes: SafeResolverChildNode[]; + /** + * nextChild can have 2 different states: + * + * null: Indicates that we have received all the descendants that can be retrieved using this node. To retrieve more + * nodes in the tree use a cursor provided in one of the returned children. If no other cursor exists then the tree + * is complete. + * + * string: Indicates this node has more descendants that can be retrieved, pass this cursor in while using this node's + * entity_id for the request. + */ + nextChild: string | null; +} + /** * A flattened tree representing the nodes in a resolver graph. */ @@ -148,6 +187,23 @@ export interface ResolverTree { stats: ResolverNodeStats; } +/** + * Safe version of `ResolverTree`. + */ +export interface SafeResolverTree { + /** + * Origin of the tree. This is in the middle of the tree. Typically this would be the same + * process node that generated an alert. + */ + entityID: string; + children: SafeResolverChildren; + relatedEvents: Omit; + relatedAlerts: Omit; + ancestry: SafeResolverAncestry; + lifecycle: SafeResolverEvent[]; + stats: ResolverNodeStats; +} + /** * The lifecycle events (start, end etc) for a node. */ @@ -160,6 +216,18 @@ export interface ResolverLifecycleNode { stats?: ResolverNodeStats; } +/** + * Safe version of `ResolverLifecycleNode`. + */ +export interface SafeResolverLifecycleNode { + entityID: string; + lifecycle: SafeResolverEvent[]; + /** + * stats are only set when the entire tree is being fetched + */ + stats?: ResolverNodeStats; +} + /** * The response structure when searching for ancestors of a node. */ @@ -175,6 +243,21 @@ export interface ResolverAncestry { nextAncestor: string | null; } +/** + * Safe version of `ResolverAncestry`. + */ +export interface SafeResolverAncestry { + /** + * An array of ancestors with the lifecycle events grouped together + */ + ancestors: SafeResolverLifecycleNode[]; + /** + * A cursor for retrieving additional ancestors for a particular node. `null` indicates that there were no additional + * ancestors when the request returned. More could have been ingested by ES after the fact though. + */ + nextAncestor: string | null; +} + /** * Response structure for the related events route. */ @@ -198,7 +281,7 @@ export interface SafeResolverRelatedEvents { */ export interface ResolverRelatedAlerts { entityID: string; - alerts: ResolverEvent[]; + alerts: SafeResolverEvent[]; nextAlert: string | null; } @@ -251,152 +334,133 @@ export interface Host { /** * A record of hashes for something. Provides hashes in multiple formats. A favorite structure of the Elastic Endpoint. */ -interface Hashes { +type Hashes = Partial<{ /** * A hash in MD5 format. */ - md5: string; + md5: ECSField; /** * A hash in SHA-1 format. */ - sha1: string; + sha1: ECSField; /** * A hash in SHA-256 format. */ - sha256: string; -} + sha256: ECSField; +}>; -interface MalwareClassification { - identifier: string; - score: number; - threshold: number; - version: string; -} +type MalwareClassification = Partial<{ + identifier: ECSField; + score: ECSField; + threshold: ECSField; + version: ECSField; +}>; -interface ThreadFields { - id: number; - Ext: { - service_name: string; - start: number; - start_address: number; - start_address_module: string; - }; -} +type ThreadFields = Partial<{ + id: ECSField; + Ext: Partial<{ + service_name: ECSField; + start: ECSField; + start_address: ECSField; + start_address_module: ECSField; + }>; +}>; -interface DllFields { +type DllFields = Partial<{ hash: Hashes; - path: string; - pe: { - architecture: string; - }; - code_signature: { - subject_name: string; - trusted: boolean; - }; - Ext: { - compile_time: number; + path: ECSField; + pe: Partial<{ + architecture: ECSField; + }>; + code_signature: Partial<{ + subject_name: ECSField; + trusted: ECSField; + }>; + Ext: Partial<{ + compile_time: ECSField; malware_classification: MalwareClassification; - mapped_address: number; - mapped_size: number; - }; -} + mapped_address: ECSField; + mapped_size: ECSField; + }>; +}>; /** * Describes an Alert Event. */ -export interface AlertEvent { - '@timestamp': number; - agent: { - id: string; - version: string; - type: string; - }; - ecs: { - version: string; - }; - event: { - id: string; - action: string; - category: string; - kind: string; - dataset: string; - module: string; - type: string; - sequence: number; - }; - Endpoint: { - policy: { - applied: { - id: string; - status: HostPolicyResponseActionStatus; - name: string; - }; - }; - }; - process: { - command_line?: string; - pid: number; - ppid?: number; - entity_id: string; - parent?: { - pid: number; - entity_id: string; - }; - name: string; - hash: Hashes; - executable: string; - start: number; - thread?: ThreadFields[]; - uptime: number; - Ext?: { - /* - * The array has a special format. The entity_ids towards the beginning of the array are closer ancestors and the - * values towards the end of the array are more distant ancestors (grandparents). Therefore - * ancestry_array[0] == process.parent.entity_id and ancestry_array[1] == process.parent.parent.entity_id - */ - ancestry?: string[]; - code_signature: Array<{ - subject_name: string; - trusted: boolean; +export type AlertEvent = Partial<{ + event: Partial<{ + action: ECSField; + dataset: ECSField; + module: ECSField; + }>; + Endpoint: Partial<{ + policy: Partial<{ + applied: Partial<{ + id: ECSField; + status: ECSField; + name: ECSField; }>; - malware_classification?: MalwareClassification; - token: { - domain: string; - type: string; - user: string; - sid: string; - integrity_level: number; - integrity_level_name: string; - privileges?: Array<{ - description: string; - name: string; - enabled: boolean; - }>; - }; - user: string; - }; - }; - file: { - owner: string; - name: string; - path: string; - accessed: number; - mtime: number; - created: number; - size: number; - hash: Hashes; - Ext: { + }>; + }>; + process: Partial<{ + command_line: ECSField; + ppid: ECSField; + start: ECSField; + // Using ECSField as the outer because the object is expected to be an array + thread: ECSField; + uptime: ECSField; + Ext: Partial<{ + // Using ECSField as the outer because the object is expected to be an array + code_signature: ECSField< + Partial<{ + subject_name: ECSField; + trusted: ECSField; + }> + >; malware_classification: MalwareClassification; - temp_file_path: string; - code_signature: Array<{ - trusted: boolean; - subject_name: string; + token: Partial<{ + domain: ECSField; + type: ECSField; + user: ECSField; + sid: ECSField; + integrity_level: ECSField; + integrity_level_name: ECSField; + // Using ECSField as the outer because the object is expected to be an array + privileges: ECSField< + Partial<{ + description: ECSField; + name: ECSField; + enabled: ECSField; + }> + >; }>; - }; - }; - host: Host; - dll?: DllFields[]; -} + user: ECSField; + }>; + }>; + file: Partial<{ + owner: ECSField; + name: ECSField; + accessed: ECSField; + mtime: ECSField; + created: ECSField; + size: ECSField; + hash: Hashes; + Ext: Partial<{ + malware_classification: MalwareClassification; + temp_file_path: ECSField; + // Using ECSField as the outer because the object is expected to be an array + code_signature: ECSField< + Partial<{ + trusted: ECSField; + subject_name: ECSField; + }> + >; + }>; + }>; + // Using ECSField as the outer because the object is expected to be an array + dll: ECSField; +}> & + SafeEndpointEvent; /** * The status of the Endpoint Agent as reported by the Agent or the @@ -585,7 +649,7 @@ export type ResolverEvent = EndpointEvent | LegacyEndpointEvent; * All mappings in Elasticsearch support arrays. They can also return null values or be missing. For example, a `keyword` mapping could return `null` or `[null]` or `[]` or `'hi'`, or `['hi', 'there']`. We need to handle these cases in order to avoid throwing an error. * When dealing with an value that comes from ES, wrap the underlying type in `ECSField`. For example, if you have a `keyword` or `text` value coming from ES, cast it to `ECSField`. */ -export type ECSField = T | null | Array; +export type ECSField = T | null | undefined | Array; /** * A more conservative version of `ResolverEvent` that treats fields as optional and use `ECSField` to type all ECS fields. @@ -648,9 +712,7 @@ export type SafeEndpointEvent = Partial<{ subject_name: ECSField; }>; pid: ECSField; - hash: Partial<{ - md5: ECSField; - }>; + hash: Hashes; parent: Partial<{ entity_id: ECSField; name: ECSField; diff --git a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/__snapshots__/isometric_taxi_layout.test.ts.snap b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/__snapshots__/isometric_taxi_layout.test.ts.snap index db8d047c2ce86..fc0d646fd62ca 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/__snapshots__/isometric_taxi_layout.test.ts.snap +++ b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/__snapshots__/isometric_taxi_layout.test.ts.snap @@ -212,6 +212,10 @@ Object { }, Object { "metadata": Object { + "elapsedTime": Object { + "duration": "<1", + "durationType": "millisecond", + }, "uniqueId": "edge:0:1", }, "points": Array [ @@ -227,6 +231,10 @@ Object { }, Object { "metadata": Object { + "elapsedTime": Object { + "duration": "<1", + "durationType": "millisecond", + }, "uniqueId": "edge:0:2", }, "points": Array [ @@ -242,6 +250,10 @@ Object { }, Object { "metadata": Object { + "elapsedTime": Object { + "duration": "<1", + "durationType": "millisecond", + }, "uniqueId": "edge:0:8", }, "points": Array [ @@ -287,6 +299,10 @@ Object { }, Object { "metadata": Object { + "elapsedTime": Object { + "duration": "<1", + "durationType": "millisecond", + }, "uniqueId": "edge:1:3", }, "points": Array [ @@ -302,6 +318,10 @@ Object { }, Object { "metadata": Object { + "elapsedTime": Object { + "duration": "<1", + "durationType": "millisecond", + }, "uniqueId": "edge:1:4", }, "points": Array [ @@ -347,6 +367,10 @@ Object { }, Object { "metadata": Object { + "elapsedTime": Object { + "duration": "<1", + "durationType": "millisecond", + }, "uniqueId": "edge:2:5", }, "points": Array [ @@ -362,6 +386,10 @@ Object { }, Object { "metadata": Object { + "elapsedTime": Object { + "duration": "<1", + "durationType": "millisecond", + }, "uniqueId": "edge:2:6", }, "points": Array [ @@ -377,6 +405,10 @@ Object { }, Object { "metadata": Object { + "elapsedTime": Object { + "duration": "<1", + "durationType": "millisecond", + }, "uniqueId": "edge:6:7", }, "points": Array [ @@ -584,6 +616,10 @@ Object { "edgeLineSegments": Array [ Object { "metadata": Object { + "elapsedTime": Object { + "duration": "<1", + "durationType": "millisecond", + }, "uniqueId": "edge:0:1", }, "points": Array [ diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts index e6e525334e818..1e2de06ea4af5 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts @@ -10,8 +10,9 @@ import { dataReducer } from './reducer'; import * as selectors from './selectors'; import { DataState } from '../../types'; import { DataAction } from './action'; -import { ResolverChildNode, ResolverTree } from '../../../../common/endpoint/types'; +import { ResolverChildNode, ResolverEvent, ResolverTree } from '../../../../common/endpoint/types'; import * as eventModel from '../../../../common/endpoint/models/event'; +import { values } from '../../../../common/endpoint/models/ecs_safety_helpers'; import { mockTreeFetcherParameters } from '../../mocks/tree_fetcher_parameters'; /** @@ -40,7 +41,9 @@ describe('Resolver Data Middleware', () => { // Generate a 'tree' using the Resolver generator code. This structure isn't the same as what the API returns. const baseTree = generateBaseTree(); const tree = mockResolverTree({ - events: baseTree.allEvents, + // Casting here because the generator returns the SafeResolverEvent type which isn't yet compatible with + // a lot of the frontend functions. So casting it back to the unsafe type for now. + events: baseTree.allEvents as ResolverEvent[], cursors: { childrenNextChild: 'aValidChildCursor', ancestryNextAncestor: 'aValidAncestorCursor', @@ -89,7 +92,9 @@ describe('Resolver Data Middleware', () => { type: 'serverReturnedRelatedEventData', payload: { entityID: firstChildNodeInTree.id, - events: firstChildNodeInTree.relatedEvents, + // Casting here because the generator returns the SafeResolverEvent type which isn't yet compatible with + // a lot of the frontend functions. So casting it back to the unsafe type for now. + events: firstChildNodeInTree.relatedEvents as ResolverEvent[], nextEvent: null, }, }; @@ -162,7 +167,9 @@ describe('Resolver Data Middleware', () => { type: 'serverReturnedRelatedEventData', payload: { entityID: firstChildNodeInTree.id, - events: firstChildNodeInTree.relatedEvents, + // Casting here because the generator returns the SafeResolverEvent type which isn't yet compatible with + // a lot of the frontend functions. So casting it back to the unsafe type for now. + events: firstChildNodeInTree.relatedEvents as ResolverEvent[], nextEvent: 'aValidNextEventCursor', }, }; @@ -232,7 +239,9 @@ function mockedTree() { const statsResults = compileStatsForChild(firstChildNodeInTree); const tree = mockResolverTree({ - events: baseTree.allEvents, + // Casting here because the generator returns the SafeResolverEvent type which isn't yet compatible with + // a lot of the frontend functions. So casting it back to the unsafe type for now. + events: baseTree.allEvents as ResolverEvent[], /** * Calculate children from the ResolverTree response using the children of the `Tree` we generated using the Resolver data generator code. * Compile (and attach) stats to the first child node. @@ -243,14 +252,15 @@ function mockedTree() { * related event limits should be shown. */ children: [...baseTree.children.values()].map((node: TreeNode) => { - // Treat each `TreeNode` as a `ResolverChildNode`. - // These types are almost close enough to be used interchangably (for the purposes of this test.) - const childNode: Partial = node; + const childNode: Partial = {}; + // Casting here because the generator returns the SafeResolverEvent type which isn't yet compatible with + // a lot of the frontend functions. So casting it back to the unsafe type for now. + childNode.lifecycle = node.lifecycle as ResolverEvent[]; // `TreeNode` has `id` which is the same as `entityID`. // The `ResolverChildNode` calls the entityID as `entityID`. // Set `entityID` on `childNode` since the code in test relies on it. - childNode.entityID = (childNode as TreeNode).id; + childNode.entityID = node.id; // This should only be true for the first child. if (node.id === firstChildNodeInTree.id) { @@ -315,10 +325,8 @@ function compileStatsForChild( const compiledStats = node.relatedEvents.reduce( (counts: Record, relatedEvent) => { - // `relatedEvent.event.category` is `string | string[]`. - // Wrap it in an array and flatten that array to get a `string[] | [string]` - // which we can loop over. - const categories: string[] = [relatedEvent.event.category].flat(); + // get an array of categories regardless of whether category is a string or string[] + const categories: string[] = values(relatedEvent.event?.category); for (const category of categories) { // Set the first category as 'categoryToOverCount' diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/alerts.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/alerts.ts index 54c6cf432aa89..8f68cba893108 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/alerts.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/alerts.ts @@ -5,7 +5,7 @@ */ import { SearchResponse } from 'elasticsearch'; import { esKuery } from '../../../../../../../../src/plugins/data/server'; -import { ResolverEvent } from '../../../../../common/endpoint/types'; +import { SafeResolverEvent } from '../../../../../common/endpoint/types'; import { ResolverQuery } from './base'; import { PaginationBuilder } from '../utils/pagination'; import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common'; @@ -13,7 +13,7 @@ import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/com /** * Builds a query for retrieving alerts for a node. */ -export class AlertsQuery extends ResolverQuery { +export class AlertsQuery extends ResolverQuery { private readonly kqlQuery: JsonObject[] = []; constructor( private readonly pagination: PaginationBuilder, @@ -68,7 +68,7 @@ export class AlertsQuery extends ResolverQuery { }; } - formatResponse(response: SearchResponse): ResolverEvent[] { + formatResponse(response: SearchResponse): SafeResolverEvent[] { return this.getResults(response); } } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/base.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/base.ts index 0d8a42d7a26f3..a2bdf358745c2 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/base.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/base.ts @@ -6,7 +6,7 @@ import { SearchResponse } from 'elasticsearch'; import { ILegacyScopedClusterClient } from 'kibana/server'; -import { ResolverEvent } from '../../../../../common/endpoint/types'; +import { SafeResolverEvent } from '../../../../../common/endpoint/types'; import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common'; import { legacyEventIndexPattern } from './legacy_event_index_pattern'; import { MSearchQuery } from './multi_searcher'; @@ -19,7 +19,7 @@ import { MSearchQuery } from './multi_searcher'; * @param R the is the type after transforming ES's response. Making this definable let's us set whether it is a resolver event * or something else. */ -export abstract class ResolverQuery implements MSearchQuery { +export abstract class ResolverQuery implements MSearchQuery { /** * * @param indexPattern the index pattern to use in the query for finding indices with documents in ES. @@ -77,7 +77,7 @@ export abstract class ResolverQuery implements MSearchQuer * @param ids a single more multiple unique node ids (e.g. entity_id or unique_pid) */ async searchAndFormat(client: ILegacyScopedClusterClient, ids: string | string[]): Promise { - const res: SearchResponse = await this.search(client, ids); + const res: SearchResponse = await this.search(client, ids); return this.formatResponse(res); } @@ -113,5 +113,5 @@ export abstract class ResolverQuery implements MSearchQuer * @param response a SearchResponse from ES resulting from executing this query * @returns the translated ES response into a structured object */ - public abstract formatResponse(response: SearchResponse): T; + public abstract formatResponse(response: SearchResponse): T; } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.ts index 6fb38a32f9581..8c7daf9451217 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { SearchResponse } from 'elasticsearch'; -import { ResolverEvent } from '../../../../../common/endpoint/types'; +import { SafeResolverEvent } from '../../../../../common/endpoint/types'; import { ResolverQuery } from './base'; import { ChildrenPaginationBuilder } from '../utils/children_pagination'; import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common'; @@ -12,7 +12,7 @@ import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/com /** * Builds a query for retrieving descendants of a node. */ -export class ChildrenQuery extends ResolverQuery { +export class ChildrenQuery extends ResolverQuery { constructor( private readonly pagination: ChildrenPaginationBuilder, indexPattern: string | string[], @@ -126,7 +126,7 @@ export class ChildrenQuery extends ResolverQuery { }; } - formatResponse(response: SearchResponse): ResolverEvent[] { + formatResponse(response: SearchResponse): SafeResolverEvent[] { return this.getResults(response); } } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts index 0969a3c360e4a..bd054d548a93a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts @@ -5,7 +5,7 @@ */ import { SearchResponse } from 'elasticsearch'; import { esKuery } from '../../../../../../../../src/plugins/data/server'; -import { ResolverEvent } from '../../../../../common/endpoint/types'; +import { SafeResolverEvent } from '../../../../../common/endpoint/types'; import { ResolverQuery } from './base'; import { PaginationBuilder } from '../utils/pagination'; import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common'; @@ -13,7 +13,7 @@ import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/com /** * Builds a query for retrieving related events for a node. */ -export class EventsQuery extends ResolverQuery { +export class EventsQuery extends ResolverQuery { private readonly kqlQuery: JsonObject[] = []; constructor( @@ -83,7 +83,7 @@ export class EventsQuery extends ResolverQuery { }; } - formatResponse(response: SearchResponse): ResolverEvent[] { + formatResponse(response: SearchResponse): SafeResolverEvent[] { return this.getResults(response); } } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/lifecycle.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/lifecycle.ts index 0b5728958e91f..ecbc5d8344928 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/lifecycle.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/lifecycle.ts @@ -6,12 +6,12 @@ import { SearchResponse } from 'elasticsearch'; import { ResolverQuery } from './base'; import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common'; -import { ResolverEvent } from '../../../../../common/endpoint/types'; +import { SafeResolverEvent } from '../../../../../common/endpoint/types'; /** * Builds a query for retrieving life cycle information about a node (start, stop, etc). */ -export class LifecycleQuery extends ResolverQuery { +export class LifecycleQuery extends ResolverQuery { protected legacyQuery(endpointID: string, uniquePIDs: string[]): JsonObject { return { query: { @@ -59,7 +59,7 @@ export class LifecycleQuery extends ResolverQuery { }; } - formatResponse(response: SearchResponse): ResolverEvent[] { + formatResponse(response: SearchResponse): SafeResolverEvent[] { return this.getResults(response); } } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/multi_searcher.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/multi_searcher.ts index 02dbd92d9252b..76203973a6211 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/multi_searcher.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/multi_searcher.ts @@ -6,7 +6,7 @@ import { ILegacyScopedClusterClient } from 'kibana/server'; import { MSearchResponse, SearchResponse } from 'elasticsearch'; -import { ResolverEvent } from '../../../../../common/endpoint/types'; +import { SafeResolverEvent } from '../../../../../common/endpoint/types'; import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common'; /** @@ -37,7 +37,7 @@ export interface QueryInfo { /** * a function to handle the response */ - handler: (response: SearchResponse) => void; + handler: (response: SearchResponse) => void; } /** @@ -65,7 +65,7 @@ export class MultiSearcher { for (const info of queries) { searchQuery.push(...info.query.buildMSearch(info.ids)); } - const res: MSearchResponse = await this.client.callAsCurrentUser('msearch', { + const res: MSearchResponse = await this.client.callAsCurrentUser('msearch', { body: searchQuery, }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/stats.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/stats.ts index b8fa409e2ca21..50e56258b7448 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/stats.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/stats.ts @@ -5,7 +5,7 @@ */ import { SearchResponse } from 'elasticsearch'; import { ResolverQuery } from './base'; -import { ResolverEvent, EventStats } from '../../../../../common/endpoint/types'; +import { SafeResolverEvent, EventStats } from '../../../../../common/endpoint/types'; import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common'; export interface StatsResult { @@ -185,7 +185,7 @@ export class StatsQuery extends ResolverQuery { }; } - public formatResponse(response: SearchResponse): StatsResult { + public formatResponse(response: SearchResponse): StatsResult { let alerts: Record = {}; if (response.aggregations?.alerts?.ids?.buckets) { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/alerts_query_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/alerts_query_handler.ts index efffbc10473d4..f34218ddbde9b 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/alerts_query_handler.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/alerts_query_handler.ts @@ -6,7 +6,7 @@ import { SearchResponse } from 'elasticsearch'; import { ILegacyScopedClusterClient } from 'kibana/server'; -import { ResolverRelatedAlerts, ResolverEvent } from '../../../../../common/endpoint/types'; +import { ResolverRelatedAlerts, SafeResolverEvent } from '../../../../../common/endpoint/types'; import { createRelatedAlerts } from './node'; import { AlertsQuery } from '../queries/alerts'; import { PaginationBuilder } from './pagination'; @@ -45,7 +45,7 @@ export class RelatedAlertsQueryHandler implements SingleQueryHandler) => { + private handleResponse = (response: SearchResponse) => { const results = this.query.formatResponse(response); this.relatedAlerts = createRelatedAlerts( this.entityID, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts index 7dd47658bc4c1..b796913118c99 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts @@ -7,14 +7,14 @@ import { SearchResponse } from 'elasticsearch'; import { ILegacyScopedClusterClient } from 'kibana/server'; import { - parentEntityId, - entityId, + parentEntityIDSafeVersion, + entityIDSafeVersion, getAncestryAsArray, } from '../../../../../common/endpoint/models/event'; import { - ResolverAncestry, - ResolverEvent, - ResolverLifecycleNode, + SafeResolverAncestry, + SafeResolverEvent, + SafeResolverLifecycleNode, } from '../../../../../common/endpoint/types'; import { createAncestry, createLifecycle } from './node'; import { LifecycleQuery } from '../queries/lifecycle'; @@ -24,8 +24,8 @@ import { QueryHandler } from './fetch'; /** * Retrieve the ancestry portion of a resolver tree. */ -export class AncestryQueryHandler implements QueryHandler { - private readonly ancestry: ResolverAncestry = createAncestry(); +export class AncestryQueryHandler implements QueryHandler { + private readonly ancestry: SafeResolverAncestry = createAncestry(); private ancestorsToFind: string[]; private readonly query: LifecycleQuery; @@ -33,7 +33,7 @@ export class AncestryQueryHandler implements QueryHandler { private levels: number, indexPattern: string, legacyEndpointID: string | undefined, - originNode: ResolverLifecycleNode | undefined + originNode: SafeResolverLifecycleNode | undefined ) { this.ancestorsToFind = getAncestryAsArray(originNode?.lifecycle[0]).slice(0, levels); this.query = new LifecycleQuery(indexPattern, legacyEndpointID); @@ -41,21 +41,28 @@ export class AncestryQueryHandler implements QueryHandler { // add the origin node to the response if it exists if (originNode) { this.ancestry.ancestors.push(originNode); - this.ancestry.nextAncestor = parentEntityId(originNode.lifecycle[0]) || null; + this.ancestry.nextAncestor = parentEntityIDSafeVersion(originNode.lifecycle[0]) || null; } } - private toMapOfNodes(results: ResolverEvent[]) { - return results.reduce((nodes: Map, event: ResolverEvent) => { - const nodeId = entityId(event); - let node = nodes.get(nodeId); - if (!node) { - node = createLifecycle(nodeId, []); - } + private toMapOfNodes(results: SafeResolverEvent[]) { + return results.reduce( + (nodes: Map, event: SafeResolverEvent) => { + const nodeId = entityIDSafeVersion(event); + if (!nodeId) { + return nodes; + } + + let node = nodes.get(nodeId); + if (!node) { + node = createLifecycle(nodeId, []); + } - node.lifecycle.push(event); - return nodes.set(nodeId, node); - }, new Map()); + node.lifecycle.push(event); + return nodes.set(nodeId, node); + }, + new Map() + ); } private setNoMore() { @@ -64,7 +71,7 @@ export class AncestryQueryHandler implements QueryHandler { this.levels = 0; } - private handleResponse = (searchResp: SearchResponse) => { + private handleResponse = (searchResp: SearchResponse) => { const results = this.query.formatResponse(searchResp); if (results.length === 0) { this.setNoMore(); @@ -97,7 +104,7 @@ export class AncestryQueryHandler implements QueryHandler { * Hence: [D, E, B, C, A] */ this.ancestry.ancestors.push(...ancestryNodes.values()); - this.ancestry.nextAncestor = parentEntityId(results[0]) || null; + this.ancestry.nextAncestor = parentEntityIDSafeVersion(results[0]) || null; this.levels = this.levels - ancestryNodes.size; // the results come back in ascending order on timestamp so the first entry in the // results should be the further ancestor (most distant grandparent) diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts index 78e4219aad75c..d33e9a2d70af6 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts @@ -10,7 +10,7 @@ import { TreeNode, } from '../../../../../common/endpoint/generate_data'; import { ChildrenNodesHelper } from './children_helper'; -import { eventId, isProcessRunning } from '../../../../../common/endpoint/models/event'; +import { eventIDSafeVersion, isProcessRunning } from '../../../../../common/endpoint/models/event'; function getStartEvents(events: Event[]): Event[] { const startEvents: Event[] = []; @@ -179,7 +179,9 @@ describe('Children helper', () => { childrenNodes.childNodes.forEach((node) => { node.lifecycle.forEach((event) => { - expect(childrenEvents.find((child) => child.event.id === eventId(event))).toEqual(event); + expect( + childrenEvents.find((child) => eventIDSafeVersion(child) === eventIDSafeVersion(event)) + ).toEqual(event); }); }); }); @@ -191,7 +193,9 @@ describe('Children helper', () => { const childrenNodes = helper.getNodes(); childrenNodes.childNodes.forEach((node) => { node.lifecycle.forEach((event) => { - expect(childrenEvents.find((child) => child.event.id === eventId(event))).toEqual(event); + expect( + childrenEvents.find((child) => eventIDSafeVersion(child) === eventIDSafeVersion(event)) + ).toEqual(event); }); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts index b82b972b887b5..e9174548898dd 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts @@ -5,15 +5,15 @@ */ import { - entityId, - parentEntityId, + parentEntityIDSafeVersion, isProcessRunning, getAncestryAsArray, + entityIDSafeVersion, } from '../../../../../common/endpoint/models/event'; import { - ResolverChildNode, - ResolverEvent, - ResolverChildren, + SafeResolverChildren, + SafeResolverChildNode, + SafeResolverEvent, } from '../../../../../common/endpoint/types'; import { createChild } from './node'; import { ChildrenPaginationBuilder } from './children_pagination'; @@ -22,7 +22,7 @@ import { ChildrenPaginationBuilder } from './children_pagination'; * This class helps construct the children structure when building a resolver tree. */ export class ChildrenNodesHelper { - private readonly entityToNodeCache: Map = new Map(); + private readonly entityToNodeCache: Map = new Map(); constructor(private readonly rootID: string, private readonly limit: number) { this.entityToNodeCache.set(rootID, createChild(rootID)); @@ -31,8 +31,8 @@ export class ChildrenNodesHelper { /** * Constructs a ResolverChildren response based on the children that were previously add. */ - getNodes(): ResolverChildren { - const cacheCopy: Map = new Map(this.entityToNodeCache); + getNodes(): SafeResolverChildren { + const cacheCopy: Map = new Map(this.entityToNodeCache); const rootNode = cacheCopy.get(this.rootID); let rootNextChild = null; @@ -51,7 +51,7 @@ export class ChildrenNodesHelper { * Get the entity_ids of the nodes that are cached. */ getEntityIDs(): string[] { - const cacheCopy: Map = new Map(this.entityToNodeCache); + const cacheCopy: Map = new Map(this.entityToNodeCache); cacheCopy.delete(this.rootID); return Array.from(cacheCopy.keys()); } @@ -69,9 +69,9 @@ export class ChildrenNodesHelper { * * @param lifecycle an array of resolver lifecycle events for different process nodes returned from ES. */ - addLifecycleEvents(lifecycle: ResolverEvent[]) { + addLifecycleEvents(lifecycle: SafeResolverEvent[]) { for (const event of lifecycle) { - const entityID = entityId(event); + const entityID = entityIDSafeVersion(event); if (entityID) { const cachedChild = this.getOrCreateChildNode(entityID); cachedChild.lifecycle.push(event); @@ -86,19 +86,22 @@ export class ChildrenNodesHelper { * @param queriedNodes the entity_ids of the nodes that returned these start events * @param startEvents an array of start events returned by ES */ - addStartEvents(queriedNodes: Set, startEvents: ResolverEvent[]): Set | undefined { + addStartEvents( + queriedNodes: Set, + startEvents: SafeResolverEvent[] + ): Set | undefined { let largestAncestryArray = 0; const nodesToQueryNext: Map> = new Map(); - const nonLeafNodes: Set = new Set(); + const nonLeafNodes: Set = new Set(); - const isDistantGrandchild = (event: ResolverEvent) => { + const isDistantGrandchild = (event: SafeResolverEvent) => { const ancestry = getAncestryAsArray(event); return ancestry.length > 0 && queriedNodes.has(ancestry[ancestry.length - 1]); }; for (const event of startEvents) { - const parentID = parentEntityId(event); - const entityID = entityId(event); + const parentID = parentEntityIDSafeVersion(event); + const entityID = entityIDSafeVersion(event); if (parentID && entityID && isProcessRunning(event)) { // don't actually add the start event to the node, because that'll be done in // a different call @@ -158,7 +161,7 @@ export class ChildrenNodesHelper { return nodesToQueryNext.get(largestAncestryArray); } - private setPaginationForNodes(nodes: Set, startEvents: ResolverEvent[]) { + private setPaginationForNodes(nodes: Set, startEvents: SafeResolverEvent[]) { for (const nodeEntityID of nodes.values()) { const cachedNode = this.entityToNodeCache.get(nodeEntityID); if (cachedNode) { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_lifecycle_query_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_lifecycle_query_handler.ts index ab610dc9776ca..f9f73c2ad75ff 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_lifecycle_query_handler.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_lifecycle_query_handler.ts @@ -6,7 +6,7 @@ import { SearchResponse } from 'elasticsearch'; import { ILegacyScopedClusterClient } from 'kibana/server'; -import { ResolverEvent, ResolverChildren } from '../../../../../common/endpoint/types'; +import { SafeResolverEvent, SafeResolverChildren } from '../../../../../common/endpoint/types'; import { LifecycleQuery } from '../queries/lifecycle'; import { QueryInfo } from '../queries/multi_searcher'; import { SingleQueryHandler } from './fetch'; @@ -16,8 +16,8 @@ import { createChildren } from './node'; /** * Returns the children of a resolver tree. */ -export class ChildrenLifecycleQueryHandler implements SingleQueryHandler { - private lifecycle: ResolverChildren | undefined; +export class ChildrenLifecycleQueryHandler implements SingleQueryHandler { + private lifecycle: SafeResolverChildren | undefined; private readonly query: LifecycleQuery; constructor( private readonly childrenHelper: ChildrenNodesHelper, @@ -27,7 +27,7 @@ export class ChildrenLifecycleQueryHandler implements SingleQueryHandler) => { + private handleResponse = (response: SearchResponse) => { this.childrenHelper.addLifecycleEvents(this.query.formatResponse(response)); this.lifecycle = this.childrenHelper.getNodes(); }; @@ -50,7 +50,7 @@ export class ChildrenLifecycleQueryHandler implements SingleQueryHandler) => { + private handleResponse = (response: SearchResponse) => { const results = this.query.formatResponse(response); this.nodesToQuery = this.childrenHelper.addStartEvents(this.nodesToQuery, results) ?? new Set(); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/events_query_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/events_query_handler.ts index 8792f917fb4d6..5c4d9a4741ad7 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/events_query_handler.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/events_query_handler.ts @@ -6,7 +6,7 @@ import { SearchResponse } from 'elasticsearch'; import { ILegacyScopedClusterClient } from 'kibana/server'; -import { ResolverRelatedEvents, ResolverEvent } from '../../../../../common/endpoint/types'; +import { SafeResolverRelatedEvents, SafeResolverEvent } from '../../../../../common/endpoint/types'; import { createRelatedEvents } from './node'; import { EventsQuery } from '../queries/events'; import { PaginationBuilder } from './pagination'; @@ -28,8 +28,8 @@ export interface RelatedEventsParams { /** * This retrieves the related events for the origin node of a resolver tree. */ -export class RelatedEventsQueryHandler implements SingleQueryHandler { - private relatedEvents: ResolverRelatedEvents | undefined; +export class RelatedEventsQueryHandler implements SingleQueryHandler { + private relatedEvents: SafeResolverRelatedEvents | undefined; private readonly query: EventsQuery; private readonly limit: number; private readonly entityID: string; @@ -46,7 +46,7 @@ export class RelatedEventsQueryHandler implements SingleQueryHandler) => { + private handleResponse = (response: SearchResponse) => { const results = this.query.formatResponse(response); this.relatedEvents = createRelatedEvents( this.entityID, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/fetch.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/fetch.ts index 1b88f965909eb..15a9639872f2a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/fetch.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/fetch.ts @@ -6,11 +6,11 @@ import { ILegacyScopedClusterClient } from 'kibana/server'; import { - ResolverChildren, - ResolverRelatedEvents, - ResolverAncestry, + SafeResolverChildren, + SafeResolverRelatedEvents, + SafeResolverAncestry, ResolverRelatedAlerts, - ResolverLifecycleNode, + SafeResolverLifecycleNode, } from '../../../../../common/endpoint/types'; import { Tree } from './tree'; import { LifecycleQuery } from '../queries/lifecycle'; @@ -190,7 +190,7 @@ export class Fetcher { * * @param limit upper limit of ancestors to retrieve */ - public async ancestors(limit: number): Promise { + public async ancestors(limit: number): Promise { const originNode = await this.getNode(this.id); const ancestryHandler = new AncestryQueryHandler( limit, @@ -207,7 +207,7 @@ export class Fetcher { * @param limit the number of children to retrieve for a single level * @param after a cursor to use as the starting point for retrieving children */ - public async children(limit: number, after?: string): Promise { + public async children(limit: number, after?: string): Promise { const childrenHandler = new ChildrenStartQueryHandler( limit, this.id, @@ -237,7 +237,7 @@ export class Fetcher { limit: number, after?: string, filter?: string - ): Promise { + ): Promise { const eventsHandler = new RelatedEventsQueryHandler({ limit, entityID: this.id, @@ -285,7 +285,7 @@ export class Fetcher { return tree; } - private async getNode(entityID: string): Promise { + private async getNode(entityID: string): Promise { const query = new LifecycleQuery(this.eventsIndexPattern, this.endpointID); const results = await query.searchAndFormat(this.client, entityID); if (results.length === 0) { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/lifecycle_query_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/lifecycle_query_handler.ts index ab0501e099490..d4dc12d5e8b66 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/lifecycle_query_handler.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/lifecycle_query_handler.ts @@ -6,7 +6,7 @@ import { SearchResponse } from 'elasticsearch'; import { ILegacyScopedClusterClient } from 'kibana/server'; -import { ResolverEvent, ResolverLifecycleNode } from '../../../../../common/endpoint/types'; +import { SafeResolverEvent, SafeResolverLifecycleNode } from '../../../../../common/endpoint/types'; import { LifecycleQuery } from '../queries/lifecycle'; import { QueryInfo } from '../queries/multi_searcher'; import { SingleQueryHandler } from './fetch'; @@ -15,8 +15,8 @@ import { createLifecycle } from './node'; /** * Retrieve the lifecycle events for a node. */ -export class LifecycleQueryHandler implements SingleQueryHandler { - private lifecycle: ResolverLifecycleNode | undefined; +export class LifecycleQueryHandler implements SingleQueryHandler { + private lifecycle: SafeResolverLifecycleNode | undefined; private readonly query: LifecycleQuery; constructor( private readonly entityID: string, @@ -26,7 +26,7 @@ export class LifecycleQueryHandler implements SingleQueryHandler) => { + private handleResponse = (response: SearchResponse) => { const results = this.query.formatResponse(response); if (results.length !== 0) { this.lifecycle = createLifecycle(this.entityID, results); @@ -51,7 +51,7 @@ export class LifecycleQueryHandler implements SingleQueryHandler { const generator = new EndpointDocGenerator(); - const getSearchAfterInfo = (events: EndpointEvent[]) => { + const getSearchAfterInfo = (events: SafeEndpointEvent[]) => { const lastEvent = events[events.length - 1]; - return [lastEvent['@timestamp'], lastEvent.event.id]; + return [timestampSafeVersion(lastEvent), eventIDSafeVersion(lastEvent)]; }; describe('cursor', () => { const root = generator.generateEvent(); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.ts index 4a6c65e55a6b6..af0311a262f30 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.ts @@ -4,8 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ResolverEvent } from '../../../../../common/endpoint/types'; -import { eventId } from '../../../../../common/endpoint/models/event'; +import { SafeResolverEvent } from '../../../../../common/endpoint/types'; +import { + eventIDSafeVersion, + timestampSafeVersion, +} from '../../../../../common/endpoint/models/event'; import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common'; import { ChildrenPaginationCursor } from './children_pagination'; @@ -116,11 +119,12 @@ export class PaginationBuilder { * * @param results the events that were returned by the ES query */ - static buildCursor(results: ResolverEvent[]): string | null { + static buildCursor(results: SafeResolverEvent[]): string | null { const lastResult = results[results.length - 1]; const cursor = { - timestamp: lastResult['@timestamp'], - eventID: eventId(lastResult) === undefined ? '' : String(eventId(lastResult)), + timestamp: timestampSafeVersion(lastResult) ?? 0, + eventID: + eventIDSafeVersion(lastResult) === undefined ? '' : String(eventIDSafeVersion(lastResult)), }; return urlEncodeCursor(cursor); } @@ -131,7 +135,10 @@ export class PaginationBuilder { * @param requestLimit the request limit for a query. * @param results the events that were returned by the ES query */ - static buildCursorRequestLimit(requestLimit: number, results: ResolverEvent[]): string | null { + static buildCursorRequestLimit( + requestLimit: number, + results: SafeResolverEvent[] + ): string | null { if (requestLimit <= results.length && results.length > 0) { return PaginationBuilder.buildCursor(results); } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.test.ts index 21db11f3affd3..290af87a61b1d 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.test.ts @@ -7,28 +7,28 @@ import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data'; import { Tree } from './tree'; import { - ResolverAncestry, - ResolverEvent, - ResolverRelatedEvents, + SafeResolverAncestry, + SafeResolverEvent, + SafeResolverRelatedEvents, } from '../../../../../common/endpoint/types'; -import { entityId } from '../../../../../common/endpoint/models/event'; +import { entityIDSafeVersion } from '../../../../../common/endpoint/models/event'; describe('Tree', () => { const generator = new EndpointDocGenerator(); describe('ancestry', () => { // transform the generator's array of events into the format expected by the tree class - const ancestorInfo: ResolverAncestry = { + const ancestorInfo: SafeResolverAncestry = { ancestors: generator .createAlertEventAncestry({ ancestors: 5, percentTerminated: 0, percentWithRelated: 0 }) .filter((event) => { - return event.event.kind === 'event'; + return event.event?.kind === 'event'; }) .map((event) => { return { - entityID: event.process.entity_id, + entityID: entityIDSafeVersion(event) ?? '', // The generator returns Events, but the tree needs a ResolverEvent - lifecycle: [event as ResolverEvent], + lifecycle: [event as SafeResolverEvent], }; }), nextAncestor: 'hello', @@ -39,7 +39,7 @@ describe('Tree', () => { const ids = tree.ids(); ids.forEach((id) => { const foundAncestor = ancestorInfo.ancestors.find( - (ancestor) => entityId(ancestor.lifecycle[0]) === id + (ancestor) => entityIDSafeVersion(ancestor.lifecycle[0]) === id ); expect(foundAncestor).not.toBeUndefined(); }); @@ -50,12 +50,12 @@ describe('Tree', () => { describe('related events', () => { it('adds related events to the tree', () => { const root = generator.generateEvent(); - const events: ResolverRelatedEvents = { - entityID: root.process.entity_id, + const events: SafeResolverRelatedEvents = { + entityID: entityIDSafeVersion(root) ?? '', events: Array.from(generator.relatedEventsGenerator(root)), nextEvent: null, }; - const tree = new Tree(root.process.entity_id, { relatedEvents: events }); + const tree = new Tree(entityIDSafeVersion(root) ?? '', { relatedEvents: events }); const rendered = tree.render(); expect(rendered.relatedEvents.nextEvent).toBeNull(); expect(rendered.relatedEvents.events).toStrictEqual(events.events); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.ts index 3f941851a4143..dd493d70ffcd3 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.ts @@ -6,26 +6,26 @@ import _ from 'lodash'; import { - ResolverEvent, + SafeResolverEvent, ResolverNodeStats, - ResolverRelatedEvents, - ResolverAncestry, - ResolverTree, - ResolverChildren, + SafeResolverRelatedEvents, + SafeResolverAncestry, + SafeResolverTree, + SafeResolverChildren, ResolverRelatedAlerts, } from '../../../../../common/endpoint/types'; import { createTree } from './node'; interface Node { entityID: string; - lifecycle: ResolverEvent[]; + lifecycle: SafeResolverEvent[]; stats?: ResolverNodeStats; } export interface Options { - relatedEvents?: ResolverRelatedEvents; - ancestry?: ResolverAncestry; - children?: ResolverChildren; + relatedEvents?: SafeResolverRelatedEvents; + ancestry?: SafeResolverAncestry; + children?: SafeResolverChildren; relatedAlerts?: ResolverRelatedAlerts; } @@ -37,7 +37,7 @@ export interface Options { */ export class Tree { protected cache: Map = new Map(); - protected tree: ResolverTree; + protected tree: SafeResolverTree; constructor(protected readonly id: string, options: Options = {}) { const tree = createTree(this.id); @@ -55,7 +55,7 @@ export class Tree { * * @returns the origin ResolverNode */ - public render(): ResolverTree { + public render(): SafeResolverTree { return this.tree; } @@ -73,7 +73,7 @@ export class Tree { * * @param relatedEventsInfo is the related events and pagination information to add to the tree. */ - private addRelatedEvents(relatedEventsInfo: ResolverRelatedEvents | undefined) { + private addRelatedEvents(relatedEventsInfo: SafeResolverRelatedEvents | undefined) { if (!relatedEventsInfo) { return; } @@ -101,7 +101,7 @@ export class Tree { * * @param ancestorInfo is the ancestors and pagination information to add to the tree. */ - private addAncestors(ancestorInfo: ResolverAncestry | undefined) { + private addAncestors(ancestorInfo: SafeResolverAncestry | undefined) { if (!ancestorInfo) { return; } @@ -132,7 +132,7 @@ export class Tree { } } - private addChildren(children: ResolverChildren | undefined) { + private addChildren(children: SafeResolverChildren | undefined) { if (!children) { return; } diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/alerts.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/alerts.ts index 82d844aae8016..bf7ed711b75a5 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/alerts.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/alerts.ts @@ -4,7 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; -import { eventId } from '../../../../plugins/security_solution/common/endpoint/models/event'; +import { + eventIDSafeVersion, + timestampSafeVersion, +} from '../../../../plugins/security_solution/common/endpoint/models/event'; import { ResolverRelatedAlerts } from '../../../../plugins/security_solution/common/endpoint/types'; import { FtrProviderContext } from '../../ftr_provider_context'; import { @@ -69,7 +72,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('should allow alerts to be filtered', async () => { - const filter = `not event.id:"${tree.origin.relatedAlerts[0].event.id}"`; + const filter = `not event.id:"${tree.origin.relatedAlerts[0].event?.id}"`; const { body }: { body: ResolverRelatedAlerts } = await supertest .post(`/api/endpoint/resolver/${tree.origin.id}/alerts`) .set('kbn-xsrf', 'xxx') @@ -84,7 +87,7 @@ export default function ({ getService }: FtrProviderContext) { // should not find the alert that we excluded in the filter expect( body.alerts.find((bodyAlert) => { - return eventId(bodyAlert) === tree.origin.relatedAlerts[0].event.id; + return eventIDSafeVersion(bodyAlert) === tree.origin.relatedAlerts[0].event?.id; }) ).to.not.be.ok(); }); @@ -135,14 +138,16 @@ export default function ({ getService }: FtrProviderContext) { .expect(200); const sortedAsc = [...tree.origin.relatedAlerts].sort((event1, event2) => { // this sorts the events by timestamp in ascending order - const diff = event1['@timestamp'] - event2['@timestamp']; + const diff = (timestampSafeVersion(event1) ?? 0) - (timestampSafeVersion(event2) ?? 0); + const event1ID = eventIDSafeVersion(event1) ?? 0; + const event2ID = eventIDSafeVersion(event2) ?? 0; // if the timestamps are the same, fallback to the event.id sorted in // ascending order if (diff === 0) { - if (event1.event.id < event2.event.id) { + if (event1ID < event2ID) { return -1; } - if (event1.event.id > event2.event.id) { + if (event1ID > event2ID) { return 1; } return 0; @@ -152,7 +157,7 @@ export default function ({ getService }: FtrProviderContext) { expect(body.alerts.length).to.eql(4); for (let i = 0; i < body.alerts.length; i++) { - expect(eventId(body.alerts[i])).to.equal(sortedAsc[i].event.id); + expect(eventIDSafeVersion(body.alerts[i])).to.equal(sortedAsc[i].event?.id); } }); }); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/children.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/children.ts index 2dec3c755a93b..49e24ff67fa77 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/children.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/children.ts @@ -5,14 +5,17 @@ */ import expect from '@kbn/expect'; import { SearchResponse } from 'elasticsearch'; -import { entityId } from '../../../../plugins/security_solution/common/endpoint/models/event'; +import { + entityIDSafeVersion, + timestampSafeVersion, +} from '../../../../plugins/security_solution/common/endpoint/models/event'; import { eventsIndexPattern } from '../../../../plugins/security_solution/common/endpoint/constants'; import { ChildrenPaginationBuilder } from '../../../../plugins/security_solution/server/endpoint/routes/resolver/utils/children_pagination'; import { ChildrenQuery } from '../../../../plugins/security_solution/server/endpoint/routes/resolver/queries/children'; import { - ResolverTree, - ResolverEvent, - ResolverChildren, + SafeResolverTree, + SafeResolverEvent, + SafeResolverChildren, } from '../../../../plugins/security_solution/common/endpoint/types'; import { FtrProviderContext } from '../../ftr_provider_context'; import { @@ -20,6 +23,7 @@ import { EndpointDocGenerator, } from '../../../../plugins/security_solution/common/endpoint/generate_data'; import { InsertedEvents } from '../../services/resolver'; +import { createAncestryArray } from './common'; export default function resolverAPIIntegrationTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -40,20 +44,20 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC // Origin -> infoEvent -> startEvent -> execEvent origin = generator.generateEvent(); infoEvent = generator.generateEvent({ - parentEntityID: origin.process.entity_id, - ancestry: [origin.process.entity_id], + parentEntityID: entityIDSafeVersion(origin), + ancestry: createAncestryArray([origin]), eventType: ['info'], }); startEvent = generator.generateEvent({ - parentEntityID: infoEvent.process.entity_id, - ancestry: [infoEvent.process.entity_id, origin.process.entity_id], + parentEntityID: entityIDSafeVersion(infoEvent), + ancestry: createAncestryArray([infoEvent, origin]), eventType: ['start'], }); execEvent = generator.generateEvent({ - parentEntityID: startEvent.process.entity_id, - ancestry: [startEvent.process.entity_id, infoEvent.process.entity_id], + parentEntityID: entityIDSafeVersion(startEvent), + ancestry: createAncestryArray([startEvent, infoEvent]), eventType: ['change'], }); genData = await resolver.insertEvents([origin, infoEvent, startEvent, execEvent]); @@ -64,13 +68,13 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC }); it('finds all the children of the origin', async () => { - const { body }: { body: ResolverTree } = await supertest - .get(`/api/endpoint/resolver/${origin.process.entity_id}?children=100`) + const { body }: { body: SafeResolverTree } = await supertest + .get(`/api/endpoint/resolver/${origin.process?.entity_id}?children=100`) .expect(200); expect(body.children.childNodes.length).to.be(3); - expect(body.children.childNodes[0].entityID).to.be(infoEvent.process.entity_id); - expect(body.children.childNodes[1].entityID).to.be(startEvent.process.entity_id); - expect(body.children.childNodes[2].entityID).to.be(execEvent.process.entity_id); + expect(body.children.childNodes[0].entityID).to.be(infoEvent.process?.entity_id); + expect(body.children.childNodes[1].entityID).to.be(startEvent.process?.entity_id); + expect(body.children.childNodes[2].entityID).to.be(execEvent.process?.entity_id); }); }); @@ -86,23 +90,23 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC // Origin -> (infoEvent, startEvent, execEvent are all for the same node) origin = generator.generateEvent(); startEvent = generator.generateEvent({ - parentEntityID: origin.process.entity_id, - ancestry: [origin.process.entity_id], + parentEntityID: entityIDSafeVersion(origin), + ancestry: createAncestryArray([origin]), eventType: ['start'], }); infoEvent = generator.generateEvent({ - parentEntityID: origin.process.entity_id, - ancestry: [origin.process.entity_id], - entityID: startEvent.process.entity_id, + parentEntityID: entityIDSafeVersion(origin), + ancestry: createAncestryArray([origin]), + entityID: entityIDSafeVersion(startEvent), eventType: ['info'], }); execEvent = generator.generateEvent({ - parentEntityID: origin.process.entity_id, - ancestry: [origin.process.entity_id], + parentEntityID: entityIDSafeVersion(origin), + ancestry: createAncestryArray([origin]), eventType: ['change'], - entityID: startEvent.process.entity_id, + entityID: entityIDSafeVersion(startEvent), }); genData = await resolver.insertEvents([origin, infoEvent, startEvent, execEvent]); }); @@ -117,12 +121,12 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC eventsIndexPattern ); // [1] here gets the body portion of the array - const [, query] = childrenQuery.buildMSearch(origin.process.entity_id); - const { body } = await es.search>({ body: query }); + const [, query] = childrenQuery.buildMSearch(entityIDSafeVersion(origin) ?? ''); + const { body } = await es.search>({ body: query }); expect(body.hits.hits.length).to.be(1); const event = body.hits.hits[0]._source; - expect(entityId(event)).to.be(startEvent.process.entity_id); + expect(entityIDSafeVersion(event)).to.be(startEvent.process?.entity_id); expect(event.event?.type).to.eql(['start']); }); }); @@ -139,25 +143,25 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC // Origin -> (infoEvent, startEvent, execEvent are all for the same node) origin = generator.generateEvent(); startEvent = generator.generateEvent({ - parentEntityID: origin.process.entity_id, - ancestry: [origin.process.entity_id], + parentEntityID: entityIDSafeVersion(origin), + ancestry: createAncestryArray([origin]), eventType: ['start'], }); infoEvent = generator.generateEvent({ - timestamp: startEvent['@timestamp'] + 100, - parentEntityID: origin.process.entity_id, - ancestry: [origin.process.entity_id], - entityID: startEvent.process.entity_id, + timestamp: (timestampSafeVersion(startEvent) ?? 0) + 100, + parentEntityID: entityIDSafeVersion(origin), + ancestry: createAncestryArray([origin]), + entityID: entityIDSafeVersion(startEvent), eventType: ['info'], }); execEvent = generator.generateEvent({ - timestamp: infoEvent['@timestamp'] + 100, - parentEntityID: origin.process.entity_id, - ancestry: [origin.process.entity_id], + timestamp: (timestampSafeVersion(infoEvent) ?? 0) + 100, + parentEntityID: entityIDSafeVersion(origin), + ancestry: createAncestryArray([origin]), eventType: ['change'], - entityID: startEvent.process.entity_id, + entityID: entityIDSafeVersion(startEvent), }); genData = await resolver.insertEvents([origin, infoEvent, startEvent, execEvent]); }); @@ -167,37 +171,37 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC }); it('retrieves the same node three times', async () => { - let { body }: { body: ResolverChildren } = await supertest - .get(`/api/endpoint/resolver/${origin.process.entity_id}/children?children=1`) + let { body }: { body: SafeResolverChildren } = await supertest + .get(`/api/endpoint/resolver/${origin.process?.entity_id}/children?children=1`) .expect(200); expect(body.childNodes.length).to.be(1); expect(body.nextChild).to.not.be(null); - expect(body.childNodes[0].entityID).to.be(startEvent.process.entity_id); - expect(body.childNodes[0].lifecycle[0].event?.type).to.eql(startEvent.event.type); + expect(body.childNodes[0].entityID).to.be(startEvent.process?.entity_id); + expect(body.childNodes[0].lifecycle[0].event?.type).to.eql(startEvent.event?.type); ({ body } = await supertest .get( - `/api/endpoint/resolver/${origin.process.entity_id}/children?children=1&afterChild=${body.nextChild}` + `/api/endpoint/resolver/${origin.process?.entity_id}/children?children=1&afterChild=${body.nextChild}` ) .expect(200)); expect(body.childNodes.length).to.be(1); expect(body.nextChild).to.not.be(null); - expect(body.childNodes[0].entityID).to.be(infoEvent.process.entity_id); - expect(body.childNodes[0].lifecycle[1].event?.type).to.eql(infoEvent.event.type); + expect(body.childNodes[0].entityID).to.be(infoEvent.process?.entity_id); + expect(body.childNodes[0].lifecycle[1].event?.type).to.eql(infoEvent.event?.type); ({ body } = await supertest .get( - `/api/endpoint/resolver/${origin.process.entity_id}/children?children=1&afterChild=${body.nextChild}` + `/api/endpoint/resolver/${origin.process?.entity_id}/children?children=1&afterChild=${body.nextChild}` ) .expect(200)); expect(body.childNodes.length).to.be(1); expect(body.nextChild).to.not.be(null); - expect(body.childNodes[0].entityID).to.be(infoEvent.process.entity_id); - expect(body.childNodes[0].lifecycle[2].event?.type).to.eql(execEvent.event.type); + expect(body.childNodes[0].entityID).to.be(infoEvent.process?.entity_id); + expect(body.childNodes[0].lifecycle[2].event?.type).to.eql(execEvent.event?.type); ({ body } = await supertest .get( - `/api/endpoint/resolver/${origin.process.entity_id}/children?children=1&afterChild=${body.nextChild}` + `/api/endpoint/resolver/${origin.process?.entity_id}/children?children=1&afterChild=${body.nextChild}` ) .expect(200)); expect(body.childNodes.length).to.be(0); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/common.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/common.ts index 92d14fb94a2d8..2c59863099ae7 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/common.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/common.ts @@ -6,14 +6,15 @@ import _ from 'lodash'; import expect from '@kbn/expect'; import { - ResolverChildNode, - ResolverLifecycleNode, - ResolverEvent, + SafeResolverChildNode, + SafeResolverLifecycleNode, + SafeResolverEvent, ResolverNodeStats, } from '../../../../plugins/security_solution/common/endpoint/types'; import { - parentEntityId, - eventId, + parentEntityIDSafeVersion, + entityIDSafeVersion, + eventIDSafeVersion, } from '../../../../plugins/security_solution/common/endpoint/models/event'; import { Event, @@ -23,13 +24,33 @@ import { categoryMapping, } from '../../../../plugins/security_solution/common/endpoint/generate_data'; +/** + * Creates the ancestry array based on an array of events. The order of the ancestry array will match the order + * of the events passed in. + * + * @param events an array of generated events + */ +export const createAncestryArray = (events: Event[]) => { + const ancestry: string[] = []; + for (const event of events) { + const entityID = entityIDSafeVersion(event); + if (entityID) { + ancestry.push(entityID); + } + } + return ancestry; +}; + /** * Check that the given lifecycle is in the resolver tree's corresponding map * * @param node a lifecycle node containing the start and end events for a node * @param nodeMap a map of entity_ids to nodes to look for the passed in `node` */ -const expectLifecycleNodeInMap = (node: ResolverLifecycleNode, nodeMap: Map) => { +const expectLifecycleNodeInMap = ( + node: SafeResolverLifecycleNode, + nodeMap: Map +) => { const genNode = nodeMap.get(node.entityID); expect(genNode).to.be.ok(); compareArrays(genNode!.lifecycle, node.lifecycle, true); @@ -44,7 +65,7 @@ const expectLifecycleNodeInMap = (node: ResolverLifecycleNode, nodeMap: Map { @@ -52,7 +73,7 @@ export const verifyAncestry = ( const groupedAncestors = _.groupBy(ancestors, (ancestor) => ancestor.entityID); // group by parent entity_id const groupedAncestorsParent = _.groupBy(ancestors, (ancestor) => - parentEntityId(ancestor.lifecycle[0]) + parentEntityIDSafeVersion(ancestor.lifecycle[0]) ); // make sure there aren't any nodes with the same entity_id expect(Object.keys(groupedAncestors).length).to.eql(ancestors.length); @@ -69,7 +90,7 @@ export const verifyAncestry = ( let foundParents = 0; let node = ancestors[0]; for (let i = 0; i < ancestors.length; i++) { - const parentID = parentEntityId(node.lifecycle[0]); + const parentID = parentEntityIDSafeVersion(node.lifecycle[0]); if (parentID !== undefined) { const nextNode = groupedAncestors[parentID]; if (!nextNode) { @@ -95,12 +116,12 @@ export const verifyAncestry = ( * * @param ancestors an array of ancestor nodes */ -export const retrieveDistantAncestor = (ancestors: ResolverLifecycleNode[]) => { +export const retrieveDistantAncestor = (ancestors: SafeResolverLifecycleNode[]) => { // group the ancestors by their entity_id mapped to a lifecycle node const groupedAncestors = _.groupBy(ancestors, (ancestor) => ancestor.entityID); let node = ancestors[0]; for (let i = 0; i < ancestors.length; i++) { - const parentID = parentEntityId(node.lifecycle[0]); + const parentID = parentEntityIDSafeVersion(node.lifecycle[0]); if (parentID !== undefined) { const nextNode = groupedAncestors[parentID]; if (nextNode) { @@ -122,7 +143,7 @@ export const retrieveDistantAncestor = (ancestors: ResolverLifecycleNode[]) => { * @param childrenPerParent an optional number to compare that there are a certain number of children for each parent */ export const verifyChildren = ( - children: ResolverChildNode[], + children: SafeResolverChildNode[], tree: Tree, numberOfParents?: number, childrenPerParent?: number @@ -132,7 +153,9 @@ export const verifyChildren = ( // make sure each child is unique expect(Object.keys(groupedChildren).length).to.eql(children.length); if (numberOfParents !== undefined) { - const groupParent = _.groupBy(children, (child) => parentEntityId(child.lifecycle[0])); + const groupParent = _.groupBy(children, (child) => + parentEntityIDSafeVersion(child.lifecycle[0]) + ); expect(Object.keys(groupParent).length).to.eql(numberOfParents); if (childrenPerParent !== undefined) { Object.values(groupParent).forEach((childNodes) => @@ -155,7 +178,7 @@ export const verifyChildren = ( */ export const compareArrays = ( expected: Event[], - toTest: ResolverEvent[], + toTest: SafeResolverEvent[], lengthCheck: boolean = false ) => { if (lengthCheck) { @@ -168,7 +191,7 @@ export const compareArrays = ( // we're only checking that the event ids are the same here. The reason we can't check the entire document // is because ingest pipelines are used to add fields to the document when it is received by elasticsearch, // therefore it will not be the same as the document created by the generator - return eventId(toTestEvent) === eventId(arrEvent); + return eventIDSafeVersion(toTestEvent) === eventIDSafeVersion(arrEvent); }) ).to.be.ok(); }); @@ -212,7 +235,7 @@ export const verifyStats = ( * @param categories the related event info used when generating the resolver tree */ export const verifyLifecycleStats = ( - nodes: ResolverLifecycleNode[], + nodes: SafeResolverLifecycleNode[], categories: RelatedEventInfo[], relatedAlerts: number ) => { diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts index cb6c49e17c712..e6d5e8fccd00d 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; +import { entityIDSafeVersion } from '../../../../plugins/security_solution/common/endpoint/models/event'; import { eventsIndexPattern } from '../../../../plugins/security_solution/common/endpoint/constants'; import { - ResolverTree, + SafeResolverTree, ResolverEntityIndex, } from '../../../../plugins/security_solution/common/endpoint/types'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -15,19 +16,26 @@ import { Event, } from '../../../../plugins/security_solution/common/endpoint/generate_data'; import { InsertedEvents } from '../../services/resolver'; +import { createAncestryArray } from './common'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const resolver = getService('resolverGenerator'); const generator = new EndpointDocGenerator('resolver'); + const setEntityIDEmptyString = (event: Event) => { + if (event.process?.entity_id) { + event.process.entity_id = ''; + } + }; + describe('Resolver handling of entity ids', () => { describe('entity api', () => { let origin: Event; let genData: InsertedEvents; before(async () => { origin = generator.generateEvent({ parentEntityID: 'a' }); - origin.process.entity_id = ''; + setEntityIDEmptyString(origin); genData = await resolver.insertEvents([origin]); }); @@ -57,16 +65,16 @@ export default function ({ getService }: FtrProviderContext) { // should not be returned by the backend. origin = generator.generateEvent({ entityID: 'a' }); childNoEntityID = generator.generateEvent({ - parentEntityID: origin.process.entity_id, - ancestry: [origin.process.entity_id], + parentEntityID: entityIDSafeVersion(origin), + ancestry: createAncestryArray([origin]), }); // force it to be empty - childNoEntityID.process.entity_id = ''; + setEntityIDEmptyString(childNoEntityID); childWithEntityID = generator.generateEvent({ entityID: 'b', - parentEntityID: origin.process.entity_id, - ancestry: [origin.process.entity_id], + parentEntityID: entityIDSafeVersion(origin), + ancestry: createAncestryArray([origin]), }); events = [origin, childNoEntityID, childWithEntityID]; genData = await resolver.insertEvents(events); @@ -77,11 +85,11 @@ export default function ({ getService }: FtrProviderContext) { }); it('does not find children without a process entity_id', async () => { - const { body }: { body: ResolverTree } = await supertest - .get(`/api/endpoint/resolver/${origin.process.entity_id}`) + const { body }: { body: SafeResolverTree } = await supertest + .get(`/api/endpoint/resolver/${origin.process?.entity_id}`) .expect(200); expect(body.children.childNodes.length).to.be(1); - expect(body.children.childNodes[0].entityID).to.be(childWithEntityID.process.entity_id); + expect(body.children.childNodes[0].entityID).to.be(childWithEntityID.process?.entity_id); }); }); @@ -101,21 +109,21 @@ export default function ({ getService }: FtrProviderContext) { }); ancestor1 = generator.generateEvent({ entityID: '1', - parentEntityID: ancestor2.process.entity_id, - ancestry: [ancestor2.process.entity_id], + parentEntityID: entityIDSafeVersion(ancestor2), + ancestry: createAncestryArray([ancestor2]), }); // we'll insert an event that doesn't have an entity id so if the backend does search for it, it should be // returned and our test should fail ancestorNoEntityID = generator.generateEvent({ - ancestry: [ancestor2.process.entity_id], + ancestry: createAncestryArray([ancestor2]), }); - ancestorNoEntityID.process.entity_id = ''; + setEntityIDEmptyString(ancestorNoEntityID); origin = generator.generateEvent({ entityID: 'a', - parentEntityID: ancestor1.process.entity_id, - ancestry: ['', ancestor2.process.entity_id], + parentEntityID: entityIDSafeVersion(ancestor1), + ancestry: ['', ...createAncestryArray([ancestor2])], }); events = [origin, ancestor1, ancestor2, ancestorNoEntityID]; @@ -127,11 +135,11 @@ export default function ({ getService }: FtrProviderContext) { }); it('does not query for ancestors that have an empty string for the entity_id', async () => { - const { body }: { body: ResolverTree } = await supertest - .get(`/api/endpoint/resolver/${origin.process.entity_id}`) + const { body }: { body: SafeResolverTree } = await supertest + .get(`/api/endpoint/resolver/${origin.process?.entity_id}`) .expect(200); expect(body.ancestry.ancestors.length).to.be(1); - expect(body.ancestry.ancestors[0].entityID).to.be(ancestor2.process.entity_id); + expect(body.ancestry.ancestors[0].entityID).to.be(ancestor2.process?.entity_id); }); }); }); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/events.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/events.ts index c0e4e466c7b62..4e248f52ec297 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/events.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/events.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; -import { eventId } from '../../../../plugins/security_solution/common/endpoint/models/event'; -import { ResolverRelatedEvents } from '../../../../plugins/security_solution/common/endpoint/types'; +import { eventIDSafeVersion } from '../../../../plugins/security_solution/common/endpoint/models/event'; +import { SafeResolverRelatedEvents } from '../../../../plugins/security_solution/common/endpoint/types'; import { FtrProviderContext } from '../../ftr_provider_context'; import { Tree, @@ -59,7 +59,7 @@ export default function ({ getService }: FtrProviderContext) { const cursor = 'eyJ0aW1lc3RhbXAiOjE1ODE0NTYyNTUwMDAsImV2ZW50SUQiOiI5NDA0MyJ9'; it('should return details for the root node', async () => { - const { body }: { body: ResolverRelatedEvents } = await supertest + const { body }: { body: SafeResolverRelatedEvents } = await supertest .post(`/api/endpoint/resolver/${entityID}/events?legacyEndpointID=${endpointID}`) .set('kbn-xsrf', 'xxx') .expect(200); @@ -69,7 +69,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('returns no values when there is no more data', async () => { - const { body }: { body: ResolverRelatedEvents } = await supertest + const { body }: { body: SafeResolverRelatedEvents } = await supertest // after is set to the document id of the last event so there shouldn't be any more after it .post( `/api/endpoint/resolver/${entityID}/events?legacyEndpointID=${endpointID}&afterEvent=${cursor}` @@ -82,7 +82,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('should return the first page of information when the cursor is invalid', async () => { - const { body }: { body: ResolverRelatedEvents } = await supertest + const { body }: { body: SafeResolverRelatedEvents } = await supertest .post( `/api/endpoint/resolver/${entityID}/events?legacyEndpointID=${endpointID}&afterEvent=blah` ) @@ -93,7 +93,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('should return no results for an invalid endpoint ID', async () => { - const { body }: { body: ResolverRelatedEvents } = await supertest + const { body }: { body: SafeResolverRelatedEvents } = await supertest .post(`/api/endpoint/resolver/${entityID}/events?legacyEndpointID=foo`) .set('kbn-xsrf', 'xxx') .expect(200); @@ -120,7 +120,7 @@ export default function ({ getService }: FtrProviderContext) { describe('endpoint events', () => { it('should not find any events', async () => { - const { body }: { body: ResolverRelatedEvents } = await supertest + const { body }: { body: SafeResolverRelatedEvents } = await supertest .post(`/api/endpoint/resolver/5555/events`) .set('kbn-xsrf', 'xxx') .expect(200); @@ -129,7 +129,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('should return details for the root node', async () => { - const { body }: { body: ResolverRelatedEvents } = await supertest + const { body }: { body: SafeResolverRelatedEvents } = await supertest .post(`/api/endpoint/resolver/${tree.origin.id}/events`) .set('kbn-xsrf', 'xxx') .expect(200); @@ -140,7 +140,7 @@ export default function ({ getService }: FtrProviderContext) { it('should allow for the events to be filtered', async () => { const filter = `event.category:"${RelatedEventCategory.Driver}"`; - const { body }: { body: ResolverRelatedEvents } = await supertest + const { body }: { body: SafeResolverRelatedEvents } = await supertest .post(`/api/endpoint/resolver/${tree.origin.id}/events`) .set('kbn-xsrf', 'xxx') .send({ @@ -156,7 +156,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('should return paginated results for the root node', async () => { - let { body }: { body: ResolverRelatedEvents } = await supertest + let { body }: { body: SafeResolverRelatedEvents } = await supertest .post(`/api/endpoint/resolver/${tree.origin.id}/events?events=2`) .set('kbn-xsrf', 'xxx') .expect(200); @@ -185,7 +185,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('should return the first page of information when the cursor is invalid', async () => { - const { body }: { body: ResolverRelatedEvents } = await supertest + const { body }: { body: SafeResolverRelatedEvents } = await supertest .post(`/api/endpoint/resolver/${tree.origin.id}/events?afterEvent=blah`) .set('kbn-xsrf', 'xxx') .expect(200); @@ -195,7 +195,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('should sort the events in descending order', async () => { - const { body }: { body: ResolverRelatedEvents } = await supertest + const { body }: { body: SafeResolverRelatedEvents } = await supertest .post(`/api/endpoint/resolver/${tree.origin.id}/events`) .set('kbn-xsrf', 'xxx') .expect(200); @@ -204,8 +204,8 @@ export default function ({ getService }: FtrProviderContext) { // the last element in the array so let's reverse it const relatedEvents = tree.origin.relatedEvents.reverse(); for (let i = 0; i < body.events.length; i++) { - expect(body.events[i].event?.category).to.equal(relatedEvents[i].event.category); - expect(eventId(body.events[i])).to.equal(relatedEvents[i].event.id); + expect(body.events[i].event?.category).to.equal(relatedEvents[i].event?.category); + expect(eventIDSafeVersion(body.events[i])).to.equal(relatedEvents[i].event?.id); } }); }); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts index 957d559087f5e..837af6a940f5c 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts @@ -5,12 +5,12 @@ */ import expect from '@kbn/expect'; import { - ResolverAncestry, - ResolverChildren, - ResolverTree, - LegacyEndpointEvent, + SafeResolverAncestry, + SafeResolverChildren, + SafeResolverTree, + SafeLegacyEndpointEvent, } from '../../../../plugins/security_solution/common/endpoint/types'; -import { parentEntityId } from '../../../../plugins/security_solution/common/endpoint/models/event'; +import { parentEntityIDSafeVersion } from '../../../../plugins/security_solution/common/endpoint/models/event'; import { FtrProviderContext } from '../../ftr_provider_context'; import { Tree, @@ -71,7 +71,7 @@ export default function ({ getService }: FtrProviderContext) { const entityID = '94042'; it('should return details for the root node', async () => { - const { body }: { body: ResolverAncestry } = await supertest + const { body }: { body: SafeResolverAncestry } = await supertest .get( `/api/endpoint/resolver/${entityID}/ancestry?legacyEndpointID=${endpointID}&ancestors=5` ) @@ -82,7 +82,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('should have a populated next parameter', async () => { - const { body }: { body: ResolverAncestry } = await supertest + const { body }: { body: SafeResolverAncestry } = await supertest .get( `/api/endpoint/resolver/${entityID}/ancestry?legacyEndpointID=${endpointID}&ancestors=0` ) @@ -91,7 +91,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('should handle an ancestors param request', async () => { - let { body }: { body: ResolverAncestry } = await supertest + let { body }: { body: SafeResolverAncestry } = await supertest .get( `/api/endpoint/resolver/${entityID}/ancestry?legacyEndpointID=${endpointID}&ancestors=0` ) @@ -110,14 +110,14 @@ export default function ({ getService }: FtrProviderContext) { describe('endpoint events', () => { it('should return the origin node at the front of the array', async () => { - const { body }: { body: ResolverAncestry } = await supertest + const { body }: { body: SafeResolverAncestry } = await supertest .get(`/api/endpoint/resolver/${tree.origin.id}/ancestry?ancestors=9`) .expect(200); expect(body.ancestors[0].entityID).to.eql(tree.origin.id); }); it('should return details for the root node', async () => { - const { body }: { body: ResolverAncestry } = await supertest + const { body }: { body: SafeResolverAncestry } = await supertest .get(`/api/endpoint/resolver/${tree.origin.id}/ancestry?ancestors=9`) .expect(200); // the tree we generated had 5 ancestors + 1 origin node @@ -128,7 +128,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('should handle an invalid id', async () => { - const { body }: { body: ResolverAncestry } = await supertest + const { body }: { body: SafeResolverAncestry } = await supertest .get(`/api/endpoint/resolver/alskdjflasj/ancestry`) .expect(200); expect(body.ancestors).to.be.empty(); @@ -136,18 +136,20 @@ export default function ({ getService }: FtrProviderContext) { }); it('should have a populated next parameter', async () => { - const { body }: { body: ResolverAncestry } = await supertest + const { body }: { body: SafeResolverAncestry } = await supertest .get(`/api/endpoint/resolver/${tree.origin.id}/ancestry?ancestors=2`) .expect(200); // it should have 2 ancestors + 1 origin expect(body.ancestors.length).to.eql(3); verifyAncestry(body.ancestors, tree, false); const distantGrandparent = retrieveDistantAncestor(body.ancestors); - expect(body.nextAncestor).to.eql(parentEntityId(distantGrandparent.lifecycle[0])); + expect(body.nextAncestor).to.eql( + parentEntityIDSafeVersion(distantGrandparent.lifecycle[0]) + ); }); it('should handle multiple ancestor requests', async () => { - let { body }: { body: ResolverAncestry } = await supertest + let { body }: { body: SafeResolverAncestry } = await supertest .get(`/api/endpoint/resolver/${tree.origin.id}/ancestry?ancestors=3`) .expect(200); expect(body.ancestors.length).to.eql(4); @@ -171,7 +173,7 @@ export default function ({ getService }: FtrProviderContext) { const entityID = '94041'; it('returns child process lifecycle events', async () => { - const { body }: { body: ResolverChildren } = await supertest + const { body }: { body: SafeResolverChildren } = await supertest .get(`/api/endpoint/resolver/${entityID}/children?legacyEndpointID=${endpointID}`) .expect(200); expect(body.childNodes.length).to.eql(1); @@ -179,12 +181,12 @@ export default function ({ getService }: FtrProviderContext) { expect( // for some reason the ts server doesn't think `endgame` exists even though we're using ResolverEvent // here, so to avoid it complaining we'll just force it - (body.childNodes[0].lifecycle[0] as LegacyEndpointEvent).endgame.unique_pid + (body.childNodes[0].lifecycle[0] as SafeLegacyEndpointEvent).endgame.unique_pid ).to.eql(94042); }); it('returns multiple levels of child process lifecycle events', async () => { - const { body }: { body: ResolverChildren } = await supertest + const { body }: { body: SafeResolverChildren } = await supertest .get(`/api/endpoint/resolver/93802/children?legacyEndpointID=${endpointID}&children=10`) .expect(200); expect(body.childNodes.length).to.eql(10); @@ -193,12 +195,12 @@ export default function ({ getService }: FtrProviderContext) { expect( // for some reason the ts server doesn't think `endgame` exists even though we're using ResolverEvent // here, so to avoid it complaining we'll just force it - (body.childNodes[0].lifecycle[0] as LegacyEndpointEvent).endgame.unique_pid + (body.childNodes[0].lifecycle[0] as SafeLegacyEndpointEvent).endgame.unique_pid ).to.eql(93932); }); it('returns no values when there is no more data', async () => { - let { body }: { body: ResolverChildren } = await supertest + let { body }: { body: SafeResolverChildren } = await supertest .get( // there should only be a single child for this node `/api/endpoint/resolver/94041/children?legacyEndpointID=${endpointID}&children=1` @@ -216,7 +218,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('returns the first page of information when the cursor is invalid', async () => { - const { body }: { body: ResolverChildren } = await supertest + const { body }: { body: SafeResolverChildren } = await supertest .get( `/api/endpoint/resolver/${entityID}/children?legacyEndpointID=${endpointID}&afterChild=blah` ) @@ -236,7 +238,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('returns empty events without a matching entity id', async () => { - const { body }: { body: ResolverChildren } = await supertest + const { body }: { body: SafeResolverChildren } = await supertest .get(`/api/endpoint/resolver/5555/children`) .expect(200); expect(body.nextChild).to.eql(null); @@ -244,7 +246,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('returns empty events with an invalid endpoint id', async () => { - const { body }: { body: ResolverChildren } = await supertest + const { body }: { body: SafeResolverChildren } = await supertest .get(`/api/endpoint/resolver/${entityID}/children?legacyEndpointID=foo`) .expect(200); expect(body.nextChild).to.eql(null); @@ -254,7 +256,7 @@ export default function ({ getService }: FtrProviderContext) { describe('endpoint events', () => { it('returns all children for the origin', async () => { - const { body }: { body: ResolverChildren } = await supertest + const { body }: { body: SafeResolverChildren } = await supertest .get(`/api/endpoint/resolver/${tree.origin.id}/children?children=100`) .expect(200); // there are 2 levels in the children part of the tree and 3 nodes for each = @@ -269,7 +271,7 @@ export default function ({ getService }: FtrProviderContext) { // this gets a node should have 3 children which were created in succession so that the timestamps // are ordered correctly to be retrieved in a single call const distantChildEntityID = Array.from(tree.childrenLevels[0].values())[0].id; - const { body }: { body: ResolverChildren } = await supertest + const { body }: { body: SafeResolverChildren } = await supertest .get(`/api/endpoint/resolver/${distantChildEntityID}/children?children=3`) .expect(200); expect(body.childNodes.length).to.eql(3); @@ -281,7 +283,7 @@ export default function ({ getService }: FtrProviderContext) { // this gets a node should have 3 children which were created in succession so that the timestamps // are ordered correctly to be retrieved in a single call const distantChildEntityID = Array.from(tree.childrenLevels[0].values())[0].id; - let { body }: { body: ResolverChildren } = await supertest + let { body }: { body: SafeResolverChildren } = await supertest .get(`/api/endpoint/resolver/${distantChildEntityID}/children?children=1`) .expect(200); expect(body.childNodes.length).to.eql(1); @@ -308,7 +310,7 @@ export default function ({ getService }: FtrProviderContext) { it('gets all children in two queries', async () => { // should get all the children of the origin - let { body }: { body: ResolverChildren } = await supertest + let { body }: { body: SafeResolverChildren } = await supertest .get(`/api/endpoint/resolver/${tree.origin.id}/children?children=3`) .expect(200); expect(body.childNodes.length).to.eql(3); @@ -334,7 +336,7 @@ export default function ({ getService }: FtrProviderContext) { const endpointID = '5a0c957f-b8e7-4538-965e-57e8bb86ad3a'; it('returns ancestors, events, children, and current process lifecycle', async () => { - const { body }: { body: ResolverTree } = await supertest + const { body }: { body: SafeResolverTree } = await supertest .get(`/api/endpoint/resolver/93933?legacyEndpointID=${endpointID}`) .expect(200); expect(body.ancestry.nextAncestor).to.equal(null); @@ -348,7 +350,7 @@ export default function ({ getService }: FtrProviderContext) { describe('endpoint events', () => { it('returns a tree', async () => { - const { body }: { body: ResolverTree } = await supertest + const { body }: { body: SafeResolverTree } = await supertest .get( `/api/endpoint/resolver/${tree.origin.id}?children=100&ancestors=5&events=5&alerts=5` ) diff --git a/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts b/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts index 7e4d4177affac..c5855281f55c9 100644 --- a/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts +++ b/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts @@ -9,6 +9,7 @@ import { EndpointDocGenerator, Event, } from '../../../plugins/security_solution/common/endpoint/generate_data'; +import { firstNonNullValue } from '../../../plugins/security_solution/common/endpoint/models/ecs_safety_helpers'; import { FtrProviderContext } from '../ftr_provider_context'; export const processEventsIndex = 'logs-endpoint.events.process-default'; @@ -87,7 +88,7 @@ export function ResolverGeneratorProvider({ getService }: FtrProviderContext) { const tree = generator.generateTree(options); const body = tree.allEvents.reduce((array: Array, doc) => { let index = eventsIndex; - if (doc.event.kind === 'alert') { + if (firstNonNullValue(doc.event?.kind) === 'alert') { index = alertsIndex; } /**