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][Resolver] Add support for predefined schemas for endpoint and winlogbeat #84103

Merged
Show file tree
Hide file tree
Changes from all 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
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([
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe add a TODO to dedupe the code in these mocks at some point

{
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