diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/README.md b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/README.md new file mode 100644 index 0000000000000..059005707625f --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/README.md @@ -0,0 +1,144 @@ +This is where you add code when you have rules which contain saved object references. Saved object references are for +when you have "joins" in the saved objects between one saved object and another one. This can be a 1 to M (1 to many) +relationship for example where you have a rule which contains the "id" of another saved object. + +Examples are the `exceptionsList` on a rule which contains a saved object reference from the rule to another set of +saved objects of the type `exception-list` + +## Useful queries +How to get all your alerts to see if you have `exceptionsList` on it or not in dev tools: + +```json +GET .kibana/_search +{ + "query": { + "term": { + "type": { + "value": "alert" + } + } + } +} +``` + +## Structure on disk +Run a query in dev tools and you should see this code that adds the following savedObject references +to any newly saved rule: + +```json + { + "_index" : ".kibana-hassanabad19_8.0.0_001", + "_id" : "alert:38482620-ef1b-11eb-ad71-7de7959be71c", + "_score" : 6.2607274, + "_source" : { + "alert" : { + "name" : "kql test rule 1", + "tags" : [ + "__internal_rule_id:4ec223b9-77fa-4895-8539-6b3e586a2858", + "__internal_immutable:false" + ], + "alertTypeId" : "siem.signals", + "other data... other data": "other data...other data", + "exceptionsList" : [ + { + "id" : "endpoint_list", + "list_id" : "endpoint_list", + "namespace_type" : "agnostic", + "type" : "endpoint" + }, + { + "id" : "50e3bd70-ef1b-11eb-ad71-7de7959be71c", + "list_id" : "cd152d0d-3590-4a45-a478-eed04da7936b", + "type" : "detection", + "namespace_type" : "single" + } + ], + "other data... other data": "other data...other data", + "references" : [ + { + "name" : "param:exceptionsList_0", + "id" : "endpoint_list", + "type" : "exception-list" + }, + { + "name" : "param:exceptionsList_1", + "id" : "50e3bd70-ef1b-11eb-ad71-7de7959be71c", + "type" : "exception-list" + } + ], + "other data... other data": "other data...other data" + } + } + } +``` + +The structure is that the alerting framework in conjunction with this code will make an array of saved object references which are going to be: +```json +{ + "references" : [ + { + "name" : "param:exceptionsList_1", + "id" : "50e3bd70-ef1b-11eb-ad71-7de7959be71c", + "type" : "exception-list" + } + ] +} +``` + +`name` is the pattern of `param:${name}_${index}`. See the functions and constants in `utils.ts` of: + +* EXCEPTIONS_LIST_NAME +* getSavedObjectNamePattern +* getSavedObjectNamePatternForExceptionsList +* getSavedObjectReference +* getSavedObjectReferenceForExceptionsList + +For how it is constructed and retrieved. If you need to add more types, you should copy and create your own versions or use the generic +utilities/helpers if possible. + +`id` is the saved object id and should always be the same value as the `"exceptionsList" : [ "id" : "50e3bd70-ef1b-11eb-ad71-7de7959be71c" ...`. +If for some reason the saved object id changes or is different, then on the next save/persist the `exceptionsList.id` will update to that within +its saved object. Note though, that the references id replaces _always_ the `exceptionsList.id` at all times through `inject_references.ts`. If +for some reason the `references` id is deleted, then on the next `inject_references` it will prefer to use the last good known reference and log +a warning. + +Within the rule parameters you can still keep the last known good saved object reference id as above it is shown +```json +{ + "exceptionsList" : [ + { + "id" : "endpoint_list", + "list_id" : "endpoint_list", + "namespace_type" : "agnostic", + "type" : "endpoint" + }, + { + "id" : "50e3bd70-ef1b-11eb-ad71-7de7959be71c", + "list_id" : "cd152d0d-3590-4a45-a478-eed04da7936b", + "type" : "detection", + "namespace_type" : "single" + } + ], +} +``` + +## How to add a new saved object id reference to a rule + +See the files of: +* extract_references.ts +* inject_references.ts + +And their top level comments for how to wire up new instances. It's best to create a new file per saved object reference and push only the needed data +per file. + +Good examples and utilities can be found in the folder of `utils` such as: +* EXCEPTIONS_LIST_NAME +* getSavedObjectNamePattern +* getSavedObjectNamePatternForExceptionsList +* getSavedObjectReference +* getSavedObjectReferenceForExceptionsList + +You can follow those patterns but if it doesn't fit your use case it's fine to just create a new file and wire up your new saved object references + +## End to end tests +At this moment there are none. \ No newline at end of file diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/extract_exceptions_list.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/extract_exceptions_list.test.ts new file mode 100644 index 0000000000000..56a1e875ac5e7 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/extract_exceptions_list.test.ts @@ -0,0 +1,74 @@ +/* + * 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 { extractExceptionsList } from './extract_exceptions_list'; +import { loggingSystemMock } from 'src/core/server/mocks'; +import { RuleParams } from '../../schemas/rule_schemas'; +import { EXCEPTION_LIST_NAMESPACE } from '@kbn/securitysolution-list-constants'; +import { EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME } from './utils'; + +describe('extract_exceptions_list', () => { + type FuncReturn = ReturnType; + let logger = loggingSystemMock.create().get('security_solution'); + const mockExceptionsList = (): RuleParams['exceptionsList'] => [ + { + id: '123', + list_id: '456', + type: 'detection', + namespace_type: 'agnostic', + }, + ]; + + beforeEach(() => { + logger = loggingSystemMock.create().get('security_solution'); + }); + + test('it returns an empty array given an empty array for exceptionsList', () => { + expect(extractExceptionsList({ logger, exceptionsList: [] })).toEqual([]); + }); + + test('logs expect error message if the exceptionsList is undefined', () => { + extractExceptionsList({ + logger, + exceptionsList: (undefined as unknown) as RuleParams['exceptionsList'], + }); + expect(logger.error).toBeCalledWith( + 'Exception list is null when it never should be. This indicates potentially that saved object migrations did not run correctly. Returning empty saved object reference' + ); + }); + + test('It returns exception list transformed into a saved object references', () => { + expect( + extractExceptionsList({ logger, exceptionsList: mockExceptionsList() }) + ).toEqual([ + { + id: '123', + name: `${EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME}_0`, + type: EXCEPTION_LIST_NAMESPACE, + }, + ]); + }); + + test('It returns two exception lists transformed into a saved object references', () => { + const twoInputs: RuleParams['exceptionsList'] = [ + mockExceptionsList()[0], + { ...mockExceptionsList()[0], id: '976' }, + ]; + expect(extractExceptionsList({ logger, exceptionsList: twoInputs })).toEqual([ + { + id: '123', + name: `${EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME}_0`, + type: EXCEPTION_LIST_NAMESPACE, + }, + { + id: '976', + name: `${EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME}_1`, + type: EXCEPTION_LIST_NAMESPACE, + }, + ]); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/extract_exceptions_list.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/extract_exceptions_list.ts new file mode 100644 index 0000000000000..9b7f8bbcefee1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/extract_exceptions_list.ts @@ -0,0 +1,41 @@ +/* + * 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 { Logger, SavedObjectReference } from 'src/core/server'; +import { EXCEPTION_LIST_NAMESPACE } from '@kbn/securitysolution-list-constants'; +import { RuleParams } from '../../schemas/rule_schemas'; +import { getSavedObjectNamePatternForExceptionsList } from './utils'; + +/** + * This extracts the "exceptionsList" "id" and returns it as a saved object reference. + * NOTE: Due to rolling upgrades with migrations and a few bugs with migrations, I do an additional check for if "exceptionsList" exists or not. Once + * those bugs are fixed, we can remove the "if (exceptionsList == null) {" check, but for the time being it is there to keep things running even + * if exceptionsList has not been migrated. + * @param logger The kibana injected logger + * @param exceptionsList The exceptions list to get the id from and return it as a saved object reference. + * @returns The saved object references from the exceptions list + */ +export const extractExceptionsList = ({ + logger, + exceptionsList, +}: { + logger: Logger; + exceptionsList: RuleParams['exceptionsList']; +}): SavedObjectReference[] => { + if (exceptionsList == null) { + logger.error( + 'Exception list is null when it never should be. This indicates potentially that saved object migrations did not run correctly. Returning empty saved object reference' + ); + return []; + } else { + return exceptionsList.map((exceptionItem, index) => ({ + name: getSavedObjectNamePatternForExceptionsList(index), + id: exceptionItem.id, + type: EXCEPTION_LIST_NAMESPACE, + })); + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/extract_references.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/extract_references.test.ts new file mode 100644 index 0000000000000..31288559e9437 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/extract_references.test.ts @@ -0,0 +1,82 @@ +/* + * 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 { loggingSystemMock } from 'src/core/server/mocks'; +import { extractReferences } from './extract_references'; +import { RuleParams } from '../../schemas/rule_schemas'; +import { EXCEPTION_LIST_NAMESPACE } from '@kbn/securitysolution-list-constants'; +import { EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME } from './utils'; + +describe('extract_references', () => { + type FuncReturn = ReturnType; + let logger = loggingSystemMock.create().get('security_solution'); + const mockExceptionsList = (): RuleParams['exceptionsList'] => [ + { + id: '123', + list_id: '456', + type: 'detection', + namespace_type: 'agnostic', + }, + ]; + + beforeEach(() => { + logger = loggingSystemMock.create().get('security_solution'); + }); + + test('It returns params untouched and the references extracted as exception list saved object references', () => { + const params: Partial = { + note: 'some note', + exceptionsList: mockExceptionsList(), + }; + expect( + extractReferences({ + logger, + params: params as RuleParams, + }) + ).toEqual({ + params: params as RuleParams, + references: [ + { + id: '123', + name: `${EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME}_0`, + type: EXCEPTION_LIST_NAMESPACE, + }, + ], + }); + }); + + test('It returns params untouched and the references an empty array if the exceptionsList is an empty array', () => { + const params: Partial = { + note: 'some note', + exceptionsList: [], + }; + expect( + extractReferences({ + logger, + params: params as RuleParams, + }) + ).toEqual({ + params: params as RuleParams, + references: [], + }); + }); + + test('It returns params untouched and the references an empty array if the exceptionsList is missing for any reason', () => { + const params: Partial = { + note: 'some note', + }; + expect( + extractReferences({ + logger, + params: params as RuleParams, + }) + ).toEqual({ + params: params as RuleParams, + references: [], + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/extract_references.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/extract_references.ts new file mode 100644 index 0000000000000..92e689e225764 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/extract_references.ts @@ -0,0 +1,57 @@ +/* + * 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 { Logger } from 'src/core/server'; +import { RuleParamsAndRefs } from '../../../../../../alerting/server'; +import { RuleParams } from '../../schemas/rule_schemas'; +import { extractExceptionsList } from './extract_exceptions_list'; + +/** + * Extracts references and returns the saved object references. + * How to add a new extracted references here: + * --- + * Add a new file for extraction named: extract_.ts, example: extract_foo.ts + * Add a function into that file named: extract, example: extractFoo(logger, params.foo) + * Add a new line below and concat together the new extract with existing ones like so: + * + * const exceptionReferences = extractExceptionsList(logger, params.exceptionsList); + * const fooReferences = extractFoo(logger, params.foo); + * const returnReferences = [...exceptionReferences, ...fooReferences]; + * + * Optionally you can remove any parameters you do not want to store within the Saved Object here: + * const paramsWithoutSavedObjectReferences = { removeParam, ...otherParams }; + * + * If you do remove params, then update the types in: security_solution/server/lib/detection_engine/signals/types.ts + * to use an omit for the functions of "isAlertExecutor" and "SignalRuleAlertTypeDefinition" + * @param logger Kibana injected logger + * @param params The params of the base rule(s). + * @returns The rule parameters and the saved object references to store. + */ +export const extractReferences = ({ + logger, + params, +}: { + logger: Logger; + params: RuleParams; +}): RuleParamsAndRefs => { + const exceptionReferences = extractExceptionsList({ + logger, + exceptionsList: params.exceptionsList, + }); + const returnReferences = [...exceptionReferences]; + + // Modify params if you want to remove any elements separately here. For exceptionLists, we do not remove the id and instead + // keep it to both fail safe guard against manually removed saved object references or if there are migration issues and the saved object + // references are removed. Also keeping it we can detect and log out a warning if the reference between it and the saved_object reference + // array have changed between each other indicating the saved_object array is being mutated outside of this functionality + const paramsWithoutSavedObjectReferences = { ...params }; + + return { + references: returnReferences, + params: paramsWithoutSavedObjectReferences, + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/index.ts new file mode 100644 index 0000000000000..b855554545837 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/index.ts @@ -0,0 +1,9 @@ +/* + * 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 * from './inject_references'; +export * from './extract_references'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_exceptions_list.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_exceptions_list.test.ts new file mode 100644 index 0000000000000..fc35088da66fc --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_exceptions_list.test.ts @@ -0,0 +1,139 @@ +/* + * 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 { loggingSystemMock } from 'src/core/server/mocks'; +import { SavedObjectReference } from 'src/core/server'; +import { EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME } from './utils'; +import { EXCEPTION_LIST_NAMESPACE } from '@kbn/securitysolution-list-constants'; +import { injectExceptionsReferences } from './inject_exceptions_list'; +import { RuleParams } from '../../schemas/rule_schemas'; + +describe('inject_exceptions_list', () => { + type FuncReturn = ReturnType; + let logger = loggingSystemMock.create().get('security_solution'); + const mockExceptionsList = (): RuleParams['exceptionsList'] => [ + { + id: '123', + list_id: '456', + type: 'detection', + namespace_type: 'agnostic', + }, + ]; + const mockSavedObjectReferences = (): SavedObjectReference[] => [ + { + id: '123', + name: `${EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME}_0`, + type: EXCEPTION_LIST_NAMESPACE, + }, + ]; + + beforeEach(() => { + logger = loggingSystemMock.create().get('security_solution'); + }); + + test('returns empty array given an empty array for both "exceptionsList" and "savedObjectReferences"', () => { + expect( + injectExceptionsReferences({ + logger, + exceptionsList: [], + savedObjectReferences: [], + }) + ).toEqual([]); + }); + + test('logs expect error message if the exceptionsList is undefined', () => { + injectExceptionsReferences({ + logger, + exceptionsList: (undefined as unknown) as RuleParams['exceptionsList'], + savedObjectReferences: mockSavedObjectReferences(), + }); + expect(logger.error).toBeCalledWith( + 'Exception list is null when it never should be. This indicates potentially that saved object migrations did not run correctly. Returning empty exception list' + ); + }); + + test('returns empty array given an empty array for "exceptionsList"', () => { + expect( + injectExceptionsReferences({ + logger, + exceptionsList: [], + savedObjectReferences: mockSavedObjectReferences(), + }) + ).toEqual([]); + }); + + test('returns exceptions list array given an empty array for "savedObjectReferences"', () => { + expect( + injectExceptionsReferences({ + logger, + exceptionsList: mockExceptionsList(), + savedObjectReferences: [], + }) + ).toEqual(mockExceptionsList()); + }); + + test('returns parameters from the saved object if found', () => { + expect( + injectExceptionsReferences({ + logger, + exceptionsList: mockExceptionsList(), + savedObjectReferences: mockSavedObjectReferences(), + }) + ).toEqual(mockExceptionsList()); + }); + + test('does not log an error if it returns parameters from the saved object when found', () => { + injectExceptionsReferences({ + logger, + exceptionsList: mockExceptionsList(), + savedObjectReferences: mockSavedObjectReferences(), + }); + expect(logger.error).not.toHaveBeenCalled(); + }); + + test('returns parameters from the saved object if found with a different saved object reference id', () => { + expect( + injectExceptionsReferences({ + logger, + exceptionsList: mockExceptionsList(), + savedObjectReferences: [{ ...mockSavedObjectReferences()[0], id: '456' }], + }) + ).toEqual([{ ...mockExceptionsList()[0], id: '456' }]); + }); + + test('logs an error if found with a different saved object reference id', () => { + injectExceptionsReferences({ + logger, + exceptionsList: mockExceptionsList(), + savedObjectReferences: [{ ...mockSavedObjectReferences()[0], id: '456' }], + }); + expect(logger.error).toBeCalledWith( + 'The id of the "saved object reference id": 456 is not the same as the "saved object id": 123. Preferring and using the "saved object reference id" instead of the "saved object id"' + ); + }); + + test('returns exceptionItem if the saved object reference cannot match as a fall back', () => { + expect( + injectExceptionsReferences({ + logger, + exceptionsList: mockExceptionsList(), + savedObjectReferences: [{ ...mockSavedObjectReferences()[0], name: 'other-name_0' }], + }) + ).toEqual(mockExceptionsList()); + }); + + test('logs an error if the saved object type could not be found', () => { + injectExceptionsReferences({ + logger, + exceptionsList: mockExceptionsList(), + savedObjectReferences: [{ ...mockSavedObjectReferences()[0], name: 'other-name_0' }], + }); + expect(logger.error).toBeCalledWith( + 'The saved object references were not found for our exception list when we were expecting to find it. Kibana migrations might not have run correctly or someone might have removed the saved object references manually. Returning the last known good exception list id which might not work. exceptionItem with its id being returned is: {"id":"123","list_id":"456","type":"detection","namespace_type":"agnostic"}' + ); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_exceptions_list.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_exceptions_list.ts new file mode 100644 index 0000000000000..2e6559fbf18cf --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_exceptions_list.ts @@ -0,0 +1,62 @@ +/* + * 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 { Logger, SavedObjectReference } from 'src/core/server'; +import { RuleParams } from '../../schemas/rule_schemas'; +import { + getSavedObjectReferenceForExceptionsList, + logMissingSavedObjectError, + logWarningIfDifferentReferencesDetected, +} from './utils'; + +/** + * This injects any "exceptionsList" "id"'s from saved object reference and returns the "exceptionsList" using the saved object reference. If for + * some reason it is missing on saved object reference, we log an error about it and then take the last known good value from the "exceptionsList" + * + * @param logger The kibana injected logger + * @param exceptionsList The exceptions list to merge the saved object reference from. + * @param savedObjectReferences The saved object references which should contain an "exceptionsList" + * @returns The exceptionsList with the saved object reference replacing any value in the saved object's id. + */ +export const injectExceptionsReferences = ({ + logger, + exceptionsList, + savedObjectReferences, +}: { + logger: Logger; + exceptionsList: RuleParams['exceptionsList']; + savedObjectReferences: SavedObjectReference[]; +}): RuleParams['exceptionsList'] => { + if (exceptionsList == null) { + logger.error( + 'Exception list is null when it never should be. This indicates potentially that saved object migrations did not run correctly. Returning empty exception list' + ); + return []; + } + return exceptionsList.map((exceptionItem, index) => { + const savedObjectReference = getSavedObjectReferenceForExceptionsList({ + logger, + index, + savedObjectReferences, + }); + if (savedObjectReference != null) { + logWarningIfDifferentReferencesDetected({ + logger, + savedObjectReferenceId: savedObjectReference.id, + savedObjectId: exceptionItem.id, + }); + const reference: RuleParams['exceptionsList'][0] = { + ...exceptionItem, + id: savedObjectReference.id, + }; + return reference; + } else { + logMissingSavedObjectError({ logger, exceptionItem }); + return exceptionItem; + } + }); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_references.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_references.test.ts new file mode 100644 index 0000000000000..a80f19ae011d7 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_references.test.ts @@ -0,0 +1,102 @@ +/* + * 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 { loggingSystemMock } from 'src/core/server/mocks'; +import { SavedObjectReference } from 'src/core/server'; +import { EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME } from './utils'; +import { EXCEPTION_LIST_NAMESPACE } from '@kbn/securitysolution-list-constants'; +import { injectReferences } from './inject_references'; +import { RuleParams } from '../../schemas/rule_schemas'; + +describe('inject_references', () => { + type FuncReturn = ReturnType; + let logger = loggingSystemMock.create().get('security_solution'); + const mockExceptionsList = (): RuleParams['exceptionsList'] => [ + { + id: '123', + list_id: '456', + type: 'detection', + namespace_type: 'agnostic', + }, + ]; + const mockSavedObjectReferences = (): SavedObjectReference[] => [ + { + id: '123', + name: `${EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME}_0`, + type: EXCEPTION_LIST_NAMESPACE, + }, + ]; + + beforeEach(() => { + logger = loggingSystemMock.create().get('security_solution'); + }); + + test('returns parameters from a saved object if found', () => { + const params: Partial = { + note: 'some note', + exceptionsList: mockExceptionsList(), + }; + expect( + injectReferences({ + logger, + params: params as RuleParams, + savedObjectReferences: mockSavedObjectReferences(), + }) + ).toEqual(params as RuleParams); + }); + + test('returns parameters from the saved object if found with a different saved object reference id', () => { + const params: Partial = { + note: 'some note', + exceptionsList: mockExceptionsList(), + }; + + const returnParams: Partial = { + note: 'some note', + exceptionsList: [{ ...mockExceptionsList()[0], id: '456' }], + }; + + expect( + injectReferences({ + logger, + params: params as RuleParams, + savedObjectReferences: [{ ...mockSavedObjectReferences()[0], id: '456' }], + }) + ).toEqual(returnParams as RuleParams); + }); + + test('It returns params untouched and the references an empty array if the exceptionsList is an empty array', () => { + const params: Partial = { + note: 'some note', + exceptionsList: [], + }; + expect( + injectReferences({ + logger, + params: params as RuleParams, + savedObjectReferences: mockSavedObjectReferences(), + }) + ).toEqual(params as RuleParams); + }); + + test('It returns params with an added exceptionsList if the exceptionsList is missing due to migration bugs', () => { + const params: Partial = { + note: 'some note', + }; + const returnParams: Partial = { + note: 'some note', + exceptionsList: [], + }; + expect( + injectReferences({ + logger, + params: params as RuleParams, + savedObjectReferences: mockSavedObjectReferences(), + }) + ).toEqual(returnParams as RuleParams); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_references.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_references.ts new file mode 100644 index 0000000000000..dae5e3037b737 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_references.ts @@ -0,0 +1,50 @@ +/* + * 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 { Logger, SavedObjectReference } from 'src/core/server'; +import { RuleParams } from '../../schemas/rule_schemas'; +import { injectExceptionsReferences } from './inject_exceptions_list'; + +/** + * Injects references and returns the saved object references. + * How to add a new injected references here: + * --- + * Add a new file for injection named: inject_.ts, example: inject_foo.ts + * Add a new function into that file named: inject, example: injectFooReferences(logger, params.foo) + * Add a new line below and spread the new parameter together like so: + * + * const foo = injectFooReferences(logger, params.foo, savedObjectReferences); + * const ruleParamsWithSavedObjectReferences: RuleParams = { + * ...params, + * foo, + * exceptionsList, + * }; + * @param logger Kibana injected logger + * @param params The params of the base rule(s). + * @param savedObjectReferences The saved object references to merge with the rule params + * @returns The rule parameters with the saved object references. + */ +export const injectReferences = ({ + logger, + params, + savedObjectReferences, +}: { + logger: Logger; + params: RuleParams; + savedObjectReferences: SavedObjectReference[]; +}): RuleParams => { + const exceptionsList = injectExceptionsReferences({ + logger, + exceptionsList: params.exceptionsList, + savedObjectReferences, + }); + const ruleParamsWithSavedObjectReferences: RuleParams = { + ...params, + exceptionsList, + }; + return ruleParamsWithSavedObjectReferences; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/constants.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/constants.ts new file mode 100644 index 0000000000000..1423dff3b92f1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/constants.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +/** + * The name of the exceptions list we give it when we save the saved object references. This name will + * end up in the saved object as in this example: + * { + * "references" : [ + * { + * "name" : "param:exceptionsList_1", + * "id" : "50e3bd70-ef1b-11eb-ad71-7de7959be71c", + * "type" : "exception-list" + * } + * ] + * } + */ +export const EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME = 'exceptionsList'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_name_pattern.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_name_pattern.test.ts new file mode 100644 index 0000000000000..41dc2d9179d83 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_name_pattern.test.ts @@ -0,0 +1,30 @@ +/* + * 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 { getSavedObjectNamePattern } from '.'; + +describe('get_saved_object_name_pattern_for_exception_list', () => { + test('returns expected pattern given a zero', () => { + expect(getSavedObjectNamePattern({ name: 'test', index: 0 })).toEqual('test_0'); + }); + + test('returns expected pattern given a positive number', () => { + expect(getSavedObjectNamePattern({ name: 'test', index: 1 })).toEqual('test_1'); + }); + + test('throws given less than zero', () => { + expect(() => getSavedObjectNamePattern({ name: 'test', index: -1 })).toThrow( + '"index" should alway be >= 0 instead of: -1' + ); + }); + + test('throws given NaN', () => { + expect(() => getSavedObjectNamePattern({ name: 'test', index: NaN })).toThrow( + '"index" should alway be >= 0 instead of: NaN' + ); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_name_pattern.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_name_pattern.ts new file mode 100644 index 0000000000000..f4e33cf57fa2b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_name_pattern.ts @@ -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. + */ + +/** + * Given a name and index this will return the pattern of "${name_${index}" + * @param name The name to suffix the string + * @param index The index to suffix the string + * @returns The pattern "${name_${index}" + */ +export const getSavedObjectNamePattern = ({ + name, + index, +}: { + name: string; + index: number; +}): string => { + if (!(index >= 0)) { + throw new TypeError(`"index" should alway be >= 0 instead of: ${index}`); + } else { + return `${name}_${index}`; + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_name_pattern_for_exception_list.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_name_pattern_for_exception_list.test.ts new file mode 100644 index 0000000000000..98c575e8835be --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_name_pattern_for_exception_list.test.ts @@ -0,0 +1,37 @@ +/* + * 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 { + EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME, + getSavedObjectNamePatternForExceptionsList, +} from '.'; + +describe('get_saved_object_name_pattern_for_exception_list', () => { + test('returns expected pattern given a zero', () => { + expect(getSavedObjectNamePatternForExceptionsList(0)).toEqual( + `${EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME}_0` + ); + }); + + test('returns expected pattern given a positive number', () => { + expect(getSavedObjectNamePatternForExceptionsList(1)).toEqual( + `${EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME}_1` + ); + }); + + test('throws given less than zero', () => { + expect(() => getSavedObjectNamePatternForExceptionsList(-1)).toThrow( + '"index" should alway be >= 0 instead of: -1' + ); + }); + + test('throws given NaN', () => { + expect(() => getSavedObjectNamePatternForExceptionsList(NaN)).toThrow( + '"index" should alway be >= 0 instead of: NaN' + ); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_name_pattern_for_exception_list.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_name_pattern_for_exception_list.ts new file mode 100644 index 0000000000000..c505309411689 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_name_pattern_for_exception_list.ts @@ -0,0 +1,23 @@ +/* + * 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 { EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME } from './constants'; +import { getSavedObjectNamePattern } from './get_saved_object_name_pattern'; + +/** + * Given an index this will return the pattern of "exceptionsList_${index}" + * @param index The index to suffix the string + * @returns The pattern of "exceptionsList_${index}" + * @throws TypeError if index is less than zero + */ +export const getSavedObjectNamePatternForExceptionsList = (index: number): string => { + if (!(index >= 0)) { + throw new TypeError(`"index" should alway be >= 0 instead of: ${index}`); + } else { + return getSavedObjectNamePattern({ name: EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME, index }); + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_reference.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_reference.test.ts new file mode 100644 index 0000000000000..70f321eed030e --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_reference.test.ts @@ -0,0 +1,133 @@ +/* + * 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 { loggingSystemMock } from 'src/core/server/mocks'; +import { SavedObjectReference } from 'src/core/server'; +import { getSavedObjectReference } from '.'; + +describe('get_saved_object_reference', () => { + type FuncReturn = ReturnType; + const mockSavedObjectReferences = (): SavedObjectReference[] => [ + { + id: '123', + name: 'test_0', + type: 'some-type', + }, + ]; + let logger = loggingSystemMock.create().get('security_solution'); + + beforeEach(() => { + logger = loggingSystemMock.create().get('security_solution'); + }); + + test('returns reference found, given index zero', () => { + expect( + getSavedObjectReference({ + name: 'test', + logger, + index: 0, + savedObjectReferences: mockSavedObjectReferences(), + }) + ).toEqual(mockSavedObjectReferences()[0]); + }); + + test('returns reference found, given positive index', () => { + const savedObjectReferences: SavedObjectReference[] = [ + mockSavedObjectReferences()[0], + { + id: '345', + name: 'test_1', + type: 'some-type', + }, + ]; + expect( + getSavedObjectReference({ + name: 'test', + logger, + index: 1, + savedObjectReferences, + }) + ).toEqual(savedObjectReferences[1]); + }); + + test('returns undefined, given index larger than the size of object references', () => { + expect( + getSavedObjectReference({ + name: 'test', + logger, + index: 100, + savedObjectReferences: mockSavedObjectReferences(), + }) + ).toEqual(undefined); + }); + + test('returns undefined, when it cannot find the reference', () => { + expect( + getSavedObjectReference({ + name: 'test', + logger, + index: 0, + savedObjectReferences: [{ ...mockSavedObjectReferences()[0], name: 'other-name_0' }], + }) + ).toEqual(undefined); + }); + + test('returns found reference, even if the reference is mixed with other references', () => { + expect( + getSavedObjectReference({ + name: 'test', + logger, + index: 0, + savedObjectReferences: [ + { ...mockSavedObjectReferences()[0], name: 'other-name_0' }, + mockSavedObjectReferences()[0], + ], + }) + ).toEqual(mockSavedObjectReferences()[0]); + }); + + test('returns found reference, even if the reference is mixed with other references and has an index of 1', () => { + const additionalException: SavedObjectReference = { + ...mockSavedObjectReferences()[0], + name: 'test_1', + }; + expect( + getSavedObjectReference({ + name: 'test', + logger, + index: 1, + savedObjectReferences: [ + { ...mockSavedObjectReferences()[0], name: 'other-name_0' }, + mockSavedObjectReferences()[0], + additionalException, + ], + }) + ).toEqual(additionalException); + }); + + test('throws given less than zero', () => { + expect(() => + getSavedObjectReference({ + name: 'test', + logger, + index: -1, + savedObjectReferences: mockSavedObjectReferences(), + }) + ).toThrow('"index" should alway be >= 0 instead of: -1'); + }); + + test('throws given NaN', () => { + expect(() => + getSavedObjectReference({ + name: 'test', + logger, + index: NaN, + savedObjectReferences: mockSavedObjectReferences(), + }) + ).toThrow('"index" should alway be >= 0 instead of: NaN'); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_reference.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_reference.ts new file mode 100644 index 0000000000000..fe3a7393bf377 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_reference.ts @@ -0,0 +1,48 @@ +/* + * 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 { Logger, SavedObjectReference } from 'src/core/server'; +import { getSavedObjectNamePattern } from './get_saved_object_name_pattern'; + +/** + * Given a saved object name, and an index, this will return the specific named saved object reference + * even if it is mixed in with other reference objects. This is needed since a references array can contain multiple + * types of saved objects in a single array, we have to use the name to get the value. + * @param logger The kibana injected logger + * @param name The name of the saved object reference we are getting from the array + * @param index The index position to get for the exceptions list. + * @param savedObjectReferences The saved object references which can contain "exceptionsList" mixed with other saved object types + * @returns The saved object reference if found, otherwise undefined + */ +export const getSavedObjectReference = ({ + logger, + name, + index, + savedObjectReferences, +}: { + logger: Logger; + name: string; + index: number; + savedObjectReferences: SavedObjectReference[]; +}): SavedObjectReference | undefined => { + if (!(index >= 0)) { + throw new TypeError(`"index" should alway be >= 0 instead of: ${index}`); + } else if (index > savedObjectReferences.length) { + logger.error( + [ + 'Cannot get a saved object reference using an index which is larger than the saved object references. Index is:', + index, + ' which is larger than the savedObjectReferences:', + JSON.stringify(savedObjectReferences), + ].join('') + ); + } else { + return savedObjectReferences.find( + (reference) => reference.name === getSavedObjectNamePattern({ name, index }) + ); + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_reference_for_exceptions_list.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_reference_for_exceptions_list.test.ts new file mode 100644 index 0000000000000..9a16037ed7fd5 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_reference_for_exceptions_list.test.ts @@ -0,0 +1,130 @@ +/* + * 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 { loggingSystemMock } from 'src/core/server/mocks'; +import { SavedObjectReference } from 'src/core/server'; +import { + EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME, + getSavedObjectReferenceForExceptionsList, +} from '.'; +import { EXCEPTION_LIST_NAMESPACE } from '@kbn/securitysolution-list-constants'; + +describe('get_saved_object_reference_for_exceptions_list', () => { + type FuncReturn = ReturnType; + let logger = loggingSystemMock.create().get('security_solution'); + + beforeEach(() => { + logger = loggingSystemMock.create().get('security_solution'); + }); + + const mockSavedObjectReferences = (): SavedObjectReference[] => [ + { + id: '123', + name: `${EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME}_0`, + type: EXCEPTION_LIST_NAMESPACE, + }, + ]; + + test('returns reference found, given index zero', () => { + expect( + getSavedObjectReferenceForExceptionsList({ + logger, + index: 0, + savedObjectReferences: mockSavedObjectReferences(), + }) + ).toEqual(mockSavedObjectReferences()[0]); + }); + + test('returns reference found, given positive index', () => { + const savedObjectReferences: SavedObjectReference[] = [ + mockSavedObjectReferences()[0], + { + id: '345', + name: `${EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME}_1`, + type: EXCEPTION_LIST_NAMESPACE, + }, + ]; + expect( + getSavedObjectReferenceForExceptionsList({ + logger, + index: 1, + savedObjectReferences, + }) + ).toEqual(savedObjectReferences[1]); + }); + + test('returns undefined, given index larger than the size of object references', () => { + expect( + getSavedObjectReferenceForExceptionsList({ + logger, + index: 100, + savedObjectReferences: mockSavedObjectReferences(), + }) + ).toEqual(undefined); + }); + + test('returns undefined, when it cannot find the reference', () => { + expect( + getSavedObjectReferenceForExceptionsList({ + logger, + index: 0, + savedObjectReferences: [{ ...mockSavedObjectReferences()[0], name: 'other-name_0' }], + }) + ).toEqual(undefined); + }); + + test('returns found reference, even if the reference is mixed with other references', () => { + expect( + getSavedObjectReferenceForExceptionsList({ + logger, + index: 0, + savedObjectReferences: [ + { ...mockSavedObjectReferences()[0], name: 'other-name_0' }, + mockSavedObjectReferences()[0], + ], + }) + ).toEqual(mockSavedObjectReferences()[0]); + }); + + test('returns found reference, even if the reference is mixed with other references and has an index of 1', () => { + const additionalException: SavedObjectReference = { + ...mockSavedObjectReferences()[0], + name: `${EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME}_1`, + }; + expect( + getSavedObjectReferenceForExceptionsList({ + logger, + index: 1, + savedObjectReferences: [ + { ...mockSavedObjectReferences()[0], name: 'other-name_0' }, + mockSavedObjectReferences()[0], + additionalException, + ], + }) + ).toEqual(additionalException); + }); + + test('throws given less than zero', () => { + expect(() => + getSavedObjectReferenceForExceptionsList({ + logger, + index: -1, + savedObjectReferences: mockSavedObjectReferences(), + }) + ).toThrow('"index" should alway be >= 0 instead of: -1'); + }); + + test('throws given NaN', () => { + expect(() => + getSavedObjectReferenceForExceptionsList({ + logger, + index: NaN, + savedObjectReferences: mockSavedObjectReferences(), + }) + ).toThrow('"index" should alway be >= 0 instead of: NaN'); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_reference_for_exceptions_list.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_reference_for_exceptions_list.ts new file mode 100644 index 0000000000000..d1534cc2a06bb --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_reference_for_exceptions_list.ts @@ -0,0 +1,40 @@ +/* + * 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 { Logger, SavedObjectReference } from 'src/core/server'; +import { EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME } from './constants'; +import { getSavedObjectReference } from './get_saved_object_reference'; + +/** + * Given an index and a saved object reference, this will return the specific "exceptionsList" saved object reference + * even if it is mixed in with other reference objects. This is needed since a references array can contain multiple + * types of saved objects in a single array, we have to use the "exceptionsList" name to get the value. + * @param logger The kibana injected logger + * @param index The index position to get for the exceptions list. + * @param savedObjectReferences The saved object references which can contain "exceptionsList" mixed with other saved object types + * @returns The saved object reference if found, otherwise undefined + */ +export const getSavedObjectReferenceForExceptionsList = ({ + logger, + index, + savedObjectReferences, +}: { + logger: Logger; + index: number; + savedObjectReferences: SavedObjectReference[]; +}): SavedObjectReference | undefined => { + if (!(index >= 0)) { + throw new TypeError(`"index" should alway be >= 0 instead of: ${index}`); + } else { + return getSavedObjectReference({ + logger, + name: EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME, + index, + savedObjectReferences, + }); + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/index.ts new file mode 100644 index 0000000000000..ca88dae364a4b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/index.ts @@ -0,0 +1,14 @@ +/* + * 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 * from './constants'; +export * from './get_saved_object_name_pattern_for_exception_list'; +export * from './get_saved_object_name_pattern'; +export * from './get_saved_object_reference_for_exceptions_list'; +export * from './get_saved_object_reference'; +export * from './log_missing_saved_object_error'; +export * from './log_warning_if_different_references_detected'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_missing_saved_object_error.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_missing_saved_object_error.test.ts new file mode 100644 index 0000000000000..d0158be285667 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_missing_saved_object_error.test.ts @@ -0,0 +1,33 @@ +/* + * 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 { loggingSystemMock } from 'src/core/server/mocks'; + +import { logMissingSavedObjectError } from '.'; + +describe('log_missing_saved_object_error', () => { + let logger = loggingSystemMock.create().get('security_solution'); + + beforeEach(() => { + logger = loggingSystemMock.create().get('security_solution'); + }); + + test('logs expect error message', () => { + logMissingSavedObjectError({ + logger, + exceptionItem: { + id: '123', + list_id: '456', + type: 'detection', + namespace_type: 'agnostic', + }, + }); + expect(logger.error).toBeCalledWith( + 'The saved object references were not found for our exception list when we were expecting to find it. Kibana migrations might not have run correctly or someone might have removed the saved object references manually. Returning the last known good exception list id which might not work. exceptionItem with its id being returned is: {"id":"123","list_id":"456","type":"detection","namespace_type":"agnostic"}' + ); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_missing_saved_object_error.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_missing_saved_object_error.ts new file mode 100644 index 0000000000000..8d448c3cd10c4 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_missing_saved_object_error.ts @@ -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 { Logger } from 'src/core/server'; +import { RuleParams } from '../../../schemas/rule_schemas'; + +/** + * This will log a warning that we are missing an object reference. + * @param logger The kibana injected logger + * @param exceptionItem The exception item to log the warning out as + */ +export const logMissingSavedObjectError = ({ + logger, + exceptionItem, +}: { + logger: Logger; + exceptionItem: RuleParams['exceptionsList'][0]; +}): void => { + logger.error( + [ + 'The saved object references were not found for our exception list when we were expecting to find it. ', + 'Kibana migrations might not have run correctly or someone might have removed the saved object references manually. ', + 'Returning the last known good exception list id which might not work. exceptionItem with its id being returned is: ', + JSON.stringify(exceptionItem), + ].join('') + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_warning_if_different_references_detected.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_warning_if_different_references_detected.test.ts new file mode 100644 index 0000000000000..a27faa6356c2b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_warning_if_different_references_detected.test.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 { loggingSystemMock } from 'src/core/server/mocks'; + +import { logWarningIfDifferentReferencesDetected } from '.'; + +describe('log_warning_if_different_references_detected', () => { + let logger = loggingSystemMock.create().get('security_solution'); + + beforeEach(() => { + logger = loggingSystemMock.create().get('security_solution'); + }); + + test('logs expect error message if the two ids are different', () => { + logWarningIfDifferentReferencesDetected({ + logger, + savedObjectReferenceId: '123', + savedObjectId: '456', + }); + expect(logger.error).toBeCalledWith( + 'The id of the "saved object reference id": 123 is not the same as the "saved object id": 456. Preferring and using the "saved object reference id" instead of the "saved object id"' + ); + }); + + test('logs nothing if the two ids are the same', () => { + logWarningIfDifferentReferencesDetected({ + logger, + savedObjectReferenceId: '123', + savedObjectId: '123', + }); + expect(logger.error).not.toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_warning_if_different_references_detected.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_warning_if_different_references_detected.ts new file mode 100644 index 0000000000000..9f80ba6d8ce83 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_warning_if_different_references_detected.ts @@ -0,0 +1,36 @@ +/* + * 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 { Logger } from 'src/core/server'; + +/** + * This will log a warning that the saved object reference id and the saved object id are not the same if that is true. + * @param logger The kibana injected logger + * @param savedObjectReferenceId The saved object reference id from "references: [{ id: ...}]" + * @param savedObjectId The saved object id from a structure such as exceptions { exceptionsList: { "id": "..." } } + */ +export const logWarningIfDifferentReferencesDetected = ({ + logger, + savedObjectReferenceId, + savedObjectId, +}: { + logger: Logger; + savedObjectReferenceId: string; + savedObjectId: string; +}): void => { + if (savedObjectReferenceId !== savedObjectId) { + logger.error( + [ + 'The id of the "saved object reference id": ', + savedObjectReferenceId, + ' is not the same as the "saved object id": ', + savedObjectId, + '. Preferring and using the "saved object reference id" instead of the "saved object id"', + ].join('') + ); + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 7e467891e6d4d..b242691577b89 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -69,6 +69,7 @@ import { wrapHitsFactory } from './wrap_hits_factory'; import { wrapSequencesFactory } from './wrap_sequences_factory'; import { ConfigType } from '../../../config'; import { ExperimentalFeatures } from '../../../../common/experimental_features'; +import { injectReferences, extractReferences } from './saved_object_references'; import { RuleExecutionLogClient } from '../rule_execution_log/rule_execution_log_client'; import { IRuleDataPluginService } from '../rule_execution_log/types'; @@ -96,6 +97,11 @@ export const signalRulesAlertType = ({ name: 'SIEM signal', actionGroups: siemRuleActionGroups, defaultActionGroupId: 'default', + useSavedObjectReferences: { + extractReferences: (params) => extractReferences({ logger, params }), + injectReferences: (params, savedObjectReferences) => + injectReferences({ logger, params, savedObjectReferences }), + }, validate: { params: { validate: (object: unknown): RuleParams => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts index 6cbe0d1a52704..4da411d0c70a1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts @@ -195,7 +195,7 @@ export const isAlertExecutor = ( obj: SignalRuleAlertTypeDefinition ): obj is AlertType< RuleParams, - never, // Only use if defining useSavedObjectReferences hook + RuleParams, // This type is used for useSavedObjectReferences, use an Omit here if you want to remove any values. AlertTypeState, AlertInstanceState, AlertInstanceContext, @@ -206,7 +206,7 @@ export const isAlertExecutor = ( export type SignalRuleAlertTypeDefinition = AlertType< RuleParams, - never, // Only use if defining useSavedObjectReferences hook + RuleParams, // This type is used for useSavedObjectReferences, use an Omit here if you want to remove any values. AlertTypeState, AlertInstanceState, AlertInstanceContext,