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 f8a6807196557..f53da8fb1f096 100644 --- a/x-pack/plugins/security_solution/common/endpoint/models/event.ts +++ b/x-pack/plugins/security_solution/common/endpoint/models/event.ts @@ -32,9 +32,9 @@ export function eventName(event: ResolverEvent): string { } } -export function eventId(event: ResolverEvent): string { +export function eventId(event: ResolverEvent): number | undefined | string { if (isLegacyEvent(event)) { - return event.endgame.serial_event_id ? String(event.endgame.serial_event_id) : ''; + return event.endgame.serial_event_id; } return event.event.id; } diff --git a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/index.ts b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/index.ts index 22f77cbc513f5..35a32d91d8a02 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/index.ts +++ b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { uniquePidForProcess, uniqueParentPidForProcess, datetime } from '../process_event'; +import { uniquePidForProcess, uniqueParentPidForProcess, orderByTime } from '../process_event'; import { IndexedProcessTree } from '../../types'; import { ResolverEvent } from '../../../../common/endpoint/types'; import { levelOrder as baseLevelOrder } from '../../lib/tree_sequencers'; @@ -38,18 +38,7 @@ export function factory( // sort the children of each node for (const siblings of idToChildren.values()) { - siblings.sort(function (firstEvent, secondEvent) { - const first: number = datetime(firstEvent); - const second: number = datetime(secondEvent); - - // if either value is NaN, compare them differently - if (isNaN(first)) { - // treat NaN as 1 and other values as 0, causing NaNs to have the highest value - return 1 - (isNaN(second) ? 1 : 0); - } else { - return first - second; - } - }); + siblings.sort(orderByTime); } return { diff --git a/x-pack/plugins/security_solution/public/resolver/models/process_event.test.ts b/x-pack/plugins/security_solution/public/resolver/models/process_event.test.ts index 04a3f9ccff772..7eb692851bc9b 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/process_event.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/models/process_event.test.ts @@ -3,10 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { eventType } from './process_event'; +import { eventType, orderByTime } from './process_event'; import { mockProcessEvent } from './process_event_test_helpers'; -import { LegacyEndpointEvent } from '../../../common/endpoint/types'; +import { LegacyEndpointEvent, ResolverEvent } from '../../../common/endpoint/types'; describe('process event', () => { describe('eventType', () => { @@ -24,4 +24,86 @@ describe('process event', () => { expect(eventType(event)).toEqual('processCreated'); }); }); + describe('orderByTime', () => { + let mock: (time: number, eventID: string) => ResolverEvent; + let events: ResolverEvent[]; + beforeEach(() => { + mock = (time, eventID) => { + return { + '@timestamp': time, + event: { + id: eventID, + }, + } as ResolverEvent; + }; + // 2 events each for numbers -1, 0, 1, and NaN + // each event has a unique id, a through h + // order is arbitrary + events = [ + mock(-1, 'a'), + mock(0, 'c'), + mock(1, 'e'), + mock(NaN, 'g'), + mock(-1, 'b'), + mock(0, 'd'), + mock(1, 'f'), + mock(NaN, 'h'), + ]; + }); + it('sorts events as expected', () => { + events.sort(orderByTime); + expect(events).toMatchInlineSnapshot(` + Array [ + Object { + "@timestamp": -1, + "event": Object { + "id": "a", + }, + }, + Object { + "@timestamp": -1, + "event": Object { + "id": "b", + }, + }, + Object { + "@timestamp": 0, + "event": Object { + "id": "c", + }, + }, + Object { + "@timestamp": 0, + "event": Object { + "id": "d", + }, + }, + Object { + "@timestamp": 1, + "event": Object { + "id": "e", + }, + }, + Object { + "@timestamp": 1, + "event": Object { + "id": "f", + }, + }, + Object { + "@timestamp": NaN, + "event": Object { + "id": "g", + }, + }, + Object { + "@timestamp": NaN, + "event": Object { + "id": "h", + }, + }, + ] + `); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/resolver/models/process_event.ts b/x-pack/plugins/security_solution/public/resolver/models/process_event.ts index 9e0911b532349..4f8df87b3ac0b 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/process_event.ts +++ b/x-pack/plugins/security_solution/public/resolver/models/process_event.ts @@ -32,10 +32,13 @@ export function isTerminatedProcess(passedEvent: ResolverEvent) { * ms since unix epoc, based on timestamp. * may return NaN if the timestamp wasn't present or was invalid. */ -export function datetime(passedEvent: ResolverEvent): number { +export function datetime(passedEvent: ResolverEvent): number | null { const timestamp = event.eventTimestamp(passedEvent); - return timestamp === undefined ? 0 : new Date(timestamp).getTime(); + const time = timestamp === undefined ? 0 : new Date(timestamp).getTime(); + + // if the date could not be parsed, return null + return isNaN(time) ? null : time; } /** @@ -171,3 +174,22 @@ export function argsForProcess(passedEvent: ResolverEvent): string | undefined { } return passedEvent?.process?.args; } + +/** + * used to sort events + */ +export function orderByTime(first: ResolverEvent, second: ResolverEvent): number { + const firstDatetime: number | null = datetime(first); + const secondDatetime: number | null = datetime(second); + + if (firstDatetime === secondDatetime) { + // break ties using an arbitrary (stable) comparison of `eventId` (which should be unique) + return String(event.eventId(first)).localeCompare(String(event.eventId(second))); + } else if (firstDatetime === null || secondDatetime === null) { + // sort `null`'s as higher than numbers + return (firstDatetime === null ? 1 : 0) - (secondDatetime === null ? 1 : 0); + } else { + // sort in ascending order. + return firstDatetime - secondDatetime; + } +} diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_related_list.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_related_list.tsx index 591432e1f9f9f..bd5d720ae4807 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_related_list.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_related_list.tsx @@ -214,9 +214,9 @@ export const ProcessEventListNarrowedByType = memo(function ProcessEventListNarr eventCategory: `${eventType}`, eventType: `${event.ecsEventType(resolverEvent)}`, name: event.descriptiveName(resolverEvent), - entityId, + entityId: String(entityId), setQueryParams: () => { - pushToQueryParams({ crumbId: entityId, crumbEvent: processEntityId }); + pushToQueryParams({ crumbId: String(entityId), crumbEvent: processEntityId }); }, }; }); 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 2b107ab1b6db4..50deaab2561db 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 @@ -61,7 +61,7 @@ export class PaginationBuilder { const lastResult = results[results.length - 1]; const cursor = { timestamp: lastResult['@timestamp'], - eventID: eventId(lastResult), + eventID: String(eventId(lastResult)), }; return PaginationBuilder.urlEncodeCursor(cursor); }