Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution] Create new events api #78326

Merged
merged 8 commits into from
Sep 24, 2020
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Copy link
Contributor

Choose a reason for hiding this comment

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

When nextEvent is null it means there are no more events, right? Maybe should add that to comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I'll add it 👍

* `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
Copy link
Contributor

Choose a reason for hiding this comment

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

We can just tag all these as @deprecated. Easier to do a find all and remove later on

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah good call 👍

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

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

export function handleEvents(
Copy link
Contributor

Choose a reason for hiding this comment

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

❔ Just noticed this needs a doc comment

log: Logger,
endpointAppContext: EndpointAppContext
log: Logger
): RequestHandler<
TypeOf<typeof validateEvents.params>,
unknown,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

No params are used for the new events route.

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';
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is really a new file for the new /resolver/events route. It does not use the base class like the other queries that are tied to a particular entity_id.

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,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do we prefer that this new /events route filters out event.category:process like the previous route or includes them by default and leaves it up to the passed in kql filter to do it?

In the current state, to find non-process events for a specific entity_id we'd pass something like this: process.entity_id:"1234" and not event.category:"process" as the filter

Copy link
Contributor

Choose a reason for hiding this comment

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

I prefer this. Better to surface everything and let the filtering logic be decided on the front end

{
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 @@
/*
Copy link
Contributor Author

@jonathan-buttner jonathan-buttner Sep 23, 2020

Choose a reason for hiding this comment

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

This file was really a move: event.ts -> related_events.ts

Copy link
Contributor

Choose a reason for hiding this comment

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

The files moved to their related_events.ts counterparts are essentially deprecated right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's correct. I wanted the events.ts file to have the new implementation since that name seemed most appropriate.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sweet, good to know. Thanks. We can probably @deprecated these files

* 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