Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution] Refactor resolver children _source #77343

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 156 additions & 20 deletions x-pack/plugins/security_solution/common/endpoint/models/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,24 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
LegacyEndpointEvent,
ResolverEvent,
SafeResolverEvent,
SafeLegacyEndpointEvent,
} from '../types';
import { LegacyEndpointEvent, ResolverEvent, SafeResolverEvent, ECSField } from '../types';
import { firstNonNullValue, hasValue, values } from './ecs_safety_helpers';

/**
* Legacy events will define the `endgame` object. This is used to narrow a ResolverEvent.
*/
interface LegacyEvent {
endgame?: object;
}

/*
* Determine if a `ResolverEvent` is the legacy variety. Can be used to narrow `ResolverEvent` to `LegacyEndpointEvent`.
* Determine if a higher level event type is the legacy variety. Can be used to narrow an event type.
* T optionally defines an `endgame` object field used for determining the type of event. If T doesn't contain the
* `endgame` field it will serve as the narrowed type.
*/
export function isLegacyEventSafeVersion(
event: SafeResolverEvent
): event is SafeLegacyEndpointEvent {
export function isLegacyEventSafeVersion<T extends LegacyEvent>(
event: LegacyEvent | {}
): event is T {
return 'endgame' in event && event.endgame !== undefined;
}

Expand All @@ -27,7 +31,30 @@ export function isLegacyEvent(event: ResolverEvent): event is LegacyEndpointEven
return (event as LegacyEndpointEvent).endgame !== undefined;
}

export function isProcessRunning(event: SafeResolverEvent): boolean {
/**
* Minimum fields needed from the `SafeResolverEvent` type for the function below to operate correctly.
*/
type ProcessRunningFields = Partial<
| {
endgame: object;
event: Partial<{
type: ECSField<string>;
action: ECSField<string>;
}>;
}
| {
event: Partial<{
Copy link
Contributor

Choose a reason for hiding this comment

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

❔ What does this second part of the union do? Doesn't the first part being marked 'partial' make this redundant?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The first Partial marks the fields like endgame and event as optional but not the fields inside those fields (it's not recursive). This line marks type as optional and any future fields added to event.

type: ECSField<string>;
}>;
}
>;

/**
* Checks if an event describes a process as running (whether it was started, already running, or changed)
*
* @param event a document to check for running fields
*/
export function isProcessRunning(event: ProcessRunningFields): boolean {
if (isLegacyEventSafeVersion(event)) {
return (
hasValue(event.event?.type, 'process_start') ||
Expand All @@ -43,15 +70,26 @@ export function isProcessRunning(event: SafeResolverEvent): boolean {
);
}

export function timestampSafeVersion(event: SafeResolverEvent): undefined | number {
/**
* Minimum fields needed from the `SafeResolverEvent` type for the function below to operate correctly.
*/
type TimestampFields = Pick<SafeResolverEvent, '@timestamp'>;

/**
* Extracts the first non null value from the `@timestamp` field in the document. Returns undefined if the field doesn't
* exist in the document.
*
* @param event a document from ES
*/
export function timestampSafeVersion(event: TimestampFields): undefined | number {
Copy link
Contributor

Choose a reason for hiding this comment

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

🔴 doc comments on exports

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added comment

return firstNonNullValue(event?.['@timestamp']);
}

/**
* The `@timestamp` for the event, as a `Date` object.
* If `@timestamp` couldn't be parsed as a `Date`, returns `undefined`.
*/
export function timestampAsDateSafeVersion(event: SafeResolverEvent): Date | undefined {
export function timestampAsDateSafeVersion(event: TimestampFields): Date | undefined {
const value = timestampSafeVersion(event);
if (value === undefined) {
return undefined;
Expand Down Expand Up @@ -93,9 +131,30 @@ export function eventId(event: ResolverEvent): number | undefined | string {
return event.event.id;
}

export function eventSequence(event: SafeResolverEvent): number | undefined {
/**
* Minimum fields needed from the `SafeResolverEvent` type for the function below to operate correctly.
*/
type EventSequenceFields = Partial<
| {
endgame: Partial<{
serial_event_id: ECSField<number>;
}>;
}
| {
event: Partial<{
sequence: ECSField<number>;
}>;
}
>;

/**
* Extract the first non null event sequence value from a document. Returns undefined if the field doesn't exist in the document.
*
* @param event a document from ES
*/
export function eventSequence(event: EventSequenceFields): number | undefined {
if (isLegacyEventSafeVersion(event)) {
return firstNonNullValue(event.endgame.serial_event_id);
return firstNonNullValue(event.endgame?.serial_event_id);
}
return firstNonNullValue(event.event?.sequence);
}
Expand All @@ -113,7 +172,29 @@ export function entityId(event: ResolverEvent): string {
return event.process.entity_id;
}

export function entityIDSafeVersion(event: SafeResolverEvent): string | undefined {
/**
* Minimum fields needed from the `SafeResolverEvent` type for the function below to operate correctly.
*/
type EntityIDFields = Partial<
| {
endgame: Partial<{
unique_pid: ECSField<number>;
}>;
}
| {
process: Partial<{
entity_id: ECSField<string>;
}>;
}
>;

/**
* Extract the first non null value from either the `entity_id` or `unique_pid` depending on the document type. Returns
* undefined if the field doesn't exist in the document.
*
* @param event a document from ES
*/
export function entityIDSafeVersion(event: EntityIDFields): string | undefined {
if (isLegacyEventSafeVersion(event)) {
return event.endgame?.unique_pid === undefined
? undefined
Expand All @@ -130,14 +211,59 @@ export function parentEntityId(event: ResolverEvent): string | undefined {
return event.process.parent?.entity_id;
}

export function parentEntityIDSafeVersion(event: SafeResolverEvent): string | undefined {
/**
* Minimum fields needed from the `SafeResolverEvent` type for the function below to operate correctly.
*/
type ParentEntityIDFields = Partial<
| {
endgame: Partial<{
unique_ppid: ECSField<number>;
}>;
}
| {
process: Partial<{
parent: Partial<{
entity_id: ECSField<string>;
}>;
}>;
}
>;

/**
* Extract the first non null value from either the `parent.entity_id` or `unique_ppid` depending on the document type. Returns
* undefined if the field doesn't exist in the document.
*
* @param event a document from ES
*/
export function parentEntityIDSafeVersion(event: ParentEntityIDFields): string | undefined {
if (isLegacyEventSafeVersion(event)) {
return String(firstNonNullValue(event.endgame.unique_ppid));
return String(firstNonNullValue(event.endgame?.unique_ppid));
}
return firstNonNullValue(event.process?.parent?.entity_id);
}

export function ancestryArray(event: SafeResolverEvent): string[] | undefined {
/**
* Minimum fields needed from the `SafeResolverEvent` type for the function below to operate correctly.
*/
type AncestryArrayFields = Partial<
| {
endgame: object;
}
| {
process: Partial<{
Ext: Partial<{
ancestry: ECSField<string>;
}>;
}>;
}
>;

/**
* Extracts all ancestry array from a document if it exists.
*
* @param event an ES document
*/
export function ancestryArray(event: AncestryArrayFields): string[] | undefined {
if (isLegacyEventSafeVersion(event)) {
return undefined;
}
Expand All @@ -146,7 +272,17 @@ export function ancestryArray(event: SafeResolverEvent): string[] | undefined {
return values(event.process?.Ext?.ancestry);
}

export function getAncestryAsArray(event: SafeResolverEvent | undefined): string[] {
/**
* Minimum fields needed from the `SafeResolverEvent` type for the function below to operate correctly.
*/
type GetAncestryArrayFields = AncestryArrayFields & ParentEntityIDFields;

/**
* Returns an array of strings representing the ancestry for a process.
*
* @param event an ES document
*/
export function getAncestryAsArray(event: GetAncestryArrayFields | undefined): string[] {
Copy link
Contributor

Choose a reason for hiding this comment

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

🔴 doc comments on exports

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added comment

if (!event) {
return [];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,59 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { SearchResponse } from 'elasticsearch';
import { SafeResolverEvent } from '../../../../../common/endpoint/types';
import { ECSField } from '../../../../../common/endpoint/types';
import { ResolverQuery } from './base';
import { ChildrenPaginationBuilder } from '../utils/children_pagination';
import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common';

/**
* This type represents the document returned from ES for a legacy event when using the ChildrenQuery to fetch legacy events.
* It contains only the necessary fields that the children api needs to process the results before
* it requests the full lifecycle information for the children in a later query.
*/
export type LegacyChildEvent = Partial<{
'@timestamp': ECSField<number>;
event: Partial<{
type: ECSField<string>;
action: ECSField<string>;
}>;
endgame: Partial<{
serial_event_id: ECSField<number>;
unique_pid: ECSField<number>;
unique_ppid: ECSField<number>;
}>;
}>;

/**
* This type represents the document returned from ES for an event when using the ChildrenQuery to fetch legacy events.
* It contains only the necessary fields that the children api needs to process the results before
* it requests the full lifecycle information for the children in a later query.
*/
export type EndpointChildEvent = Partial<{
'@timestamp': ECSField<number>;
event: Partial<{
type: ECSField<string>;
sequence: ECSField<number>;
}>;
process: Partial<{
entity_id: ECSField<string>;
parent: Partial<{
entity_id: ECSField<string>;
}>;
Ext: Partial<{
ancestry: ECSField<string>;
}>;
}>;
}>;

export type ChildEvent = EndpointChildEvent | LegacyChildEvent;

/**
* Builds a query for retrieving descendants of a node.
* The first type `ChildEvent[]` represents the final formatted result. The second type `ChildEvent` defines the type
* used in the `SearchResponse<T>` field returned from the ES query.
*/
export class ChildrenQuery extends ResolverQuery<SafeResolverEvent[]> {
export class ChildrenQuery extends ResolverQuery<ChildEvent[], ChildEvent> {
constructor(
private readonly pagination: ChildrenPaginationBuilder,
indexPattern: string | string[],
Expand All @@ -24,6 +68,14 @@ export class ChildrenQuery extends ResolverQuery<SafeResolverEvent[]> {
protected legacyQuery(endpointID: string, uniquePIDs: string[]): JsonObject {
const paginationFields = this.pagination.buildQueryFields('endgame.serial_event_id');
return {
_source: [
'@timestamp',
'endgame.serial_event_id',
'endgame.unique_pid',
'endgame.unique_ppid',
'event.type',
'event.action',
],
collapse: {
field: 'endgame.unique_pid',
},
Expand Down Expand Up @@ -64,8 +116,18 @@ export class ChildrenQuery extends ResolverQuery<SafeResolverEvent[]> {
}

protected query(entityIDs: string[]): JsonObject {
// we don't have to include the `event.id` in the source response because it is not needed for processing
// the data returned by ES, it is only used for breaking ties when ES is doing the search
const paginationFields = this.pagination.buildQueryFields('event.id');
return {
_source: [
'@timestamp',
'event.type',
'event.sequence',
'process.entity_id',
'process.parent.entity_id',
'process.Ext.ancestry',
],
/**
* Using collapse here will only return a single event per occurrence of a process.entity_id. The events are sorted
* based on timestamp in ascending order so it will be the first event that ocurred. The actual type of event that
Expand Down Expand Up @@ -126,7 +188,7 @@ export class ChildrenQuery extends ResolverQuery<SafeResolverEvent[]> {
};
}

formatResponse(response: SearchResponse<SafeResolverEvent>): SafeResolverEvent[] {
formatResponse(response: SearchResponse<ChildEvent>): ChildEvent[] {
return this.getResults(response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
} from '../../../../../common/endpoint/types';
import { createChild } from './node';
import { ChildrenPaginationBuilder } from './children_pagination';
import { ChildEvent } from '../queries/children';

/**
* This class helps construct the children structure when building a resolver tree.
Expand Down Expand Up @@ -86,15 +87,12 @@ 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<string>,
startEvents: SafeResolverEvent[]
): Set<string> | undefined {
addStartEvents(queriedNodes: Set<string>, startEvents: ChildEvent[]): Set<string> | undefined {
let largestAncestryArray = 0;
const nodesToQueryNext: Map<number, Set<string>> = new Map();
const nonLeafNodes: Set<SafeResolverChildNode> = new Set();

const isDistantGrandchild = (event: SafeResolverEvent) => {
const isDistantGrandchild = (event: ChildEvent) => {
const ancestry = getAncestryAsArray(event);
return ancestry.length > 0 && queriedNodes.has(ancestry[ancestry.length - 1]);
};
Expand Down Expand Up @@ -161,7 +159,7 @@ export class ChildrenNodesHelper {
return nodesToQueryNext.get(largestAncestryArray);
}

private setPaginationForNodes(nodes: Set<string>, startEvents: SafeResolverEvent[]) {
private setPaginationForNodes(nodes: Set<string>, startEvents: ChildEvent[]) {
for (const nodeEntityID of nodes.values()) {
const cachedNode = this.entityToNodeCache.get(nodeEntityID);
if (cachedNode) {
Expand Down
Loading