Skip to content

Commit

Permalink
[Security Solution] Create new events api (#78326)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
jonathan-buttner and elasticmachine authored Sep 24, 2020
1 parent d7538a3 commit 8081a85
Show file tree
Hide file tree
Showing 11 changed files with 470 additions and 159 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 }),
Expand All @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,86 +4,59 @@
* 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'),
...this.pagination.buildQueryFields('event.id', 'desc'),
};
}

protected query(entityIDs: string[]): JsonObject {
private buildSearch(kql: 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'),
body: this.query(kql),
index: this.indexPattern,
};
}

formatResponse(response: SearchResponse<SafeResolverEvent>): SafeResolverEvent[] {
return this.getResults(response);
/**
* 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading

0 comments on commit 8081a85

Please sign in to comment.