From c555d0fd2f4735e4ee7a98676606a7bfcd3b0894 Mon Sep 17 00:00:00 2001 From: Devin Hurley Date: Fri, 1 Nov 2019 18:16:24 -0400 Subject: [PATCH] tests for detection engine get/put utils --- .../alerts/__mocks__/es_results.ts | 93 ++++++ .../alerts/build_events_query.test.ts | 144 ++++++++++ .../alerts/build_events_query.ts | 2 +- .../lib/detection_engine/alerts/utils.test.ts | 269 ++++++++++++++++++ .../lib/detection_engine/alerts/utils.ts | 23 +- 5 files changed, 527 insertions(+), 4 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts new file mode 100644 index 000000000000000..a2a8805eb9c24e6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts @@ -0,0 +1,93 @@ +/* + * 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. + */ + +import { SignalSourceHit, SignalSearchResponse, SignalAlertParams, BulkResponse } from '../types'; + +export const sampleSignalAlertParams = (): SignalAlertParams => ({ + id: 'rule-1', + description: 'Detecting root and admin users', + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + name: 'Detect Root/Admin Users', + type: 'query', + from: 'now-6m', + to: 'now', + severity: 'high', + query: 'user.name: root or user.name: admin', + language: 'kuery', + references: ['http://google.com'], + maxSignals: 100, + enabled: true, + filter: undefined, + filters: undefined, + savedId: undefined, + size: undefined, +}); + +export const sampleDocNoSortId: SignalSourceHit = { + _index: 'myFakeSignalIndex', + _type: 'doc', + _score: 100, + _version: 1, + _id: 'someFakeId', + _source: { + someKey: 'someValue', + '@timestamp': 'someTimeStamp', + }, +}; + +export const sampleDocWithSortId: SignalSourceHit = { + _index: 'myFakeSignalIndex', + _type: 'doc', + _score: 100, + _version: 1, + _id: 'someFakeId', + _source: { + someKey: 'someValue', + '@timestamp': 'someTimeStamp', + }, + sort: ['1234567891111'], +}; + +export const sampleDocSearchResultsNoSortId: SignalSearchResponse = { + took: 10, + timed_out: false, + _shards: { + total: 10, + successful: 10, + failed: 0, + skipped: 0, + }, + hits: { + total: 100, + max_score: 100, + hits: [ + { + ...sampleDocNoSortId, + }, + ], + }, +}; + +export const sampleDocSearchResultsWithSortId: SignalSearchResponse = { + took: 10, + timed_out: false, + _shards: { + total: 10, + successful: 10, + failed: 0, + skipped: 0, + }, + hits: { + total: 1, + max_score: 100, + hits: [ + { + ...sampleDocWithSortId, + }, + ], + }, +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/build_events_query.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/build_events_query.test.ts index 6c95e82485e452e..d20c92bd2698a97 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/build_events_query.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/build_events_query.test.ts @@ -147,4 +147,148 @@ describe('create_signals', () => { }, }); }); + test('if searchAfterSortId is a valid sortId string', () => { + const fakeSortId = '123456789012'; + const query = buildEventsSearchQuery({ + index: ['auditbeat-*'], + from: 'now-5m', + to: 'today', + filter: {}, + size: 100, + searchAfterSortId: fakeSortId, + }); + expect(query).toEqual({ + allowNoIndices: true, + index: ['auditbeat-*'], + size: 100, + ignoreUnavailable: true, + body: { + query: { + bool: { + filter: [ + {}, + { + bool: { + filter: [ + { + bool: { + should: [ + { + range: { + '@timestamp': { + gte: 'now-5m', + }, + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + range: { + '@timestamp': { + lte: 'today', + }, + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + { + match_all: {}, + }, + ], + }, + }, + track_total_hits: true, + sort: [ + { + '@timestamp': { + order: 'asc', + }, + }, + ], + search_after: [fakeSortId], + }, + }); + }); + test('if searchAfterSortId is a valid sortId number', () => { + const fakeSortIdNumber = 123456789012; + const query = buildEventsSearchQuery({ + index: ['auditbeat-*'], + from: 'now-5m', + to: 'today', + filter: {}, + size: 100, + searchAfterSortId: fakeSortIdNumber, + }); + expect(query).toEqual({ + allowNoIndices: true, + index: ['auditbeat-*'], + size: 100, + ignoreUnavailable: true, + body: { + query: { + bool: { + filter: [ + {}, + { + bool: { + filter: [ + { + bool: { + should: [ + { + range: { + '@timestamp': { + gte: 'now-5m', + }, + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + range: { + '@timestamp': { + lte: 'today', + }, + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + { + match_all: {}, + }, + ], + }, + }, + track_total_hits: true, + sort: [ + { + '@timestamp': { + order: 'asc', + }, + }, + ], + search_after: [fakeSortIdNumber], + }, + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/build_events_query.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/build_events_query.ts index d2fb7c21f66f546..541650d0ebe3887 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/build_events_query.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/build_events_query.ts @@ -10,7 +10,7 @@ interface BuildEventsSearchQuery { to: string; filter: unknown; size: number; - searchAfterSortId?: string; + searchAfterSortId?: string | number; } export const buildEventsSearchQuery = ({ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts new file mode 100644 index 000000000000000..6fa8bc039d4a4bd --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts @@ -0,0 +1,269 @@ +/* + * 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. + */ +import { BulkIndexDocumentsParams, SearchParams } from 'elasticsearch'; +import { savedObjectsClientMock } from 'src/core/server/mocks'; + +import { AlertServices } from '../../../../../alerting/server/types'; +import { Logger } from '../../../../../../../../src/core/server'; +import { + buildBulkBody, + singleBulkIndex, + singleSearchAfter, + searchAfterAndBulkIndex, +} from './utils'; +import { SignalHit } from '../../types'; +import { + sampleDocNoSortId, + sampleSignalAlertParams, + sampleDocSearchResultsNoSortId, + sampleDocSearchResultsWithSortId, +} from './__mocks__/es_results'; +import { SignalAlertParams } from './types'; +import { buildEventsSearchQuery } from './build_events_query'; + +describe('utils', () => { + describe('buildBulkBody', () => { + test('if bulk body builds well-defined body', () => { + const sampleParams: SignalAlertParams = sampleSignalAlertParams(); + const fakeSignalSourceHit = buildBulkBody(sampleDocNoSortId, sampleParams); + expect(fakeSignalSourceHit).toEqual({ + someKey: 'someValue', + '@timestamp': 'someTimeStamp', + signal: { + '@timestamp': fakeSignalSourceHit.signal['@timestamp'], // timestamp generated in the body + rule_revision: 1, + rule_id: sampleParams.id, + rule_type: sampleParams.type, + parent: { + id: sampleDocNoSortId._id, + type: 'event', + index: sampleDocNoSortId._index, + depth: 1, + }, + name: sampleParams.name, + severity: sampleParams.severity, + description: sampleParams.description, + original_time: sampleDocNoSortId._source['@timestamp'], + index_patterns: sampleParams.index, + references: sampleParams.references, + }, + }); + }); + }); + describe('singleBulkIndex', () => { + test('create successful bulk index', async () => { + // need a sample search result, sample signal params, mock service, mock logger + const sampleParams = sampleSignalAlertParams(); + const sampleSearchResult = sampleDocSearchResultsNoSortId; + const savedObjectsClient = savedObjectsClientMock.create(); + const bulkBody = sampleDocSearchResultsNoSortId.hits.hits.flatMap(doc => [ + { + index: { + _index: process.env.SIGNALS_INDEX || '.siem-signals-10-01-2019', + _id: doc._id, + }, + }, + buildBulkBody(doc, sampleParams), + ]); + const mockService: AlertServices = { + callCluster: async (action: string, params: BulkIndexDocumentsParams) => { + expect(action).toEqual('bulk'); + expect(params.index).toEqual('.siem-signals-10-01-2019'); + + // timestamps are annoying... + (bulkBody[1] as SignalHit).signal['@timestamp'] = params.body[1].signal['@timestamp']; + expect(params.body).toEqual(bulkBody); + return { + took: 100, + errors: false, + items: [ + { + fakeItemValue: 'fakeItemKey', + }, + ], + }; + }, + alertInstanceFactory: jest.fn(), + savedObjectsClient, + }; + const mockLogger: Logger = { + info: jest.fn((logString: string) => { + return logString; + }), + warn: jest.fn((logString: string) => { + return logString; + }), + trace: jest.fn(), + debug: jest.fn(), + error: jest.fn(), + fatal: jest.fn(), + log: jest.fn(), + }; + const successfulSingleBulkIndex = await singleBulkIndex( + sampleSearchResult, + sampleParams, + mockService, + mockLogger + ); + expect(mockLogger.info).toHaveBeenCalledTimes(2); + expect(mockLogger.warn).toHaveBeenCalledTimes(0); + expect(successfulSingleBulkIndex).toEqual(true); + }); + }); + describe('singleSearchAfter', () => { + test('if singleSearchAfter works without a given sort id', async () => { + let searchAfterSortId; + const sampleParams = sampleSignalAlertParams(); + const savedObjectsClient = savedObjectsClientMock.create(); + const expectedSearchAfterQuery = buildEventsSearchQuery({ + index: sampleParams.index, + from: sampleParams.from, + to: sampleParams.to, + filter: sampleParams.filter, + size: sampleParams.size ? sampleParams.size : 1, + }); + const mockService: AlertServices = { + callCluster: async (action: string, params: SearchParams) => { + expect(action).toEqual('search'); + expect(params.index).toEqual(sampleParams.index); + expect(params).toEqual(expectedSearchAfterQuery); + return sampleDocSearchResultsNoSortId; + }, + alertInstanceFactory: jest.fn(), + savedObjectsClient, + }; + const mockLogger: Logger = { + info: jest.fn((logString: string) => { + return logString; + }), + warn: jest.fn((logString: string) => { + return logString; + }), + trace: jest.fn(), + debug: jest.fn(), + error: jest.fn(), + fatal: jest.fn(), + log: jest.fn(), + }; + try { + const searchAfterResult = await singleSearchAfter( + searchAfterSortId, + sampleParams, + mockService, + mockLogger + ); + expect(searchAfterResult).toBeEmpty(); + } catch (exc) { + expect(exc.message).toEqual('Attempted to search after with empty sort id'); + } + }); + test('if singleSearchAfter works with a given sort id', async () => { + const searchAfterSortId = '1234567891111'; + const sampleParams = sampleSignalAlertParams(); + const savedObjectsClient = savedObjectsClientMock.create(); + const expectedSearchAfterQuery = buildEventsSearchQuery({ + index: sampleParams.index, + from: sampleParams.from, + to: sampleParams.to, + filter: sampleParams.filter, + size: sampleParams.size ? sampleParams.size : 1, + searchAfterSortId, + }); + const mockService: AlertServices = { + callCluster: async (action: string, params: SearchParams) => { + expect(action).toEqual('search'); + expect(params.index).toEqual(sampleParams.index); + expect(params).toEqual(expectedSearchAfterQuery); + return sampleDocSearchResultsWithSortId; + }, + alertInstanceFactory: jest.fn(), + savedObjectsClient, + }; + const mockLogger: Logger = { + info: jest.fn((logString: string) => { + return logString; + }), + warn: jest.fn((logString: string) => { + return logString; + }), + trace: jest.fn(), + debug: jest.fn(), + error: jest.fn(), + fatal: jest.fn(), + log: jest.fn(), + }; + const searchAfterResult = await singleSearchAfter( + searchAfterSortId, + sampleParams, + mockService, + mockLogger + ); + expect(searchAfterResult).toEqual(sampleDocSearchResultsWithSortId); + }); + }); + describe('searchAfterAndBulkIndex', () => { + test('if one successful iteration of searchAfterAndBulkIndex', async () => { + const searchAfterSortId = '1234567891111'; + const sampleParams = sampleSignalAlertParams(); + const savedObjectsClient = savedObjectsClientMock.create(); + const expectedSearchAfterQuery = buildEventsSearchQuery({ + index: sampleParams.index, + from: sampleParams.from, + to: sampleParams.to, + filter: sampleParams.filter, + size: sampleParams.size ? sampleParams.size : 1, + searchAfterSortId, + }); + const mockService: AlertServices = { + callCluster: async (action: string, params: SearchParams) => { + if (action === 'bulk') { + expect(action).toEqual('bulk'); + expect(params.index).toEqual('.siem-signals-10-01-2019'); + return { + took: 100, + errors: false, + items: [ + { + fakeItemValue: 'fakeItemKey', + }, + ], + }; + } + expect(action).toEqual('search'); + expect(params.index).toEqual(sampleParams.index); + expect(params).toEqual(expectedSearchAfterQuery); + return sampleDocSearchResultsWithSortId; + }, + alertInstanceFactory: jest.fn(), + savedObjectsClient, + }; + const mockLogger: Logger = { + info: jest.fn((logString: string) => { + return logString; + }), + warn: jest.fn((logString: string) => { + expect(logString).toEqual('something'); + return logString; + }), + trace: jest.fn(), + debug: jest.fn(), + error: jest.fn((logString: string) => { + expect(logString).toEqual('something'); + return logString; + }), + fatal: jest.fn(), + log: jest.fn(), + }; + const result = await searchAfterAndBulkIndex( + sampleDocSearchResultsWithSortId, + sampleParams, + mockService, + mockLogger + ); + expect(result).toEqual(true); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts index 08e99de0f2581d4..7d864b0141543c7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts @@ -61,6 +61,20 @@ export const singleBulkIndex = async ( refresh: false, body: bulkBody, }); + // return service + // .callCluster('bulk', { + // index: process.env.SIGNALS_INDEX || '.siem-signals-10-01-2019', + // refresh: false, + // body: bulkBody, + // }) + // .then(result => { + // if (result.errors) { + // logger.error(result.errors); + // return Promise.reject(false); // eslint-disable-line prefer-promise-reject-errors + // } + // logger.info('finished'); + // return Promise.resolve(true); + // }); const time2 = performance.now(); logger.info(`individual bulk process time took: ${time2 - time1} milliseconds`); logger.info(`took property says bulk took: ${firstResult.took} milliseconds`); @@ -87,10 +101,13 @@ export const singleSearchAfter = async ( from: params.from, to: params.to, filter: params.filter, - size: params.size ? params.size : 1000, + size: params.size ? params.size : 1, searchAfterSortId, }); - const nextSearchAfterResult = await service.callCluster('search', searchAfterQuery); + const nextSearchAfterResult: SignalSearchResponse = await service.callCluster( + 'search', + searchAfterQuery + ); return nextSearchAfterResult; } catch (exc) { logger.error(`[-] nextSearchAfter threw an error ${exc}`); @@ -127,7 +144,7 @@ export const searchAfterAndBulkIndex = async ( if (sortIds != null) { sortId = sortIds[0]; } - while (size < totalHits) { + while (size < totalHits && size !== 0) { // utilize track_total_hits instead of true try { logger.info(`sortIds: ${sortIds}`);