[Security Solution][CTI] Event enrichment search strategy (#101553)

* Adding boilerplate for new CTI search strategy type

This is going to be a subtype of the general SecSol search strategy;
the main functionality is going to be:

* transformation of the incoming parameters into named equivalents
* transformation of responses to include enrichment context fields
  (matched.*)

* More boilerplate, including tests

A few type errors because our functions don't actually do anything yet,
nor are our request/response types fleshed out.

* Starting to flesh out the request parsing

* Defines a basic request, along with a mock
* Defines helper function to generate should clauses from field values
* Adds placeholder tests throughout

* Fleshing out unit tests around our enrichment query

* Fleshing out response parsing of eventEnrichment strategy

* Fix types from elasticsearch

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Ryland Herrick 2021-06-15 16:59:50 -05:00 committed by GitHub
parent 3decc35668
commit 4d921ffb7e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 721 additions and 2 deletions

View file

@ -44,3 +44,16 @@ export const SORTED_THREAT_SUMMARY_FIELDS = [
INDICATOR_FIRSTSEEN,
INDICATOR_LASTSEEN,
];
export const EVENT_ENRICHMENT_INDICATOR_FIELD_MAP = {
'file.hash.md5': 'threatintel.indicator.file.hash.md5',
'file.hash.sha1': 'threatintel.indicator.file.hash.sha1',
'file.hash.sha256': 'threatintel.indicator.file.hash.sha256',
'file.pe.imphash': 'threatintel.indicator.file.pe.imphash',
'file.elf.telfhash': 'threatintel.indicator.file.elf.telfhash',
'file.hash.ssdeep': 'threatintel.indicator.file.hash.ssdeep',
'source.ip': 'threatintel.indicator.ip',
'destination.ip': 'threatintel.indicator.ip',
'url.full': 'threatintel.indicator.url.full',
'registry.path': 'threatintel.indicator.registry.path',
};

View file

@ -10,6 +10,8 @@ import { EventEcs } from '../event';
interface ThreatMatchEcs {
atomic?: string[];
field?: string[];
id?: string[];
index?: string[];
type?: string[];
}

View file

@ -0,0 +1,110 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { IEsSearchResponse } from 'src/plugins/data/public';
import {
CtiEventEnrichmentRequestOptions,
CtiEventEnrichmentStrategyResponse,
CtiQueries,
} from '.';
export const buildEventEnrichmentRequestOptionsMock = (
overrides: Partial<CtiEventEnrichmentRequestOptions> = {}
): CtiEventEnrichmentRequestOptions => ({
defaultIndex: ['filebeat-*'],
eventFields: {
'file.hash.md5': '1eee2bf3f56d8abed72da2bc523e7431',
'source.ip': '127.0.0.1',
'url.full': 'elastic.co',
},
factoryQueryType: CtiQueries.eventEnrichment,
filterQuery: '{"bool":{"must":[],"filter":[{"match_all":{}}],"should":[],"must_not":[]}}',
timerange: { interval: '', from: '2020-09-13T09:00:43.249Z', to: '2020-09-14T09:00:43.249Z' },
...overrides,
});
export const buildEventEnrichmentRawResponseMock = (): IEsSearchResponse => ({
rawResponse: {
took: 17,
timed_out: false,
_shards: {
total: 1,
successful: 1,
skipped: 0,
failed: 0,
},
hits: {
total: {
value: 1,
relation: 'eq',
},
max_score: 6.0637846,
hits: [
{
_index: 'filebeat-8.0.0-2021.05.28-000001',
_id: '31408415b6d5601a92d29b86c2519658f210c194057588ae396d55cc20b3f03d',
_score: 6.0637846,
fields: {
'event.category': ['threat'],
'threatintel.indicator.file.type': ['html'],
'related.hash': [
'5529de7b60601aeb36f57824ed0e1ae8',
'15b012e6f626d0f88c2926d2bf4ca394d7b8ee07cc06d2ec05ea76bed3e8a05e',
'768:NXSFGJ/ooP6FawrB7Bo1MWnF/jRmhJImp:1SFXIqBo1Mwj2p',
],
'threatintel.indicator.first_seen': ['2021-05-28T18:33:29.000Z'],
'threatintel.indicator.file.hash.tlsh': [
'FFB20B82F6617061C32784E2712F7A46B179B04FD1EA54A0F28CD8E9CFE4CAA1617F1C',
],
'service.type': ['threatintel'],
'threatintel.indicator.file.hash.ssdeep': [
'768:NXSFGJ/ooP6FawrB7Bo1MWnF/jRmhJImp:1SFXIqBo1Mwj2p',
],
'agent.type': ['filebeat'],
'event.module': ['threatintel'],
'threatintel.indicator.type': ['file'],
'agent.name': ['rylastic.local'],
'threatintel.indicator.file.hash.sha256': [
'15b012e6f626d0f88c2926d2bf4ca394d7b8ee07cc06d2ec05ea76bed3e8a05e',
],
'event.kind': ['enrichment'],
'threatintel.indicator.file.hash.md5': ['5529de7b60601aeb36f57824ed0e1ae8'],
'fileset.name': ['abusemalware'],
'input.type': ['httpjson'],
'agent.hostname': ['rylastic.local'],
tags: ['threatintel-abusemalware', 'forwarded'],
'event.ingested': ['2021-05-28T18:33:55.086Z'],
'@timestamp': ['2021-05-28T18:33:52.993Z'],
'agent.id': ['ff93aee5-86a1-4a61-b0e6-0cdc313d01b5'],
'ecs.version': ['1.6.0'],
'event.reference': [
'https://urlhaus-api.abuse.ch/v1/download/15b012e6f626d0f88c2926d2bf4ca394d7b8ee07cc06d2ec05ea76bed3e8a05e/',
],
'event.type': ['indicator'],
'event.created': ['2021-05-28T18:33:52.993Z'],
'agent.ephemeral_id': ['d6b14f65-5bf3-430d-8315-7b5613685979'],
'threatintel.indicator.file.size': [24738],
'agent.version': ['8.0.0'],
'event.dataset': ['threatintel.abusemalware'],
},
matched_queries: ['file.hash.md5'],
},
],
},
},
});
export const buildEventEnrichmentResponseMock = (
overrides: Partial<CtiEventEnrichmentStrategyResponse> = {}
): CtiEventEnrichmentStrategyResponse => ({
...buildEventEnrichmentRawResponseMock(),
enrichments: [],
inspect: { dsl: ['{"mocked": "json"}'] },
totalCount: 0,
...overrides,
});

View file

@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { IEsSearchResponse } from 'src/plugins/data/public';
import { Inspect } from '../../common';
import { RequestBasicOptions } from '..';
export enum CtiQueries {
eventEnrichment = 'eventEnrichment',
}
export interface CtiEventEnrichmentRequestOptions extends RequestBasicOptions {
eventFields: Record<string, unknown>;
}
export type CtiEnrichment = Record<string, unknown[]>;
export interface CtiEventEnrichmentStrategyResponse extends IEsSearchResponse {
enrichments: CtiEnrichment[];
inspect?: Inspect;
totalCount: number;
}

View file

@ -66,6 +66,11 @@ import {
MatrixHistogramStrategyResponse,
} from './matrix_histogram';
import { TimerangeInput, SortField, PaginationInput, PaginationInputPaginated } from '../common';
import {
CtiEventEnrichmentRequestOptions,
CtiEventEnrichmentStrategyResponse,
CtiQueries,
} from './cti';
export * from './hosts';
export * from './matrix_histogram';
@ -76,6 +81,7 @@ export type FactoryQueryTypes =
| HostsKpiQueries
| NetworkQueries
| NetworkKpiQueries
| CtiQueries
| typeof MatrixHistogramQuery
| typeof MatrixHistogramQueryEntities;
@ -145,6 +151,8 @@ export type StrategyResponseType<T extends FactoryQueryTypes> = T extends HostsQ
? NetworkKpiUniquePrivateIpsStrategyResponse
: T extends typeof MatrixHistogramQuery
? MatrixHistogramStrategyResponse
: T extends CtiQueries.eventEnrichment
? CtiEventEnrichmentStrategyResponse
: never;
export type StrategyRequestType<T extends FactoryQueryTypes> = T extends HostsQueries.hosts
@ -193,6 +201,8 @@ export type StrategyRequestType<T extends FactoryQueryTypes> = T extends HostsQu
? NetworkKpiUniquePrivateIpsRequestOptions
: T extends typeof MatrixHistogramQuery
? MatrixHistogramRequestOptions
: T extends CtiQueries.eventEnrichment
? CtiEventEnrichmentRequestOptions
: never;
export interface DocValueFieldsInput {

View file

@ -0,0 +1,16 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { CtiQueries } from '../../../../../../common/search_strategy/security_solution/cti';
import { SecuritySolutionFactory } from '../../types';
import { buildEventEnrichmentQuery } from './query';
import { parseEventEnrichmentResponse } from './response';
export const eventEnrichment: SecuritySolutionFactory<CtiQueries.eventEnrichment> = {
buildDsl: buildEventEnrichmentQuery,
parse: parseEventEnrichmentResponse,
};

View file

@ -0,0 +1,172 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { buildIndicatorEnrichments, buildIndicatorShouldClauses, getTotalCount } from './helpers';
describe('buildIndicatorShouldClauses', () => {
it('returns an empty array given an empty fieldset', () => {
expect(buildIndicatorShouldClauses({})).toEqual([]);
});
it('returns an empty array given no relevant values', () => {
const eventFields = { 'url.domain': 'elastic.co' };
expect(buildIndicatorShouldClauses(eventFields)).toEqual([]);
});
it('returns a clause for each relevant value', () => {
const eventFields = { 'source.ip': '127.0.0.1', 'url.full': 'elastic.co' };
expect(buildIndicatorShouldClauses(eventFields)).toHaveLength(2);
});
it('excludes non-CTI fields', () => {
const eventFields = { 'source.ip': '127.0.0.1', 'url.domain': 'elastic.co' };
expect(buildIndicatorShouldClauses(eventFields)).toHaveLength(1);
});
it('defines a named query where the name is the event field and the value is the event field value', () => {
const eventFields = { 'file.hash.md5': '1eee2bf3f56d8abed72da2bc523e7431' };
expect(buildIndicatorShouldClauses(eventFields)).toContainEqual({
match: {
'threatintel.indicator.file.hash.md5': {
_name: 'file.hash.md5',
query: '1eee2bf3f56d8abed72da2bc523e7431',
},
},
});
});
it('returns valid queries for multiple valid fields', () => {
const eventFields = { 'source.ip': '127.0.0.1', 'url.full': 'elastic.co' };
expect(buildIndicatorShouldClauses(eventFields)).toEqual(
expect.arrayContaining([
{ match: { 'threatintel.indicator.ip': { _name: 'source.ip', query: '127.0.0.1' } } },
{ match: { 'threatintel.indicator.url.full': { _name: 'url.full', query: 'elastic.co' } } },
])
);
});
});
describe('getTotalCount', () => {
it('returns 0 when total is null (not tracking)', () => {
expect(getTotalCount(null)).toEqual(0);
});
it('returns total when total is a number', () => {
expect(getTotalCount(5)).toEqual(5);
});
it('returns total.value when total is an object', () => {
expect(getTotalCount({ value: 20, relation: 'eq' })).toEqual(20);
});
});
describe('buildIndicatorEnrichments', () => {
it('returns nothing if hits have no matched queries', () => {
const hits = [{ _id: '_id', _index: '_index', matched_queries: [] }];
expect(buildIndicatorEnrichments(hits)).toEqual([]);
});
it("returns nothing if hits' matched queries are not valid", () => {
const hits = [{ _id: '_id', _index: '_index', matched_queries: ['invalid.field'] }];
expect(buildIndicatorEnrichments(hits)).toEqual([]);
});
it('builds a single enrichment if the hit has a matched query', () => {
const hits = [
{
_id: '_id',
_index: '_index',
matched_queries: ['file.hash.md5'],
fields: {
'threatintel.indicator.file.hash.md5': ['indicator_value'],
},
},
];
expect(buildIndicatorEnrichments(hits)).toEqual([
expect.objectContaining({
'matched.atomic': ['indicator_value'],
'matched.field': ['file.hash.md5'],
'matched.id': ['_id'],
'matched.index': ['_index'],
'threatintel.indicator.file.hash.md5': ['indicator_value'],
}),
]);
});
it('builds multiple enrichments if the hit has matched queries', () => {
const hits = [
{
_id: '_id',
_index: '_index',
matched_queries: ['file.hash.md5', 'source.ip'],
fields: {
'threatintel.indicator.file.hash.md5': ['indicator_value'],
'threatintel.indicator.ip': ['127.0.0.1'],
},
},
];
expect(buildIndicatorEnrichments(hits)).toEqual([
expect.objectContaining({
'matched.atomic': ['indicator_value'],
'matched.field': ['file.hash.md5'],
'matched.id': ['_id'],
'matched.index': ['_index'],
'threatintel.indicator.file.hash.md5': ['indicator_value'],
'threatintel.indicator.ip': ['127.0.0.1'],
}),
expect.objectContaining({
'matched.atomic': ['127.0.0.1'],
'matched.field': ['source.ip'],
'matched.id': ['_id'],
'matched.index': ['_index'],
'threatintel.indicator.file.hash.md5': ['indicator_value'],
'threatintel.indicator.ip': ['127.0.0.1'],
}),
]);
});
it('builds an enrichment for each hit', () => {
const hits = [
{
_id: '_id',
_index: '_index',
matched_queries: ['file.hash.md5'],
fields: {
'threatintel.indicator.file.hash.md5': ['indicator_value'],
},
},
{
_id: '_id2',
_index: '_index2',
matched_queries: ['source.ip'],
fields: {
'threatintel.indicator.ip': ['127.0.0.1'],
},
},
];
expect(buildIndicatorEnrichments(hits)).toEqual([
expect.objectContaining({
'matched.atomic': ['indicator_value'],
'matched.field': ['file.hash.md5'],
'matched.id': ['_id'],
'matched.index': ['_index'],
'threatintel.indicator.file.hash.md5': ['indicator_value'],
}),
expect.objectContaining({
'matched.atomic': ['127.0.0.1'],
'matched.field': ['source.ip'],
'matched.id': ['_id2'],
'matched.index': ['_index2'],
'threatintel.indicator.ip': ['127.0.0.1'],
}),
]);
});
});

View file

@ -0,0 +1,83 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { get, isEmpty } from 'lodash';
import { estypes } from '@elastic/elasticsearch';
import { EVENT_ENRICHMENT_INDICATOR_FIELD_MAP } from '../../../../../../common/cti/constants';
import { CtiEnrichment } from '../../../../../../common/search_strategy/security_solution/cti';
type EventField = keyof typeof EVENT_ENRICHMENT_INDICATOR_FIELD_MAP;
const validEventFields = Object.keys(EVENT_ENRICHMENT_INDICATOR_FIELD_MAP) as EventField[];
const isValidEventField = (field: string): field is EventField =>
validEventFields.includes(field as EventField);
export const buildIndicatorShouldClauses = (
eventFields: Record<string, unknown>
): estypes.QueryDslQueryContainer[] => {
return validEventFields.reduce<estypes.QueryDslQueryContainer[]>((shoulds, eventField) => {
const eventFieldValue = eventFields[eventField];
if (!isEmpty(eventFieldValue)) {
shoulds.push({
match: {
[EVENT_ENRICHMENT_INDICATOR_FIELD_MAP[eventField]]: {
query: eventFieldValue,
_name: eventField,
},
},
});
}
return shoulds;
}, []);
};
export const buildIndicatorEnrichments = (hits: estypes.SearchHit[]): CtiEnrichment[] => {
return hits.flatMap<CtiEnrichment>(({ matched_queries: matchedQueries, ...hit }) => {
return (
matchedQueries?.reduce<CtiEnrichment[]>((enrichments, matchedQuery) => {
if (isValidEventField(matchedQuery)) {
enrichments.push({
...hit.fields,
...buildIndicatorMatchedFields(hit, matchedQuery),
});
}
return enrichments;
}, []) ?? []
);
});
};
const buildIndicatorMatchedFields = (
hit: estypes.SearchHit,
eventField: EventField
): Record<string, unknown[]> => {
const indicatorField = EVENT_ENRICHMENT_INDICATOR_FIELD_MAP[eventField];
const atomic = get(hit.fields, indicatorField) as string[];
return {
'matched.atomic': atomic,
'matched.field': [eventField],
'matched.id': [hit._id],
'matched.index': [hit._index],
};
};
export const getTotalCount = (total: number | estypes.SearchTotalHits | null): number => {
if (total == null) {
return 0;
}
if (typeof total === 'number') {
return total;
}
return total.value;
};

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { eventEnrichment } from './factory';

View file

@ -0,0 +1,90 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { buildEventEnrichmentRequestOptionsMock } from '../../../../../../common/search_strategy/security_solution/cti/index.mock';
import { buildEventEnrichmentQuery } from './query';
describe('buildEventEnrichmentQuery', () => {
it('converts each event field/value into a named filter', () => {
const options = buildEventEnrichmentRequestOptionsMock();
const query = buildEventEnrichmentQuery(options);
expect(query.body?.query?.bool?.should).toEqual(
expect.arrayContaining([
{
match: {
'threatintel.indicator.file.hash.md5': {
_name: 'file.hash.md5',
query: '1eee2bf3f56d8abed72da2bc523e7431',
},
},
},
{ match: { 'threatintel.indicator.ip': { _name: 'source.ip', query: '127.0.0.1' } } },
{ match: { 'threatintel.indicator.url.full': { _name: 'url.full', query: 'elastic.co' } } },
])
);
});
it('filters on indicator events', () => {
const options = buildEventEnrichmentRequestOptionsMock();
const query = buildEventEnrichmentQuery(options);
expect(query.body?.query?.bool?.filter).toEqual(
expect.arrayContaining([{ term: { 'event.type': 'indicator' } }])
);
});
it('includes the specified timerange', () => {
const options = buildEventEnrichmentRequestOptionsMock();
const query = buildEventEnrichmentQuery(options);
expect(query.body?.query?.bool?.filter).toEqual(
expect.arrayContaining([
{
range: {
'@timestamp': {
format: 'strict_date_optional_time',
gte: '2020-09-13T09:00:43.249Z',
lte: '2020-09-14T09:00:43.249Z',
},
},
},
])
);
});
it('includes specified docvalue_fields', () => {
const docValueFields = [
{ field: '@timestamp', format: 'date_time' },
{ field: 'event.created', format: 'date_time' },
{ field: 'event.end', format: 'date_time' },
];
const options = buildEventEnrichmentRequestOptionsMock({ docValueFields });
const query = buildEventEnrichmentQuery(options);
expect(query.body?.docvalue_fields).toEqual(expect.arrayContaining(docValueFields));
});
it('requests all fields', () => {
const options = buildEventEnrichmentRequestOptionsMock();
const query = buildEventEnrichmentQuery(options);
expect(query.body?.fields).toEqual(['*']);
});
it('excludes _source', () => {
const options = buildEventEnrichmentRequestOptionsMock();
const query = buildEventEnrichmentQuery(options);
expect(query.body?._source).toEqual(false);
});
it('includes specified filters', () => {
const filterQuery = {
query: 'query_field: query_value',
language: 'kuery',
};
const options = buildEventEnrichmentRequestOptionsMock({ filterQuery });
const query = buildEventEnrichmentQuery(options);
expect(query.body?.query?.bool?.filter).toEqual(expect.arrayContaining([filterQuery]));
});
});

View file

@ -0,0 +1,52 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { isEmpty } from 'lodash';
import { CtiQueries } from '../../../../../../common/search_strategy/security_solution/cti';
import { createQueryFilterClauses } from '../../../../../utils/build_query';
import { SecuritySolutionFactory } from '../../types';
import { buildIndicatorShouldClauses } from './helpers';
export const buildEventEnrichmentQuery: SecuritySolutionFactory<CtiQueries.eventEnrichment>['buildDsl'] = ({
defaultIndex,
docValueFields,
eventFields,
filterQuery,
timerange: { from, to },
}) => {
const filter = [
...createQueryFilterClauses(filterQuery),
{ term: { 'event.type': 'indicator' } },
{
range: {
'@timestamp': {
gte: from,
lte: to,
format: 'strict_date_optional_time',
},
},
},
];
return {
allowNoIndices: true,
ignoreUnavailable: true,
index: defaultIndex,
body: {
_source: false,
...(!isEmpty(docValueFields) && { docvalue_fields: docValueFields }),
fields: ['*'],
query: {
bool: {
should: buildIndicatorShouldClauses(eventFields),
filter,
minimum_should_match: 1,
},
},
},
};
};

View file

@ -0,0 +1,89 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
buildEventEnrichmentRequestOptionsMock,
buildEventEnrichmentRawResponseMock,
} from '../../../../../../common/search_strategy/security_solution/cti/index.mock';
import { parseEventEnrichmentResponse } from './response';
describe('parseEventEnrichmentResponse', () => {
it('includes an accurate inspect response', async () => {
const options = buildEventEnrichmentRequestOptionsMock();
const response = buildEventEnrichmentRawResponseMock();
const parsedResponse = await parseEventEnrichmentResponse(options, response);
const expectedInspect = expect.objectContaining({
allowNoIndices: true,
body: {
_source: false,
fields: ['*'],
query: {
bool: {
filter: [
{ bool: { filter: [{ match_all: {} }], must: [], must_not: [], should: [] } },
{ term: { 'event.type': 'indicator' } },
{
range: {
'@timestamp': {
format: 'strict_date_optional_time',
gte: '2020-09-13T09:00:43.249Z',
lte: '2020-09-14T09:00:43.249Z',
},
},
},
],
minimum_should_match: 1,
should: [
{
match: {
'threatintel.indicator.file.hash.md5': {
_name: 'file.hash.md5',
query: '1eee2bf3f56d8abed72da2bc523e7431',
},
},
},
{ match: { 'threatintel.indicator.ip': { _name: 'source.ip', query: '127.0.0.1' } } },
{
match: {
'threatintel.indicator.url.full': { _name: 'url.full', query: 'elastic.co' },
},
},
],
},
},
},
ignoreUnavailable: true,
index: ['filebeat-*'],
});
const parsedInspect = JSON.parse(parsedResponse.inspect!.dsl[0]);
expect(parsedInspect).toEqual(expectedInspect);
});
it('includes an accurate total count', async () => {
const options = buildEventEnrichmentRequestOptionsMock();
const response = buildEventEnrichmentRawResponseMock();
const parsedResponse = await parseEventEnrichmentResponse(options, response);
expect(parsedResponse.totalCount).toEqual(1);
});
it('adds matched.* enrichment fields based on the named query', async () => {
const options = buildEventEnrichmentRequestOptionsMock();
const response = buildEventEnrichmentRawResponseMock();
const parsedResponse = await parseEventEnrichmentResponse(options, response);
expect(parsedResponse.enrichments).toEqual([
expect.objectContaining({
'matched.atomic': ['5529de7b60601aeb36f57824ed0e1ae8'],
'matched.field': ['file.hash.md5'],
'matched.id': ['31408415b6d5601a92d29b86c2519658f210c194057588ae396d55cc20b3f03d'],
'matched.index': ['filebeat-8.0.0-2021.05.28-000001'],
}),
]);
});
});

View file

@ -0,0 +1,31 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { CtiQueries } from '../../../../../../common/search_strategy/security_solution/cti';
import { inspectStringifyObject } from '../../../../../utils/build_query';
import { SecuritySolutionFactory } from '../../types';
import { buildIndicatorEnrichments, getTotalCount } from './helpers';
import { buildEventEnrichmentQuery } from './query';
export const parseEventEnrichmentResponse: SecuritySolutionFactory<CtiQueries.eventEnrichment>['parse'] = async (
options,
response,
deps
) => {
const inspect = {
dsl: [inspectStringifyObject(buildEventEnrichmentQuery(options))],
};
const totalCount = getTotalCount(response.rawResponse.hits.total);
const enrichments = buildIndicatorEnrichments(response.rawResponse.hits.hits);
return {
...response,
enrichments,
inspect,
totalCount,
};
};

View file

@ -0,0 +1,15 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { FactoryQueryTypes } from '../../../../../common/search_strategy/security_solution';
import { CtiQueries } from '../../../../../common/search_strategy/security_solution/cti';
import type { SecuritySolutionFactory } from '../types';
import { eventEnrichment } from './event_enrichment';
export const ctiFactoryTypes: Record<CtiQueries, SecuritySolutionFactory<FactoryQueryTypes>> = {
[CtiQueries.eventEnrichment]: eventEnrichment,
};

View file

@ -5,12 +5,13 @@
* 2.0.
*/
import { FactoryQueryTypes } from '../../../../common/search_strategy/security_solution';
import type { FactoryQueryTypes } from '../../../../common/search_strategy/security_solution';
import type { SecuritySolutionFactory } from './types';
import { hostsFactory } from './hosts';
import { matrixHistogramFactory } from './matrix_histogram';
import { networkFactory } from './network';
import { SecuritySolutionFactory } from './types';
import { ctiFactoryTypes } from './cti';
export const securitySolutionFactory: Record<
FactoryQueryTypes,
@ -19,4 +20,5 @@ export const securitySolutionFactory: Record<
...hostsFactory,
...matrixHistogramFactory,
...networkFactory,
...ctiFactoryTypes,
};