diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts b/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts index 311aa0c04c9a..a6d59615794a 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts @@ -26,7 +26,7 @@ export const validateTree = { /** * Used to validate GET requests for non process events for a specific event. */ -export const validateEvents = { +export const validateRelatedEvents = { params: schema.object({ id: schema.string({ minLength: 1 }) }), query: schema.object({ events: schema.number({ defaultValue: 1000, min: 1, max: 10000 }), @@ -40,6 +40,22 @@ export const validateEvents = { ), }; +/** + * Used to validate POST requests for `/resolver/events` api. + */ +export const validateEvents = { + query: schema.object({ + // keeping the max as 10k because the limit in ES for a single query is also 10k + limit: schema.number({ defaultValue: 1000, min: 1, max: 10000 }), + afterEvent: schema.maybe(schema.string()), + }), + body: schema.nullable( + schema.object({ + filter: schema.maybe(schema.string()), + }) + ), +}; + /** * Used to validate GET requests for alerts for a specific process. */ 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 d97fdfbf7d18..abb0ccee8d90 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -276,6 +276,15 @@ export interface SafeResolverRelatedEvents { nextEvent: string | null; } +/** + * Response structure for the events route. + * `nextEvent` will be set to null when at the time of querying there were no more results to retrieve from ES. + */ +export interface ResolverPaginatedEvents { + events: SafeResolverEvent[]; + nextEvent: string | null; +} + /** * Response structure for the alerts route. */ diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver.ts index 3ec968e4a0e1..c9159032a791 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver.ts @@ -8,29 +8,42 @@ import { IRouter } from 'kibana/server'; import { EndpointAppContext } from '../types'; import { validateTree, + validateRelatedEvents, validateEvents, validateChildren, validateAncestry, validateAlerts, validateEntities, } from '../../../common/endpoint/schema/resolver'; -import { handleEvents } from './resolver/events'; +import { handleRelatedEvents } from './resolver/related_events'; import { handleChildren } from './resolver/children'; import { handleAncestry } from './resolver/ancestry'; import { handleTree } from './resolver/tree'; import { handleAlerts } from './resolver/alerts'; import { handleEntities } from './resolver/entity'; +import { handleEvents } from './resolver/events'; export function registerResolverRoutes(router: IRouter, endpointAppContext: EndpointAppContext) { const log = endpointAppContext.logFactory.get('resolver'); + // this route will be removed in favor of the one below router.post( { + // @deprecated use `/resolver/events` instead path: '/api/endpoint/resolver/{id}/events', + validate: validateRelatedEvents, + options: { authRequired: true }, + }, + handleRelatedEvents(log, endpointAppContext) + ); + + router.post( + { + path: '/api/endpoint/resolver/events', validate: validateEvents, options: { authRequired: true }, }, - handleEvents(log, endpointAppContext) + handleEvents(log) ); router.post( diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/events.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/events.ts index 80d21ae11828..a212215fe18a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/events.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/events.ts @@ -6,32 +6,39 @@ import { TypeOf } from '@kbn/config-schema'; import { RequestHandler, Logger } from 'kibana/server'; -import { eventsIndexPattern, alertsIndexPattern } from '../../../../common/endpoint/constants'; +import { eventsIndexPattern } from '../../../../common/endpoint/constants'; import { validateEvents } from '../../../../common/endpoint/schema/resolver'; -import { Fetcher } from './utils/fetch'; -import { EndpointAppContext } from '../../types'; +import { EventsQuery } from './queries/events'; +import { createEvents } from './utils/node'; +import { PaginationBuilder } from './utils/pagination'; +/** + * This function handles the `/events` api and returns an array of events and a cursor if more events exist than were + * requested. + * @param log a logger object + */ export function handleEvents( - log: Logger, - endpointAppContext: EndpointAppContext + log: Logger ): RequestHandler< - TypeOf, + unknown, TypeOf, TypeOf > { return async (context, req, res) => { const { - params: { id }, - query: { events, afterEvent, legacyEndpointID: endpointID }, + query: { limit, afterEvent }, body, } = req; try { - const client = context.core.elasticsearch.legacy.client; - - const fetcher = new Fetcher(client, id, eventsIndexPattern, alertsIndexPattern, endpointID); + const client = context.core.elasticsearch.client; + const query = new EventsQuery( + PaginationBuilder.createBuilder(limit, afterEvent), + eventsIndexPattern + ); + const results = await query.search(client, body?.filter); return res.ok({ - body: await fetcher.events(events, afterEvent, body?.filter), + body: createEvents(results, PaginationBuilder.buildCursorRequestLimit(limit, results)), }); } catch (err) { log.warn(err); 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 bd054d548a93..3be0cc5daff7 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 @@ -4,78 +4,31 @@ * you may not use this file except in compliance with the Elastic License. */ import { SearchResponse } from 'elasticsearch'; +import { IScopedClusterClient } from 'kibana/server'; +import { ApiResponse } from '@elastic/elasticsearch'; import { esKuery } from '../../../../../../../../src/plugins/data/server'; import { SafeResolverEvent } from '../../../../../common/endpoint/types'; -import { ResolverQuery } from './base'; import { PaginationBuilder } from '../utils/pagination'; import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common'; /** - * Builds a query for retrieving related events for a node. + * Builds a query for retrieving events. */ -export class EventsQuery extends ResolverQuery { - private readonly kqlQuery: JsonObject[] = []; - +export class EventsQuery { constructor( private readonly pagination: PaginationBuilder, - indexPattern: string | string[], - endpointID?: string, - kql?: string - ) { - super(indexPattern, endpointID); - if (kql) { - this.kqlQuery.push(esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kql))); - } - } + private readonly indexPattern: string | string[] + ) {} - protected legacyQuery(endpointID: string, uniquePIDs: string[]): JsonObject { + private query(kqlQuery: JsonObject[]): JsonObject { return { query: { bool: { filter: [ - ...this.kqlQuery, - { - terms: { 'endgame.unique_pid': uniquePIDs }, - }, - { - term: { 'agent.id': endpointID }, - }, + ...kqlQuery, { term: { 'event.kind': 'event' }, }, - { - bool: { - must_not: { - term: { 'event.category': 'process' }, - }, - }, - }, - ], - }, - }, - ...this.pagination.buildQueryFields('endgame.serial_event_id', 'desc'), - }; - } - - protected query(entityIDs: string[]): JsonObject { - return { - query: { - bool: { - filter: [ - ...this.kqlQuery, - { - terms: { 'process.entity_id': entityIDs }, - }, - { - term: { 'event.kind': 'event' }, - }, - { - bool: { - must_not: { - term: { 'event.category': 'process' }, - }, - }, - }, ], }, }, @@ -83,7 +36,27 @@ export class EventsQuery extends ResolverQuery { }; } - formatResponse(response: SearchResponse): SafeResolverEvent[] { - return this.getResults(response); + private buildSearch(kql: JsonObject[]) { + return { + body: this.query(kql), + index: this.indexPattern, + }; + } + + /** + * Searches ES for the specified events and format the response. + * + * @param client a client for searching ES + * @param kql an optional kql string for filtering the results + */ + async search(client: IScopedClusterClient, kql?: string): Promise { + const kqlQuery: JsonObject[] = []; + if (kql) { + kqlQuery.push(esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kql))); + } + const response: ApiResponse> = await client.asCurrentUser.search(this.buildSearch(kqlQuery)); + return response.body.hits.hits.map((hit) => hit._source); } } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/related_events.test.ts similarity index 93% rename from x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.test.ts rename to x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/related_events.test.ts index 00d7570b2b65..3ddf8fa4090d 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/related_events.test.ts @@ -3,7 +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 { EventsQuery } from './events'; +/** + * @deprecated use the `events.ts` file's query instead + */ +import { EventsQuery } from './related_events'; import { PaginationBuilder } from '../utils/pagination'; import { legacyEventIndexPattern } from './legacy_event_index_pattern'; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/related_events.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/related_events.ts new file mode 100644 index 000000000000..f419c1fb6e1d --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/related_events.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/** + * @deprecated use the `events.ts` file's query instead + */ +import { SearchResponse } from 'elasticsearch'; +import { esKuery } from '../../../../../../../../src/plugins/data/server'; +import { SafeResolverEvent } from '../../../../../common/endpoint/types'; +import { ResolverQuery } from './base'; +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 { + private readonly kqlQuery: JsonObject[] = []; + + constructor( + private readonly pagination: PaginationBuilder, + indexPattern: string | string[], + endpointID?: string, + kql?: string + ) { + super(indexPattern, endpointID); + if (kql) { + this.kqlQuery.push(esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kql))); + } + } + + protected legacyQuery(endpointID: string, uniquePIDs: string[]): JsonObject { + return { + query: { + bool: { + filter: [ + ...this.kqlQuery, + { + terms: { 'endgame.unique_pid': uniquePIDs }, + }, + { + term: { 'agent.id': endpointID }, + }, + { + term: { 'event.kind': 'event' }, + }, + { + bool: { + must_not: { + term: { 'event.category': 'process' }, + }, + }, + }, + ], + }, + }, + ...this.pagination.buildQueryFields('endgame.serial_event_id', 'desc'), + }; + } + + protected query(entityIDs: string[]): JsonObject { + return { + query: { + bool: { + filter: [ + ...this.kqlQuery, + { + terms: { 'process.entity_id': entityIDs }, + }, + { + term: { 'event.kind': 'event' }, + }, + { + bool: { + must_not: { + term: { 'event.category': 'process' }, + }, + }, + }, + ], + }, + }, + ...this.pagination.buildQueryFields('event.id', 'desc'), + }; + } + + formatResponse(response: SearchResponse): SafeResolverEvent[] { + return this.getResults(response); + } +} diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/related_events.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/related_events.ts new file mode 100644 index 000000000000..8fd9ab9a5ccd --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/related_events.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/** + * @deprecated use the `resolver/events` route and handler instead + */ +import { TypeOf } from '@kbn/config-schema'; +import { RequestHandler, Logger } from 'kibana/server'; +import { eventsIndexPattern, alertsIndexPattern } from '../../../../common/endpoint/constants'; +import { validateRelatedEvents } from '../../../../common/endpoint/schema/resolver'; +import { Fetcher } from './utils/fetch'; +import { EndpointAppContext } from '../../types'; + +export function handleRelatedEvents( + log: Logger, + endpointAppContext: EndpointAppContext +): RequestHandler< + TypeOf, + TypeOf, + TypeOf +> { + return async (context, req, res) => { + const { + params: { id }, + query: { events, afterEvent, legacyEndpointID: endpointID }, + body, + } = req; + try { + const client = context.core.elasticsearch.legacy.client; + + const fetcher = new Fetcher(client, id, eventsIndexPattern, alertsIndexPattern, endpointID); + + return res.ok({ + body: await fetcher.events(events, afterEvent, body?.filter), + }); + } catch (err) { + log.warn(err); + return res.internalError({ body: err }); + } + }; +} 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 5c4d9a4741ad..a5aa9b6c288c 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 @@ -3,12 +3,14 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +/** + * @deprecated msearch functionality for querying events will be removed shortly + */ import { SearchResponse } from 'elasticsearch'; import { ILegacyScopedClusterClient } from 'kibana/server'; import { SafeResolverRelatedEvents, SafeResolverEvent } from '../../../../../common/endpoint/types'; import { createRelatedEvents } from './node'; -import { EventsQuery } from '../queries/events'; +import { EventsQuery } from '../queries/related_events'; import { PaginationBuilder } from './pagination'; import { QueryInfo } from '../queries/multi_searcher'; import { SingleQueryHandler } from './fetch'; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/node.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/node.ts index 535ee37f3db3..cecdc8a47895 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/node.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/node.ts @@ -13,6 +13,7 @@ import { SafeResolverEvent, SafeResolverChildNode, SafeResolverRelatedEvents, + ResolverPaginatedEvents, } from '../../../../../common/endpoint/types'; /** @@ -30,6 +31,19 @@ export function createRelatedEvents( return { entityID, events, nextEvent }; } +/** + * Creates an object that the events handler would return + * + * @param events array of events + * @param nextEvent the cursor to retrieve the next event + */ +export function createEvents( + events: SafeResolverEvent[] = [], + nextEvent: string | null = null +): ResolverPaginatedEvents { + return { events, nextEvent }; +} + /** * Creates an alert object that the alerts handler would return * 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 4e248f52ec29..b57486ee55ca 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 @@ -5,7 +5,10 @@ */ import expect from '@kbn/expect'; import { eventIDSafeVersion } from '../../../../plugins/security_solution/common/endpoint/models/event'; -import { SafeResolverRelatedEvents } from '../../../../plugins/security_solution/common/endpoint/types'; +import { + ResolverPaginatedEvents, + SafeResolverRelatedEvents, +} from '../../../../plugins/security_solution/common/endpoint/types'; import { FtrProviderContext } from '../../ftr_provider_context'; import { Tree, @@ -41,97 +44,221 @@ export default function ({ getService }: FtrProviderContext) { ancestryArraySize: 2, }; - describe('related events route', () => { - before(async () => { - await esArchiver.load('endpoint/resolver/api_feature'); - resolverTrees = await resolver.createTrees(treeOptions); - // we only requested a single alert so there's only 1 tree - tree = resolverTrees.trees[0]; - }); - after(async () => { - await resolver.deleteData(resolverTrees); - await esArchiver.unload('endpoint/resolver/api_feature'); + describe('event routes', () => { + describe('related events route', () => { + before(async () => { + await esArchiver.load('endpoint/resolver/api_feature'); + resolverTrees = await resolver.createTrees(treeOptions); + // we only requested a single alert so there's only 1 tree + tree = resolverTrees.trees[0]; + }); + after(async () => { + await resolver.deleteData(resolverTrees); + await esArchiver.unload('endpoint/resolver/api_feature'); + }); + + describe('legacy events', () => { + const endpointID = '5a0c957f-b8e7-4538-965e-57e8bb86ad3a'; + const entityID = '94042'; + const cursor = 'eyJ0aW1lc3RhbXAiOjE1ODE0NTYyNTUwMDAsImV2ZW50SUQiOiI5NDA0MyJ9'; + + it('should return details for the root node', async () => { + const { body }: { body: SafeResolverRelatedEvents } = await supertest + .post(`/api/endpoint/resolver/${entityID}/events?legacyEndpointID=${endpointID}`) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.events.length).to.eql(1); + expect(body.entityID).to.eql(entityID); + expect(body.nextEvent).to.eql(null); + }); + + it('returns no values when there is no more data', async () => { + 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}` + ) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.events).be.empty(); + expect(body.entityID).to.eql(entityID); + expect(body.nextEvent).to.eql(null); + }); + + it('should return the first page of information when the cursor is invalid', async () => { + const { body }: { body: SafeResolverRelatedEvents } = await supertest + .post( + `/api/endpoint/resolver/${entityID}/events?legacyEndpointID=${endpointID}&afterEvent=blah` + ) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.entityID).to.eql(entityID); + expect(body.nextEvent).to.eql(null); + }); + + it('should return no results for an invalid endpoint ID', async () => { + const { body }: { body: SafeResolverRelatedEvents } = await supertest + .post(`/api/endpoint/resolver/${entityID}/events?legacyEndpointID=foo`) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.nextEvent).to.eql(null); + expect(body.entityID).to.eql(entityID); + expect(body.events).to.be.empty(); + }); + + it('should error on invalid pagination values', async () => { + await supertest + .post(`/api/endpoint/resolver/${entityID}/events?events=0`) + .set('kbn-xsrf', 'xxx') + .expect(400); + await supertest + .post(`/api/endpoint/resolver/${entityID}/events?events=20000`) + .set('kbn-xsrf', 'xxx') + .expect(400); + await supertest + .post(`/api/endpoint/resolver/${entityID}/events?events=-1`) + .set('kbn-xsrf', 'xxx') + .expect(400); + }); + }); + + describe('endpoint events', () => { + it('should not find any events', async () => { + const { body }: { body: SafeResolverRelatedEvents } = await supertest + .post(`/api/endpoint/resolver/5555/events`) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.nextEvent).to.eql(null); + expect(body.events).to.be.empty(); + }); + + it('should return details for the root node', async () => { + const { body }: { body: SafeResolverRelatedEvents } = await supertest + .post(`/api/endpoint/resolver/${tree.origin.id}/events`) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.events.length).to.eql(4); + compareArrays(tree.origin.relatedEvents, body.events, true); + expect(body.nextEvent).to.eql(null); + }); + + it('should allow for the events to be filtered', async () => { + const filter = `event.category:"${RelatedEventCategory.Driver}"`; + const { body }: { body: SafeResolverRelatedEvents } = await supertest + .post(`/api/endpoint/resolver/${tree.origin.id}/events`) + .set('kbn-xsrf', 'xxx') + .send({ + filter, + }) + .expect(200); + expect(body.events.length).to.eql(2); + compareArrays(tree.origin.relatedEvents, body.events); + expect(body.nextEvent).to.eql(null); + for (const event of body.events) { + expect(event.event?.category).to.be(RelatedEventCategory.Driver); + } + }); + + it('should return paginated results for the root node', async () => { + let { body }: { body: SafeResolverRelatedEvents } = await supertest + .post(`/api/endpoint/resolver/${tree.origin.id}/events?events=2`) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.events.length).to.eql(2); + compareArrays(tree.origin.relatedEvents, body.events); + expect(body.nextEvent).not.to.eql(null); + + ({ body } = await supertest + .post( + `/api/endpoint/resolver/${tree.origin.id}/events?events=2&afterEvent=${body.nextEvent}` + ) + .set('kbn-xsrf', 'xxx') + .expect(200)); + expect(body.events.length).to.eql(2); + compareArrays(tree.origin.relatedEvents, body.events); + expect(body.nextEvent).to.not.eql(null); + + ({ body } = await supertest + .post( + `/api/endpoint/resolver/${tree.origin.id}/events?events=2&afterEvent=${body.nextEvent}` + ) + .set('kbn-xsrf', 'xxx') + .expect(200)); + expect(body.events).to.be.empty(); + expect(body.nextEvent).to.eql(null); + }); + + it('should return the first page of information when the cursor is invalid', async () => { + const { body }: { body: SafeResolverRelatedEvents } = await supertest + .post(`/api/endpoint/resolver/${tree.origin.id}/events?afterEvent=blah`) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.events.length).to.eql(4); + compareArrays(tree.origin.relatedEvents, body.events, true); + expect(body.nextEvent).to.eql(null); + }); + + it('should sort the events in descending order', async () => { + const { body }: { body: SafeResolverRelatedEvents } = await supertest + .post(`/api/endpoint/resolver/${tree.origin.id}/events`) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.events.length).to.eql(4); + // these events are created in the order they are defined in the array so the newest one is + // 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(eventIDSafeVersion(body.events[i])).to.equal(relatedEvents[i].event?.id); + } + }); + }); }); - describe('legacy events', () => { - const endpointID = '5a0c957f-b8e7-4538-965e-57e8bb86ad3a'; - const entityID = '94042'; - const cursor = 'eyJ0aW1lc3RhbXAiOjE1ODE0NTYyNTUwMDAsImV2ZW50SUQiOiI5NDA0MyJ9'; + describe('kql events route', () => { + let entityIDFilter: string | undefined; + before(async () => { + resolverTrees = await resolver.createTrees(treeOptions); + // we only requested a single alert so there's only 1 tree + tree = resolverTrees.trees[0]; + entityIDFilter = `process.entity_id:"${tree.origin.id}" and not event.category:"process"`; + }); + after(async () => { + await resolver.deleteData(resolverTrees); + }); - it('should return details for the root node', async () => { - const { body }: { body: SafeResolverRelatedEvents } = await supertest - .post(`/api/endpoint/resolver/${entityID}/events?legacyEndpointID=${endpointID}`) + it('should filter events by event.id', async () => { + const { body }: { body: ResolverPaginatedEvents } = await supertest + .post(`/api/endpoint/resolver/events`) .set('kbn-xsrf', 'xxx') + .send({ + filter: `event.id:"${tree.origin.relatedEvents[0]?.event?.id}"`, + }) .expect(200); expect(body.events.length).to.eql(1); - expect(body.entityID).to.eql(entityID); + expect(tree.origin.relatedEvents[0]?.event?.id).to.eql(body.events[0].event?.id); expect(body.nextEvent).to.eql(null); }); - it('returns no values when there is no more data', async () => { - 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}` - ) - .set('kbn-xsrf', 'xxx') - .expect(200); - expect(body.events).be.empty(); - expect(body.entityID).to.eql(entityID); - expect(body.nextEvent).to.eql(null); - }); - - it('should return the first page of information when the cursor is invalid', async () => { - const { body }: { body: SafeResolverRelatedEvents } = await supertest - .post( - `/api/endpoint/resolver/${entityID}/events?legacyEndpointID=${endpointID}&afterEvent=blah` - ) - .set('kbn-xsrf', 'xxx') - .expect(200); - expect(body.entityID).to.eql(entityID); - expect(body.nextEvent).to.eql(null); - }); - - it('should return no results for an invalid endpoint ID', async () => { - const { body }: { body: SafeResolverRelatedEvents } = await supertest - .post(`/api/endpoint/resolver/${entityID}/events?legacyEndpointID=foo`) - .set('kbn-xsrf', 'xxx') - .expect(200); - expect(body.nextEvent).to.eql(null); - expect(body.entityID).to.eql(entityID); - expect(body.events).to.be.empty(); - }); - - it('should error on invalid pagination values', async () => { - await supertest - .post(`/api/endpoint/resolver/${entityID}/events?events=0`) - .set('kbn-xsrf', 'xxx') - .expect(400); - await supertest - .post(`/api/endpoint/resolver/${entityID}/events?events=20000`) - .set('kbn-xsrf', 'xxx') - .expect(400); - await supertest - .post(`/api/endpoint/resolver/${entityID}/events?events=-1`) - .set('kbn-xsrf', 'xxx') - .expect(400); - }); - }); - - describe('endpoint events', () => { - it('should not find any events', async () => { - const { body }: { body: SafeResolverRelatedEvents } = await supertest - .post(`/api/endpoint/resolver/5555/events`) + it('should not find any events when given an invalid entity id', async () => { + const { body }: { body: ResolverPaginatedEvents } = await supertest + .post(`/api/endpoint/resolver/events`) .set('kbn-xsrf', 'xxx') + .send({ + filter: 'process.entity_id:"5555"', + }) .expect(200); expect(body.nextEvent).to.eql(null); expect(body.events).to.be.empty(); }); - it('should return details for the root node', async () => { - const { body }: { body: SafeResolverRelatedEvents } = await supertest - .post(`/api/endpoint/resolver/${tree.origin.id}/events`) + it('should return related events for the root node', async () => { + const { body }: { body: ResolverPaginatedEvents } = await supertest + .post(`/api/endpoint/resolver/events`) .set('kbn-xsrf', 'xxx') + .send({ + filter: entityIDFilter, + }) .expect(200); expect(body.events.length).to.eql(4); compareArrays(tree.origin.relatedEvents, body.events, true); @@ -139,9 +266,9 @@ export default function ({ getService }: FtrProviderContext) { }); it('should allow for the events to be filtered', async () => { - const filter = `event.category:"${RelatedEventCategory.Driver}"`; - const { body }: { body: SafeResolverRelatedEvents } = await supertest - .post(`/api/endpoint/resolver/${tree.origin.id}/events`) + const filter = `event.category:"${RelatedEventCategory.Driver}" and ${entityIDFilter}`; + const { body }: { body: ResolverPaginatedEvents } = await supertest + .post(`/api/endpoint/resolver/events`) .set('kbn-xsrf', 'xxx') .send({ filter, @@ -156,38 +283,46 @@ export default function ({ getService }: FtrProviderContext) { }); it('should return paginated results for the root node', async () => { - let { body }: { body: SafeResolverRelatedEvents } = await supertest - .post(`/api/endpoint/resolver/${tree.origin.id}/events?events=2`) + let { body }: { body: ResolverPaginatedEvents } = await supertest + .post(`/api/endpoint/resolver/events?limit=2`) .set('kbn-xsrf', 'xxx') + .send({ + filter: entityIDFilter, + }) .expect(200); expect(body.events.length).to.eql(2); compareArrays(tree.origin.relatedEvents, body.events); expect(body.nextEvent).not.to.eql(null); ({ body } = await supertest - .post( - `/api/endpoint/resolver/${tree.origin.id}/events?events=2&afterEvent=${body.nextEvent}` - ) + .post(`/api/endpoint/resolver/events?limit=2&afterEvent=${body.nextEvent}`) .set('kbn-xsrf', 'xxx') + .send({ + filter: entityIDFilter, + }) .expect(200)); expect(body.events.length).to.eql(2); compareArrays(tree.origin.relatedEvents, body.events); expect(body.nextEvent).to.not.eql(null); ({ body } = await supertest - .post( - `/api/endpoint/resolver/${tree.origin.id}/events?events=2&afterEvent=${body.nextEvent}` - ) + .post(`/api/endpoint/resolver/events?limit=2&afterEvent=${body.nextEvent}`) .set('kbn-xsrf', 'xxx') + .send({ + filter: entityIDFilter, + }) .expect(200)); expect(body.events).to.be.empty(); expect(body.nextEvent).to.eql(null); }); it('should return the first page of information when the cursor is invalid', async () => { - const { body }: { body: SafeResolverRelatedEvents } = await supertest - .post(`/api/endpoint/resolver/${tree.origin.id}/events?afterEvent=blah`) + const { body }: { body: ResolverPaginatedEvents } = await supertest + .post(`/api/endpoint/resolver/events?afterEvent=blah`) .set('kbn-xsrf', 'xxx') + .send({ + filter: entityIDFilter, + }) .expect(200); expect(body.events.length).to.eql(4); compareArrays(tree.origin.relatedEvents, body.events, true); @@ -195,9 +330,12 @@ export default function ({ getService }: FtrProviderContext) { }); it('should sort the events in descending order', async () => { - const { body }: { body: SafeResolverRelatedEvents } = await supertest - .post(`/api/endpoint/resolver/${tree.origin.id}/events`) + const { body }: { body: ResolverPaginatedEvents } = await supertest + .post(`/api/endpoint/resolver/events`) .set('kbn-xsrf', 'xxx') + .send({ + filter: entityIDFilter, + }) .expect(200); expect(body.events.length).to.eql(4); // these events are created in the order they are defined in the array so the newest one is