From e774ab491412944a08bff8d25c5b9654afb766a1 Mon Sep 17 00:00:00 2001 From: Khristinin Nikita Date: Fri, 28 Jan 2022 09:32:04 +0100 Subject: [PATCH] Stop IM rule execution if there are no events (#123811) * Add event count check * Fix linter * Make tuple required --- .../threat_mapping/create_threat_signals.ts | 19 +++ .../threat_mapping/get_event_count.test.ts | 132 ++++++++++++++++++ .../signals/threat_mapping/get_event_count.ts | 38 +++++ .../signals/threat_mapping/types.ts | 11 ++ 4 files changed, 200 insertions(+) create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_event_count.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_event_count.ts diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts index 3728ff840db59..93eba0709a0d1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts @@ -13,6 +13,7 @@ import { createThreatSignal } from './create_threat_signal'; import { SearchAfterAndBulkCreateReturnType } from '../types'; import { buildExecutionIntervalValidator, combineConcurrentResults } from './utils'; import { buildThreatEnrichment } from './build_threat_enrichment'; +import { getEventCount } from './get_event_count'; export const createThreatSignals = async ({ alertId, @@ -62,6 +63,23 @@ export const createThreatSignals = async ({ warningMessages: [], }; + const eventCount = await getEventCount({ + esClient: services.scopedClusterClient.asCurrentUser, + index: inputIndex, + exceptionItems, + tuple, + query, + language, + filters, + }); + + logger.debug(`Total event count: ${eventCount}`); + + if (eventCount === 0) { + logger.debug(buildRuleMessage('Indicator matching rule has completed')); + return results; + } + let threatListCount = await getThreatListCount({ esClient: services.scopedClusterClient.asCurrentUser, exceptionItems, @@ -70,6 +88,7 @@ export const createThreatSignals = async ({ language: threatLanguage, index: threatIndex, }); + logger.debug(buildRuleMessage(`Total indicator items: ${threatListCount}`)); let threatList = await getThreatList({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_event_count.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_event_count.test.ts new file mode 100644 index 0000000000000..bd10c4817e6dc --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_event_count.test.ts @@ -0,0 +1,132 @@ +/* + * 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 moment from 'moment'; +import { elasticsearchServiceMock } from 'src/core/server/mocks'; +import { getEventCount } from './get_event_count'; + +describe('getEventCount', () => { + const esClient = elasticsearchServiceMock.createElasticsearchClient(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('can respect tuple', () => { + getEventCount({ + esClient, + query: '*:*', + language: 'kuery', + filters: [], + exceptionItems: [], + index: ['test-index'], + tuple: { to: moment('2022-01-14'), from: moment('2022-01-13'), maxSignals: 1337 }, + }); + + expect(esClient.count).toHaveBeenCalledWith({ + body: { + query: { + bool: { + filter: [ + { bool: { must: [], filter: [], should: [], must_not: [] } }, + { + bool: { + filter: [ + { + bool: { + should: [ + { + range: { + '@timestamp': { + lte: '2022-01-14T05:00:00.000Z', + gte: '2022-01-13T05:00:00.000Z', + format: 'strict_date_optional_time', + }, + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + { match_all: {} }, + ], + }, + }, + }, + ignore_unavailable: true, + index: ['test-index'], + }); + }); + + it('can override timestamp', () => { + getEventCount({ + esClient, + query: '*:*', + language: 'kuery', + filters: [], + exceptionItems: [], + index: ['test-index'], + tuple: { to: moment('2022-01-14'), from: moment('2022-01-13'), maxSignals: 1337 }, + timestampOverride: 'event.ingested', + }); + + expect(esClient.count).toHaveBeenCalledWith({ + body: { + query: { + bool: { + filter: [ + { bool: { must: [], filter: [], should: [], must_not: [] } }, + { + bool: { + filter: [ + { + bool: { + should: [ + { + range: { + 'event.ingested': { + lte: '2022-01-14T05:00:00.000Z', + gte: '2022-01-13T05:00:00.000Z', + format: 'strict_date_optional_time', + }, + }, + }, + { + bool: { + filter: [ + { + range: { + '@timestamp': { + lte: '2022-01-14T05:00:00.000Z', + gte: '2022-01-13T05:00:00.000Z', + format: 'strict_date_optional_time', + }, + }, + }, + { bool: { must_not: { exists: { field: 'event.ingested' } } } }, + ], + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + { match_all: {} }, + ], + }, + }, + }, + ignore_unavailable: true, + index: ['test-index'], + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_event_count.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_event_count.ts new file mode 100644 index 0000000000000..1833491851831 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_event_count.ts @@ -0,0 +1,38 @@ +/* + * 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 { EventCountOptions } from './types'; +import { getQueryFilter } from '../../../../../common/detection_engine/get_query_filter'; +import { buildEventsSearchQuery } from '../build_events_query'; + +export const getEventCount = async ({ + esClient, + query, + language, + filters, + index, + exceptionItems, + tuple, + timestampOverride, +}: EventCountOptions): Promise => { + const filter = getQueryFilter(query, language ?? 'kuery', filters, index, exceptionItems); + const eventSearchQueryBodyQuery = buildEventsSearchQuery({ + index, + from: tuple.from.toISOString(), + to: tuple.to.toISOString(), + filter, + size: 0, + timestampOverride, + searchAfterSortIds: undefined, + }).body.query; + const { body: response } = await esClient.count({ + body: { query: eventSearchQueryBodyQuery }, + ignore_unavailable: true, + index, + }); + return response.count; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts index bf710cde93fe6..1f12b60e09628 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts @@ -203,3 +203,14 @@ export interface BuildThreatEnrichmentOptions { threatLanguage: ThreatLanguageOrUndefined; threatQuery: ThreatQuery; } + +export interface EventCountOptions { + esClient: ElasticsearchClient; + exceptionItems: ExceptionListItemSchema[]; + index: string[]; + language: ThreatLanguageOrUndefined; + query: string; + filters: unknown[]; + tuple: RuleRangeTuple; + timestampOverride?: string; +}