[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:
parent
d7538a3521
commit
8081a85eae
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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 });
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue