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][Endpoint] Leveraging msearch and ancestry array for resolver #70134

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1f0e448
Refactor generator for ancestry support
jonathan-buttner Jun 26, 2020
f0ee76a
Adding optional ancestry array
jonathan-buttner Jun 26, 2020
c95132f
Refactor the pagination since the totals are not used anymore
jonathan-buttner Jun 26, 2020
e4b8a13
Updating the queries to not use aggregations for determining the totals
jonathan-buttner Jun 26, 2020
407db62
Refactoring the children helper to handle pagination without totals
jonathan-buttner Jun 26, 2020
8947b43
Pinning the seed for the resolver tree generator service
jonathan-buttner Jun 26, 2020
1287379
Splitting the fetcher into multiple classes for msearch
jonathan-buttner Jun 26, 2020
54f1a3f
Updating tests and api for ancestry array and msearch
jonathan-buttner Jun 26, 2020
92005dd
Adding more comments and fixing type errors
jonathan-buttner Jun 26, 2020
6cdf961
Fixing resolver test import
jonathan-buttner Jun 26, 2020
40f5537
Fixing tests and type errors
jonathan-buttner Jun 29, 2020
f54a462
Fixing merge conflicts
jonathan-buttner Jun 29, 2020
1eac63f
Fixing type errors and tests
jonathan-buttner Jun 29, 2020
79464ed
Merge branch 'master' of github.com:elastic/kibana into msearch-ances…
jonathan-buttner Jun 29, 2020
5a872da
Merge branch 'master' of github.com:elastic/kibana into generator-ref…
jonathan-buttner Jun 29, 2020
99acb58
Removing useAncestry field
jonathan-buttner Jun 29, 2020
3f9f3eb
Fixing test
jonathan-buttner Jun 29, 2020
e75f7bf
Merge branch 'generator-refactor' into msearch-ancestry-array
jonathan-buttner Jun 29, 2020
caa8f8f
Removing useAncestry field from tests
jonathan-buttner Jun 29, 2020
544e1f3
An empty array will be returned because that's how ES will do it too
jonathan-buttner Jun 29, 2020
c99773b
Merge branch 'generator-refactor' into msearch-ancestry-array
jonathan-buttner Jun 29, 2020
d274a07
Resolving conflicts
jonathan-buttner Jun 29, 2020
200ce25
Resolving conflicts
jonathan-buttner Jul 1, 2020
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
18 changes: 18 additions & 0 deletions x-pack/plugins/security_solution/common/endpoint/models/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,24 @@ export function ancestryArray(event: ResolverEvent): string[] | undefined {
return event.process.Ext.ancestry;
}

export function getAncestryAsArray(event: ResolverEvent | undefined): string[] {
if (!event) {
return [];
}

const ancestors = ancestryArray(event);
if (ancestors) {
return ancestors;
}

const parentID = parentEntityId(event);
if (parentID) {
return [parentID];
}

return [];
}

/**
* @param event The event to get the category for
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export const validateTree = {
params: schema.object({ id: schema.string() }),
query: schema.object({
children: schema.number({ defaultValue: 10, min: 0, max: 100 }),
generations: schema.number({ defaultValue: 3, min: 0, max: 3 }),
ancestors: schema.number({ defaultValue: 3, min: 0, max: 5 }),
events: schema.number({ defaultValue: 100, min: 0, max: 1000 }),
alerts: schema.number({ defaultValue: 100, min: 0, max: 1000 }),
Expand Down Expand Up @@ -66,7 +65,6 @@ export const validateChildren = {
params: schema.object({ id: schema.string() }),
query: schema.object({
children: schema.number({ defaultValue: 10, min: 1, max: 100 }),
generations: schema.number({ defaultValue: 3, min: 1, max: 3 }),
afterChild: schema.maybe(schema.string()),
legacyEndpointID: schema.maybe(schema.string()),
}),
Expand Down
25 changes: 19 additions & 6 deletions x-pack/plugins/security_solution/common/endpoint/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,18 @@ export interface ResolverNodeStats {
*/
export interface ResolverChildNode extends ResolverLifecycleNode {
/**
* A child node's pagination cursor can be null for a couple reasons:
* 1. At the time of querying it could have no children in ES, in which case it will be marked as
* null because we know it does not have children during this query.
* 2. If the max level was reached we do not know if this node has children or not so we'll mark it as null
* nextChild can have 3 different states:
Copy link
Contributor

Choose a reason for hiding this comment

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

Just a thought, I understand what this is saying, especially after reading through your write up, but one of @oatkiller's famous diagrams may be useful here, you could probs just copy paste one he's already done. Not necessary, but I think it could be helpful

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks @michaelolo24 do you mean an ascii diagram of a tree? Or a table?

Copy link
Contributor

Choose a reason for hiding this comment

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

ascii I think

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think what I'll probably do is put a link to the relevant section in the docs that I'll be adding as a follow up or add on to this PR.

*
* 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
*/
nextChild: string | null;
nextChild?: string | null;
}

/**
Expand All @@ -91,7 +97,14 @@ export interface ResolverChildNode extends ResolverLifecycleNode {
export interface ResolverChildren {
childNodes: ResolverChildNode[];
/**
* This is the children cursor for the origin of a tree.
* 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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ export function handleChildren(
return async (context, req, res) => {
const {
params: { id },
query: { children, generations, afterChild, legacyEndpointID: endpointID },
query: { children, afterChild, legacyEndpointID: endpointID },
} = req;
try {
const client = context.core.elasticsearch.legacy.client;
const fetcher = new Fetcher(client, id, eventsIndexPattern, alertsIndexPattern, endpointID);

return res.ok({
body: await fetcher.children(children, generations, afterChild),
body: await fetcher.children(children, afterChild),
});
} catch (err) {
log.warn(err);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
import { SearchResponse } from 'elasticsearch';
import { ResolverEvent } from '../../../../../common/endpoint/types';
import { ResolverQuery } from './base';
import { PaginationBuilder, PaginatedResults } from '../utils/pagination';
import { PaginationBuilder } from '../utils/pagination';
import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common';

/**
* Builds a query for retrieving alerts for a node.
*/
export class AlertsQuery extends ResolverQuery<PaginatedResults> {
export class AlertsQuery extends ResolverQuery<ResolverEvent[]> {
constructor(
private readonly pagination: PaginationBuilder,
indexPattern: string | string[],
Expand All @@ -38,11 +38,7 @@ export class AlertsQuery extends ResolverQuery<PaginatedResults> {
],
},
},
...this.pagination.buildQueryFields(
uniquePIDs.length,
'endgame.serial_event_id',
'endgame.unique_pid'
),
...this.pagination.buildQueryFields('endgame.serial_event_id'),
};
}

Expand All @@ -60,14 +56,11 @@ export class AlertsQuery extends ResolverQuery<PaginatedResults> {
],
},
},
...this.pagination.buildQueryFields(entityIDs.length, 'event.id', 'process.entity_id'),
...this.pagination.buildQueryFields('event.id'),
};
}

formatResponse(response: SearchResponse<ResolverEvent>): PaginatedResults {
return {
results: ResolverQuery.getResults(response),
totals: PaginationBuilder.getTotals(response.aggregations),
};
formatResponse(response: SearchResponse<ResolverEvent>): ResolverEvent[] {
return this.getResults(response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ import { MSearchQuery } from './multi_searcher';
/**
* ResolverQuery provides the base structure for queries to retrieve events when building a resolver graph.
*
* @param T the structured return type of a resolver query. This represents the type that is returned when translating
* Elasticsearch's SearchResponse<ResolverEvent> response.
* @param T the structured return type of a resolver query. This represents the final return type of the query after handling
* any aggregations.
* @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<T> implements MSearchQuery {
export abstract class ResolverQuery<T, R = ResolverEvent> implements MSearchQuery {
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 R here sets the stage for being able to limit the _source of a queries response. This will be in a follow up PR. Limit the source will be a performance improvement for querying for the start events for the children.

/**
*
* @param indexPattern the index pattern to use in the query for finding indices with documents in ES.
Expand Down Expand Up @@ -50,7 +52,7 @@ export abstract class ResolverQuery<T> implements MSearchQuery {
};
}

protected static getResults(response: SearchResponse<ResolverEvent>): ResolverEvent[] {
protected getResults(response: SearchResponse<R>): R[] {
return response.hits.hits.map((hit) => hit._source);
}

Expand All @@ -68,19 +70,26 @@ export abstract class ResolverQuery<T> implements MSearchQuery {
}

/**
* Searches ES for the specified ids.
* Searches ES for the specified ids and format the response.
*
* @param client a client for searching ES
* @param ids a single more multiple unique node ids (e.g. entity_id or unique_pid)
*/
async search(client: ILegacyScopedClusterClient, ids: string | string[]): Promise<T> {
const res: SearchResponse<ResolverEvent> = await client.callAsCurrentUser(
'search',
this.buildSearch(ids)
);
async searchAndFormat(client: ILegacyScopedClusterClient, ids: string | string[]): Promise<T> {
const res: SearchResponse<ResolverEvent> = await this.search(client, ids);
return this.formatResponse(res);
}

/**
* Searches ES for the specified ids but do not format the response.
*
* @param client a client for searching ES
* @param ids a single more multiple unique node ids (e.g. entity_id or unique_pid)
*/
async search(client: ILegacyScopedClusterClient, ids: string | string[]) {
return client.callAsCurrentUser('search', this.buildSearch(ids));
}

/**
* Builds a query to search the legacy data format.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ describe('Children query', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const msearch: any = query.buildMSearch(['1234', '5678']);
expect(msearch[0].index).toBe('index-pattern');
expect(msearch[1].query.bool.filter[0]).toStrictEqual({
expect(msearch[1].query.bool.filter[0].bool.should[0]).toStrictEqual({
terms: { 'process.parent.entity_id': ['1234', '5678'] },
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
import { SearchResponse } from 'elasticsearch';
import { ResolverEvent } from '../../../../../common/endpoint/types';
import { ResolverQuery } from './base';
import { PaginationBuilder, PaginatedResults } from '../utils/pagination';
import { PaginationBuilder } from '../utils/pagination';
import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common';

/**
* Builds a query for retrieving descendants of a node.
*/
export class ChildrenQuery extends ResolverQuery<PaginatedResults> {
export class ChildrenQuery extends ResolverQuery<ResolverEvent[]> {
constructor(
private readonly pagination: PaginationBuilder,
indexPattern: string | string[],
Expand Down Expand Up @@ -53,11 +53,7 @@ export class ChildrenQuery extends ResolverQuery<PaginatedResults> {
],
},
},
...this.pagination.buildQueryFields(
uniquePIDs.length,
'endgame.serial_event_id',
'endgame.unique_ppid'
),
...this.pagination.buildQueryFields('endgame.serial_event_id'),
};
}

Expand All @@ -67,7 +63,16 @@ export class ChildrenQuery extends ResolverQuery<PaginatedResults> {
bool: {
filter: [
{
terms: { 'process.parent.entity_id': entityIDs },
bool: {
should: [
{
terms: { 'process.parent.entity_id': entityIDs },
},
{
terms: { 'process.Ext.ancestry': entityIDs },
},
],
},
},
{
term: { 'event.category': 'process' },
Expand All @@ -81,14 +86,11 @@ export class ChildrenQuery extends ResolverQuery<PaginatedResults> {
],
},
},
...this.pagination.buildQueryFields(entityIDs.length, 'event.id', 'process.parent.entity_id'),
...this.pagination.buildQueryFields('event.id'),
};
}

formatResponse(response: SearchResponse<ResolverEvent>): PaginatedResults {
return {
results: ResolverQuery.getResults(response),
totals: PaginationBuilder.getTotals(response.aggregations),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Totals are inferred based on the response of ES while using the ancestry array.

};
formatResponse(response: SearchResponse<ResolverEvent>): ResolverEvent[] {
return this.getResults(response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
import { SearchResponse } from 'elasticsearch';
import { ResolverEvent } from '../../../../../common/endpoint/types';
import { ResolverQuery } from './base';
import { PaginationBuilder, PaginatedResults } from '../utils/pagination';
import { PaginationBuilder } from '../utils/pagination';
import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common';

/**
* Builds a query for retrieving related events for a node.
*/
export class EventsQuery extends ResolverQuery<PaginatedResults> {
export class EventsQuery extends ResolverQuery<ResolverEvent[]> {
constructor(
private readonly pagination: PaginationBuilder,
indexPattern: string | string[],
Expand Down Expand Up @@ -45,11 +45,7 @@ export class EventsQuery extends ResolverQuery<PaginatedResults> {
],
},
},
...this.pagination.buildQueryFields(
uniquePIDs.length,
'endgame.serial_event_id',
'endgame.unique_pid'
),
...this.pagination.buildQueryFields('endgame.serial_event_id'),
};
}

Expand All @@ -74,14 +70,11 @@ export class EventsQuery extends ResolverQuery<PaginatedResults> {
],
},
},
...this.pagination.buildQueryFields(entityIDs.length, 'event.id', 'process.entity_id'),
...this.pagination.buildQueryFields('event.id'),
};
}

formatResponse(response: SearchResponse<ResolverEvent>): PaginatedResults {
return {
results: ResolverQuery.getResults(response),
totals: PaginationBuilder.getTotals(response.aggregations),
};
formatResponse(response: SearchResponse<ResolverEvent>): ResolverEvent[] {
return this.getResults(response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,6 @@ export class LifecycleQuery extends ResolverQuery<ResolverEvent[]> {
}

formatResponse(response: SearchResponse<ResolverEvent>): ResolverEvent[] {
return ResolverQuery.getResults(response);
return this.getResults(response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import { ILegacyScopedClusterClient } from 'kibana/server';
import { MSearchResponse } from 'elasticsearch';
import { MSearchResponse, SearchResponse } from 'elasticsearch';
import { ResolverEvent } from '../../../../../common/endpoint/types';
import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common';

Expand Down Expand Up @@ -34,6 +34,10 @@ export interface QueryInfo {
* one or many unique identifiers to be searched for in this query
*/
ids: string | string[];
/**
* a function to handle the response
*/
handler: (response: SearchResponse<ResolverEvent>) => void;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This calls into the class (e.g. RelatedEventsQueryHandler etc) for it to handle the response from ES.

}

/**
Expand All @@ -57,10 +61,10 @@ export class MultiSearcher {
throw new Error('No queries provided to MultiSearcher');
}

let searchQuery: JsonObject[] = [];
queries.forEach(
(info) => (searchQuery = [...searchQuery, ...info.query.buildMSearch(info.ids)])
);
const searchQuery: JsonObject[] = [];
for (const info of queries) {
searchQuery.push(...info.query.buildMSearch(info.ids));
}
const res: MSearchResponse<ResolverEvent> = await this.client.callAsCurrentUser('msearch', {
body: searchQuery,
});
Expand All @@ -72,6 +76,8 @@ export class MultiSearcher {
if (res.responses.length !== queries.length) {
throw new Error(`Responses length was: ${res.responses.length} expected ${queries.length}`);
}
return res.responses;
for (let i = 0; i < queries.length; i++) {
queries[i].handler(res.responses[i]);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@ import { SearchResponse } from 'elasticsearch';
import { ResolverQuery } from './base';
import { ResolverEvent, EventStats } from '../../../../../common/endpoint/types';
import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common';
import { AggBucket } from '../utils/pagination';

export interface StatsResult {
alerts: Record<string, number>;
events: Record<string, EventStats>;
}

interface AggBucket {
key: string;
doc_count: number;
}

interface CategoriesAgg extends AggBucket {
/**
* The reason categories is optional here is because if no data was returned in the query the categories aggregation
Expand Down
Loading