-
Notifications
You must be signed in to change notification settings - Fork 8.2k
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
Changes from 6 commits
b1c275b
8fa3d1b
2e3ae2e
7938cf2
2cf7dcd
7bfccf7
aa9b7b8
5fae08d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can just tag all these as There was a problem hiding this comment. Choose a reason for hiding this commentThe 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( | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is really a new file for the new |
||
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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we prefer that this new In the current state, to find non-process events for a specific entity_id we'd pass something like this: There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
---|---|---|
@@ -0,0 +1,92 @@ | ||
/* | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This file was really a move: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The files moved to their There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's correct. I wanted the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sweet, good to know. Thanks. We can probably |
||
* 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); | ||
} | ||
} |
There was a problem hiding this comment.
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.There was a problem hiding this comment.
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 👍