[Security Solution] Create new events api (#78326)

* Creating new events route

* Trying to get github to recognize the indent change

* Using paginated name for events api return type

* Updating comment

* Updating comment

* Adding deprecated comments

* Adding more comments

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Jonathan Buttner 2020-09-24 13:25:20 -04:00 committed by GitHub
parent d7538a3521
commit 8081a85eae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 477 additions and 166 deletions

View file

@ -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.
*/

View file

@ -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.
*/

View file

@ -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(

View file

@ -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<typeof validateEvents.params>,
unknown,
TypeOf<typeof validateEvents.query>,
TypeOf<typeof validateEvents.body>
> {
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);

View file

@ -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<SafeResolverEvent[]> {
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<SafeResolverEvent[]> {
};
}
formatResponse(response: SearchResponse<SafeResolverEvent>): 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<SafeResolverEvent[]> {
const kqlQuery: JsonObject[] = [];
if (kql) {
kqlQuery.push(esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kql)));
}
const response: ApiResponse<SearchResponse<
SafeResolverEvent
>> = await client.asCurrentUser.search(this.buildSearch(kqlQuery));
return response.body.hits.hits.map((hit) => hit._source);
}
}

View file

@ -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';

View file

@ -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<SafeResolverEvent[]> {
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>): SafeResolverEvent[] {
return this.getResults(response);
}
}

View file

@ -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 validateRelatedEvents.params>,
TypeOf<typeof validateRelatedEvents.query>,
TypeOf<typeof validateRelatedEvents.body>
> {
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 });
}
};
}

View file

@ -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';

View file

@ -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
*

View file

@ -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