Skip to content

Commit

Permalink
[Security Solution][Resolver] Add support for predefined schemas for …
Browse files Browse the repository at this point in the history
…endpoint and winlogbeat (#84103)

* Refactoring entity route to return schema

* Refactoring frontend middleware to pick off id field from entity route

* Refactoring schema and adding name and comments

* Adding name to schema mocks

* Fixing type issue
  • Loading branch information
jonathan-buttner authored Nov 25, 2020
1 parent 4975112 commit 5fda300
Show file tree
Hide file tree
Showing 17 changed files with 259 additions and 125 deletions.
39 changes: 38 additions & 1 deletion x-pack/plugins/security_solution/common/endpoint/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -870,10 +870,47 @@ export interface SafeLegacyEndpointEvent {
}>;
}

/**
* The fields to use to identify nodes within a resolver tree.
*/
export interface ResolverSchema {
/**
* the ancestry field should be set to a field that contains an order array representing
* the ancestors of a node.
*/
ancestry?: string;
/**
* id represents the field to use as the unique ID for a node.
*/
id: string;
/**
* field to use for the name of the node
*/
name?: string;
/**
* parent represents the field that is the edge between two nodes.
*/
parent: string;
}

/**
* The response body for the resolver '/entity' index API
*/
export type ResolverEntityIndex = Array<{ entity_id: string }>;
export type ResolverEntityIndex = Array<{
/**
* A name for the schema that is being used (e.g. endpoint, winlogbeat, etc)
*/
name: string;
/**
* The schema to pass to the /tree api and other backend requests, based on the contents of the document found using
* the _id
*/
schema: ResolverSchema;
/**
* Unique ID value for the requested document using the `_id` field passed to the /entity route
*/
id: string;
}>;

/**
* Takes a @kbn/config-schema 'schema' type and returns a type that represents valid inputs.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,18 @@ export function noAncestorsTwoChildren(): { dataAccessLayer: DataAccessLayer; me
* Get entities matching a document.
*/
entities(): Promise<ResolverEntityIndex> {
return Promise.resolve([{ entity_id: metadata.entityIDs.origin }]);
return Promise.resolve([
{
name: 'endpoint',
schema: {
id: 'process.entity_id',
parent: 'process.parent.entity_id',
ancestry: 'process.Ext.ancestry',
name: 'process.name',
},
id: metadata.entityIDs.origin,
},
]);
},
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,18 @@ export function noAncestorsTwoChildenInIndexCalledAwesomeIndex(): {
entities({ indices }): Promise<ResolverEntityIndex> {
// Only return values if the `indices` array contains exactly `'awesome_index'`
if (indices.length === 1 && indices[0] === 'awesome_index') {
return Promise.resolve([{ entity_id: metadata.entityIDs.origin }]);
return Promise.resolve([
{
name: 'endpoint',
schema: {
id: 'process.entity_id',
parent: 'process.parent.entity_id',
ancestry: 'process.Ext.ancestry',
name: 'process.name',
},
id: metadata.entityIDs.origin,
},
]);
}
return Promise.resolve([]);
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,18 @@ export function noAncestorsTwoChildrenWithRelatedEventsOnOriginWithOneAfterCurso
* Get entities matching a document.
*/
async entities(): Promise<ResolverEntityIndex> {
return [{ entity_id: metadata.entityIDs.origin }];
return [
{
name: 'endpoint',
schema: {
id: 'process.entity_id',
parent: 'process.parent.entity_id',
ancestry: 'process.Ext.ancestry',
name: 'process.name',
},
id: metadata.entityIDs.origin,
},
];
},
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,18 @@ export function noAncestorsTwoChildrenWithRelatedEventsOnOrigin(): {
* Get entities matching a document.
*/
async entities(): Promise<ResolverEntityIndex> {
return [{ entity_id: metadata.entityIDs.origin }];
return [
{
name: 'endpoint',
schema: {
id: 'process.entity_id',
parent: 'process.parent.entity_id',
ancestry: 'process.Ext.ancestry',
name: 'process.name',
},
id: metadata.entityIDs.origin,
},
];
},
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,18 @@ export function oneNodeWithPaginatedEvents(): {
* Get entities matching a document.
*/
async entities(): Promise<ResolverEntityIndex> {
return [{ entity_id: metadata.entityIDs.origin }];
return [
{
name: 'endpoint',
schema: {
id: 'process.entity_id',
parent: 'process.parent.entity_id',
ancestry: 'process.Ext.ancestry',
name: 'process.name',
},
id: metadata.entityIDs.origin,
},
];
},
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export function ResolverTreeFetcher(
});
return;
}
const entityIDToFetch = matchingEntities[0].entity_id;
const entityIDToFetch = matchingEntities[0].id;
result = await dataAccessLayer.resolverTree(
entityIDToFetch,
lastRequestAbortController.signal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,70 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { RequestHandler } from 'kibana/server';
import _ from 'lodash';
import { RequestHandler, SearchResponse } from 'kibana/server';
import { TypeOf } from '@kbn/config-schema';
import { ApiResponse } from '@elastic/elasticsearch';
import { validateEntities } from '../../../../common/endpoint/schema/resolver';
import { ResolverEntityIndex } from '../../../../common/endpoint/types';
import { ResolverEntityIndex, ResolverSchema } from '../../../../common/endpoint/types';

interface SupportedSchema {
/**
* A name for the schema being used
*/
name: string;

/**
* A constraint to search for in the documented returned by Elasticsearch
*/
constraint: { field: string; value: string };

/**
* Schema to return to the frontend so that it can be passed in to call to the /tree API
*/
schema: ResolverSchema;
}

/**
* This structure defines the preset supported schemas for a resolver graph. We'll probably want convert this
* implementation to something similar to how row renderers is implemented.
*/
const supportedSchemas: SupportedSchema[] = [
{
name: 'endpoint',
constraint: {
field: 'agent.type',
value: 'endpoint',
},
schema: {
id: 'process.entity_id',
parent: 'process.parent.entity_id',
ancestry: 'process.Ext.ancestry',
name: 'process.name',
},
},
{
name: 'winlogbeat',
constraint: {
field: 'agent.type',
value: 'winlogbeat',
},
schema: {
id: 'process.entity_id',
parent: 'process.parent.entity_id',
name: 'process.name',
},
},
];

function getFieldAsString(doc: unknown, field: string): string | undefined {
const value = _.get(doc, field);
if (value === undefined) {
return undefined;
}

return String(value);
}

/**
* This is used to get an 'entity_id' which is an internal-to-Resolver concept, from an `_id`, which
Expand All @@ -18,61 +78,46 @@ export function handleEntities(): RequestHandler<unknown, TypeOf<typeof validate
query: { _id, indices },
} = request;

/**
* A safe type for the response based on the semantics of the query.
* We specify _source, asking for `process.entity_id` and we only
* accept documents that have it.
* Also, we only request 1 document.
*/
interface ExpectedQueryResponse {
hits: {
hits:
| []
| [
const queryResponse: ApiResponse<
SearchResponse<unknown>
> = await context.core.elasticsearch.client.asCurrentUser.search({
ignore_unavailable: true,
index: indices,
body: {
// only return 1 match at most
size: 1,
query: {
bool: {
filter: [
{
_source: {
process?: {
entity_id?: string;
};
};
}
];
};
}

const queryResponse: ExpectedQueryResponse = await context.core.elasticsearch.legacy.client.callAsCurrentUser(
'search',
{
ignoreUnavailable: true,
index: indices,
body: {
// only return process.entity_id
_source: 'process.entity_id',
// only return 1 match at most
size: 1,
query: {
bool: {
filter: [
{
// only return documents with the matching _id
ids: {
values: _id,
},
// only return documents with the matching _id
ids: {
values: _id,
},
],
},
},
],
},
},
}
);
},
});

const responseBody: ResolverEntityIndex = [];
for (const hit of queryResponse.hits.hits) {
// check that the field is defined and that is not an empty string
if (hit._source.process?.entity_id) {
responseBody.push({
entity_id: hit._source.process.entity_id,
});
for (const hit of queryResponse.body.hits.hits) {
for (const supportedSchema of supportedSchemas) {
const fieldValue = getFieldAsString(hit._source, supportedSchema.constraint.field);
const id = getFieldAsString(hit._source, supportedSchema.schema.id);
// check that the constraint and id fields are defined and that the id field is not an empty string
if (
fieldValue?.toLowerCase() === supportedSchema.constraint.value.toLowerCase() &&
id !== undefined &&
id !== ''
) {
responseBody.push({
name: supportedSchema.name,
schema: supportedSchema.schema,
id,
});
}
}
}
return response.ok({ body: responseBody });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
import { SearchResponse } from 'elasticsearch';
import { ApiResponse } from '@elastic/elasticsearch';
import { IScopedClusterClient } from 'src/core/server';
import { FieldsObject } from '../../../../../../common/endpoint/types';
import { FieldsObject, ResolverSchema } from '../../../../../../common/endpoint/types';
import { JsonObject, JsonValue } from '../../../../../../../../../src/plugins/kibana_utils/common';
import { NodeID, Schema, Timerange, docValueFields } from '../utils/index';
import { NodeID, Timerange, docValueFields } from '../utils/index';

interface DescendantsParams {
schema: Schema;
schema: ResolverSchema;
indexPatterns: string | string[];
timerange: Timerange;
}
Expand All @@ -20,7 +20,7 @@ interface DescendantsParams {
* Builds a query for retrieving descendants of a node.
*/
export class DescendantsQuery {
private readonly schema: Schema;
private readonly schema: ResolverSchema;
private readonly indexPatterns: string | string[];
private readonly timerange: Timerange;
private readonly docValueFields: JsonValue[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
import { SearchResponse } from 'elasticsearch';
import { ApiResponse } from '@elastic/elasticsearch';
import { IScopedClusterClient } from 'src/core/server';
import { FieldsObject } from '../../../../../../common/endpoint/types';
import { FieldsObject, ResolverSchema } from '../../../../../../common/endpoint/types';
import { JsonObject, JsonValue } from '../../../../../../../../../src/plugins/kibana_utils/common';
import { NodeID, Schema, Timerange, docValueFields } from '../utils/index';
import { NodeID, Timerange, docValueFields } from '../utils/index';

interface LifecycleParams {
schema: Schema;
schema: ResolverSchema;
indexPatterns: string | string[];
timerange: Timerange;
}
Expand All @@ -20,7 +20,7 @@ interface LifecycleParams {
* Builds a query for retrieving descendants of a node.
*/
export class LifecycleQuery {
private readonly schema: Schema;
private readonly schema: ResolverSchema;
private readonly indexPatterns: string | string[];
private readonly timerange: Timerange;
private readonly docValueFields: JsonValue[];
Expand Down
Loading

0 comments on commit 5fda300

Please sign in to comment.