diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/get_rules_to_update.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/get_rules_to_update.test.ts index 89d59ec8f77e3..9cedfc101a932 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/get_rules_to_update.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/get_rules_to_update.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { filterInstalledRules, getRulesToUpdate, mergeExceptionLists } from './get_rules_to_update'; +import { filterInstalledRules, getRulesToUpdate } from './get_rules_to_update'; import { getRuleMock } from '../../routes/__mocks__/request_responses'; import { getPrebuiltRuleMock } from '../mocks'; import { getQueryRuleParams } from '../../rule_schema/mocks'; @@ -111,207 +111,6 @@ describe('get_rules_to_update', () => { ); expect(update).toEqual([ruleAsset1, ruleAsset2]); }); - - test('should add back an exception_list if it was removed by the end user on an immutable rule during an upgrade', () => { - const ruleAsset1 = getPrebuiltRuleMock(); - ruleAsset1.exceptions_list = [ - { - id: 'endpoint_list', - list_id: 'endpoint_list', - namespace_type: 'agnostic', - type: 'endpoint', - }, - ]; - ruleAsset1.rule_id = 'rule-1'; - ruleAsset1.version = 2; - - const installedRule1 = getRuleMock(getQueryRuleParams()); - installedRule1.params.ruleId = 'rule-1'; - installedRule1.params.version = 1; - installedRule1.params.exceptionsList = []; - - const [update] = getRulesToUpdate([ruleAsset1], rulesToMap([installedRule1])); - expect(update.exceptions_list).toEqual(ruleAsset1.exceptions_list); - }); - - test('should not remove an additional exception_list if an additional one was added by the end user on an immutable rule during an upgrade', () => { - const ruleAsset1 = getPrebuiltRuleMock(); - ruleAsset1.exceptions_list = [ - { - id: 'endpoint_list', - list_id: 'endpoint_list', - namespace_type: 'agnostic', - type: 'endpoint', - }, - ]; - ruleAsset1.rule_id = 'rule-1'; - ruleAsset1.version = 2; - - const installedRule1 = getRuleMock(getQueryRuleParams()); - installedRule1.params.ruleId = 'rule-1'; - installedRule1.params.version = 1; - installedRule1.params.exceptionsList = [ - { - id: 'second_exception_list', - list_id: 'some-other-id', - namespace_type: 'single', - type: 'detection', - }, - ]; - - const [update] = getRulesToUpdate([ruleAsset1], rulesToMap([installedRule1])); - expect(update.exceptions_list).toEqual([ - ...ruleAsset1.exceptions_list, - ...installedRule1.params.exceptionsList, - ]); - }); - - test('should not remove an existing exception_list if they are the same between the current installed one and the upgraded one', () => { - const ruleAsset1 = getPrebuiltRuleMock(); - ruleAsset1.exceptions_list = [ - { - id: 'endpoint_list', - list_id: 'endpoint_list', - namespace_type: 'agnostic', - type: 'endpoint', - }, - ]; - ruleAsset1.rule_id = 'rule-1'; - ruleAsset1.version = 2; - - const installedRule1 = getRuleMock(getQueryRuleParams()); - installedRule1.params.ruleId = 'rule-1'; - installedRule1.params.version = 1; - installedRule1.params.exceptionsList = [ - { - id: 'endpoint_list', - list_id: 'endpoint_list', - namespace_type: 'agnostic', - type: 'endpoint', - }, - ]; - - const [update] = getRulesToUpdate([ruleAsset1], rulesToMap([installedRule1])); - expect(update.exceptions_list).toEqual(ruleAsset1.exceptions_list); - }); - - test('should not remove an existing exception_list if the rule has an empty exceptions list', () => { - const ruleAsset1 = getPrebuiltRuleMock(); - ruleAsset1.exceptions_list = []; - ruleAsset1.rule_id = 'rule-1'; - ruleAsset1.version = 2; - - const installedRule1 = getRuleMock(getQueryRuleParams()); - installedRule1.params.ruleId = 'rule-1'; - installedRule1.params.version = 1; - installedRule1.params.exceptionsList = [ - { - id: 'endpoint_list', - list_id: 'endpoint_list', - namespace_type: 'agnostic', - type: 'endpoint', - }, - ]; - - const [update] = getRulesToUpdate([ruleAsset1], rulesToMap([installedRule1])); - expect(update.exceptions_list).toEqual(installedRule1.params.exceptionsList); - }); - - test('should not remove an existing exception_list if the rule has an empty exceptions list for multiple rules', () => { - const ruleAsset1 = getPrebuiltRuleMock(); - ruleAsset1.exceptions_list = []; - ruleAsset1.rule_id = 'rule-1'; - ruleAsset1.version = 2; - - const ruleAsset2 = getPrebuiltRuleMock(); - ruleAsset2.exceptions_list = []; - ruleAsset2.rule_id = 'rule-2'; - ruleAsset2.version = 2; - - const installedRule1 = getRuleMock(getQueryRuleParams()); - installedRule1.params.ruleId = 'rule-1'; - installedRule1.params.version = 1; - installedRule1.params.exceptionsList = [ - { - id: 'endpoint_list', - list_id: 'endpoint_list', - namespace_type: 'agnostic', - type: 'endpoint', - }, - ]; - const installedRule2 = getRuleMock(getQueryRuleParams()); - installedRule2.params.ruleId = 'rule-2'; - installedRule2.params.version = 1; - installedRule2.params.exceptionsList = [ - { - id: 'endpoint_list', - list_id: 'endpoint_list', - namespace_type: 'agnostic', - type: 'endpoint', - }, - ]; - - const [update1, update2] = getRulesToUpdate( - [ruleAsset1, ruleAsset2], - rulesToMap([installedRule1, installedRule2]) - ); - expect(update1.exceptions_list).toEqual(installedRule1.params.exceptionsList); - expect(update2.exceptions_list).toEqual(installedRule2.params.exceptionsList); - }); - - test('should not remove an existing exception_list if the rule has an empty exceptions list for mixed rules', () => { - const ruleAsset1 = getPrebuiltRuleMock(); - ruleAsset1.exceptions_list = []; - ruleAsset1.rule_id = 'rule-1'; - ruleAsset1.version = 2; - - const ruleAsset2 = getPrebuiltRuleMock(); - ruleAsset2.exceptions_list = []; - ruleAsset2.rule_id = 'rule-2'; - ruleAsset2.version = 2; - ruleAsset2.exceptions_list = [ - { - id: 'second_list', - list_id: 'second_list', - namespace_type: 'single', - type: 'detection', - }, - ]; - - const installedRule1 = getRuleMock(getQueryRuleParams()); - installedRule1.params.ruleId = 'rule-1'; - installedRule1.params.version = 1; - installedRule1.params.exceptionsList = [ - { - id: 'endpoint_list', - list_id: 'endpoint_list', - namespace_type: 'agnostic', - type: 'endpoint', - }, - ]; - - const installedRule2 = getRuleMock(getQueryRuleParams()); - installedRule2.params.ruleId = 'rule-2'; - installedRule2.params.version = 1; - installedRule2.params.exceptionsList = [ - { - id: 'endpoint_list', - list_id: 'endpoint_list', - namespace_type: 'agnostic', - type: 'endpoint', - }, - ]; - - const [update1, update2] = getRulesToUpdate( - [ruleAsset1, ruleAsset2], - rulesToMap([installedRule1, installedRule2]) - ); - expect(update1.exceptions_list).toEqual(installedRule1.params.exceptionsList); - expect(update2.exceptions_list).toEqual([ - ...ruleAsset2.exceptions_list, - ...installedRule2.params.exceptionsList, - ]); - }); }); describe('filterInstalledRules', () => { @@ -365,110 +164,3 @@ describe('filterInstalledRules', () => { expect(shouldUpdate).toEqual(true); }); }); - -describe('mergeExceptionLists', () => { - test('should add back an exception_list if it was removed by the end user on an immutable rule during an upgrade', () => { - const ruleAsset1 = getPrebuiltRuleMock(); - ruleAsset1.exceptions_list = [ - { - id: 'endpoint_list', - list_id: 'endpoint_list', - namespace_type: 'agnostic', - type: 'endpoint', - }, - ]; - ruleAsset1.rule_id = 'rule-1'; - ruleAsset1.version = 2; - - const installedRule1 = getRuleMock(getQueryRuleParams()); - installedRule1.params.ruleId = 'rule-1'; - installedRule1.params.version = 1; - installedRule1.params.exceptionsList = []; - - const update = mergeExceptionLists(ruleAsset1, rulesToMap([installedRule1])); - expect(update.exceptions_list).toEqual(ruleAsset1.exceptions_list); - }); - - test('should not remove an additional exception_list if an additional one was added by the end user on an immutable rule during an upgrade', () => { - const ruleAsset1 = getPrebuiltRuleMock(); - ruleAsset1.exceptions_list = [ - { - id: 'endpoint_list', - list_id: 'endpoint_list', - namespace_type: 'agnostic', - type: 'endpoint', - }, - ]; - ruleAsset1.rule_id = 'rule-1'; - ruleAsset1.version = 2; - - const installedRule1 = getRuleMock(getQueryRuleParams()); - installedRule1.params.ruleId = 'rule-1'; - installedRule1.params.version = 1; - installedRule1.params.exceptionsList = [ - { - id: 'second_exception_list', - list_id: 'some-other-id', - namespace_type: 'single', - type: 'detection', - }, - ]; - - const update = mergeExceptionLists(ruleAsset1, rulesToMap([installedRule1])); - expect(update.exceptions_list).toEqual([ - ...ruleAsset1.exceptions_list, - ...installedRule1.params.exceptionsList, - ]); - }); - - test('should not remove an existing exception_list if they are the same between the current installed one and the upgraded one', () => { - const ruleAsset1 = getPrebuiltRuleMock(); - ruleAsset1.exceptions_list = [ - { - id: 'endpoint_list', - list_id: 'endpoint_list', - namespace_type: 'agnostic', - type: 'endpoint', - }, - ]; - ruleAsset1.rule_id = 'rule-1'; - ruleAsset1.version = 2; - - const installedRule1 = getRuleMock(getQueryRuleParams()); - installedRule1.params.ruleId = 'rule-1'; - installedRule1.params.version = 1; - installedRule1.params.exceptionsList = [ - { - id: 'endpoint_list', - list_id: 'endpoint_list', - namespace_type: 'agnostic', - type: 'endpoint', - }, - ]; - - const update = mergeExceptionLists(ruleAsset1, rulesToMap([installedRule1])); - expect(update.exceptions_list).toEqual(ruleAsset1.exceptions_list); - }); - - test('should not remove an existing exception_list if the rule has an empty exceptions list', () => { - const ruleAsset1 = getPrebuiltRuleMock(); - ruleAsset1.exceptions_list = []; - ruleAsset1.rule_id = 'rule-1'; - ruleAsset1.version = 2; - - const installedRule1 = getRuleMock(getQueryRuleParams()); - installedRule1.params.ruleId = 'rule-1'; - installedRule1.params.version = 1; - installedRule1.params.exceptionsList = [ - { - id: 'endpoint_list', - list_id: 'endpoint_list', - namespace_type: 'agnostic', - type: 'endpoint', - }, - ]; - - const update = mergeExceptionLists(ruleAsset1, rulesToMap([installedRule1])); - expect(update.exceptions_list).toEqual(installedRule1.params.exceptionsList); - }); -}); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/get_rules_to_update.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/get_rules_to_update.ts index e25ca4f1348e1..dac43534e7420 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/get_rules_to_update.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/get_rules_to_update.ts @@ -19,9 +19,9 @@ export const getRulesToUpdate = ( latestPrebuiltRules: PrebuiltRuleAsset[], installedRules: Map ) => { - return latestPrebuiltRules - .filter((latestRule) => filterInstalledRules(latestRule, installedRules)) - .map((latestRule) => mergeExceptionLists(latestRule, installedRules)); + return latestPrebuiltRules.filter((latestRule) => + filterInstalledRules(latestRule, installedRules) + ); }; /** @@ -38,33 +38,3 @@ export const filterInstalledRules = ( return !!installedRule && installedRule.params.version < latestPrebuiltRule.version; }; - -/** - * Given a rule from the file system and the set of installed rules this will merge the exception lists - * from the installed rules onto the rules from the file system. - * @param latestPrebuiltRule The latest prepackaged rule version that might have exceptions_lists - * @param installedRules The installed rules which might have user driven exceptions_lists - */ -export const mergeExceptionLists = ( - latestPrebuiltRule: PrebuiltRuleAsset, - installedRules: Map -): PrebuiltRuleAsset => { - if (latestPrebuiltRule.exceptions_list != null) { - const installedRule = installedRules.get(latestPrebuiltRule.rule_id); - - if (installedRule != null && installedRule.params.exceptionsList != null) { - const installedExceptionList = installedRule.params.exceptionsList; - const fileSystemExceptions = latestPrebuiltRule.exceptions_list.filter((potentialDuplicate) => - installedExceptionList.every((item) => item.list_id !== potentialDuplicate.list_id) - ); - return { - ...latestPrebuiltRule, - exceptions_list: [...fileSystemExceptions, ...installedRule.params.exceptionsList], - }; - } else { - return latestPrebuiltRule; - } - } else { - return latestPrebuiltRule; - } -}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.upgrade_prebuilt_rule.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.upgrade_prebuilt_rule.test.ts index 3860fed1dabe6..eed63e9bd0aa3 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.upgrade_prebuilt_rule.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.upgrade_prebuilt_rule.test.ts @@ -168,6 +168,23 @@ describe('DetectionRulesClient.upgradePrebuiltRule', () => { }; // Installed version is "eql" const installedRule = getRulesEqlSchemaMock(); + installedRule.actions = [ + { + group: 'default', + id: 'test_id', + action_type_id: '.index', + params: {}, + }, + ]; + installedRule.exceptions_list = [ + { + id: 'exception_list', + list_id: 'some-id', + namespace_type: 'single', + type: 'detection', + }, + ]; + beforeEach(() => { (getRuleByRuleId as jest.Mock).mockResolvedValue(installedRule); }); @@ -175,6 +192,56 @@ describe('DetectionRulesClient.upgradePrebuiltRule', () => { it('patches the existing rule with the new params from the rule asset', async () => { rulesClient.update.mockResolvedValue(getRuleMock(getEqlRuleParams())); + await detectionRulesClient.upgradePrebuiltRule({ ruleAsset }); + expect(rulesClient.update).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + name: ruleAsset.name, + tags: ruleAsset.tags, + // actions are kept from original rule + actions: [ + expect.objectContaining({ + actionTypeId: '.index', + group: 'default', + id: 'test_id', + params: {}, + }), + ], + params: expect.objectContaining({ + index: ruleAsset.index, + description: ruleAsset.description, + exceptionsList: [ + { + id: 'exception_list', + list_id: 'some-id', + namespace_type: 'single', + type: 'detection', + }, + ], + }), + }), + id: installedRule.id, + }) + ); + }); + + it('merges exceptions lists for existing rule and new rule asset', async () => { + rulesClient.update.mockResolvedValue(getRuleMock(getEqlRuleParams())); + ruleAsset.exceptions_list = [ + { + id: 'exception_list', + list_id: 'some-id', + namespace_type: 'single', + type: 'detection', + }, + { + id: 'second_exception_list', + list_id: 'some-other-id', + namespace_type: 'single', + type: 'detection', + }, + ]; + await detectionRulesClient.upgradePrebuiltRule({ ruleAsset }); expect(rulesClient.update).toHaveBeenCalledWith( expect.objectContaining({ @@ -184,6 +251,20 @@ describe('DetectionRulesClient.upgradePrebuiltRule', () => { params: expect.objectContaining({ index: ruleAsset.index, description: ruleAsset.description, + exceptionsList: [ + { + id: 'second_exception_list', + list_id: 'some-other-id', + namespace_type: 'single', + type: 'detection', + }, + { + id: 'exception_list', + list_id: 'some-id', + namespace_type: 'single', + type: 'detection', + }, + ], }), }), id: installedRule.id, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/upgrade_prebuilt_rule.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/upgrade_prebuilt_rule.ts index 64486bed14304..7c2bda89180e2 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/upgrade_prebuilt_rule.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/upgrade_prebuilt_rule.ts @@ -15,7 +15,7 @@ import type { IPrebuiltRuleAssetsClient } from '../../../../prebuilt_rules/logic import { convertAlertingRuleToRuleResponse } from '../converters/convert_alerting_rule_to_rule_response'; import { convertRuleResponseToAlertingRule } from '../converters/convert_rule_response_to_alerting_rule'; import { applyRuleUpdate } from '../mergers/apply_rule_update'; -import { ClientError, validateMlAuth } from '../utils'; +import { ClientError, validateMlAuth, mergeExceptionLists } from '../utils'; import { createRule } from './create_rule'; import { getRuleByRuleId } from './get_rule_by_rule_id'; @@ -75,9 +75,16 @@ export const upgradePrebuiltRule = async ({ ruleUpdate: ruleAsset, }); + // We want to preserve existing actions from existing rule on upgrade + if (existingRule.actions.length) { + updatedRule.actions = existingRule.actions; + } + + const updatedRuleWithMergedExceptions = mergeExceptionLists(updatedRule, existingRule); + const updatedInternalRule = await rulesClient.update({ id: existingRule.id, - data: convertRuleResponseToAlertingRule(updatedRule, actionsClient), + data: convertRuleResponseToAlertingRule(updatedRuleWithMergedExceptions, actionsClient), }); return convertAlertingRuleToRuleResponse(updatedInternalRule); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/utils.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/utils.test.ts new file mode 100644 index 0000000000000..8f8aff876a950 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/utils.test.ts @@ -0,0 +1,116 @@ +/* + * 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 { getRulesSchemaMock } from '../../../../../../common/api/detection_engine/model/rule_schema/rule_response_schema.mock'; +import { mergeExceptionLists } from './utils'; + +describe('mergeExceptionLists', () => { + test('should add back an exception_list if it was removed by the end user on an immutable rule during an upgrade', () => { + const ruleAsset1 = getRulesSchemaMock(); + ruleAsset1.exceptions_list = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; + ruleAsset1.rule_id = 'rule-1'; + ruleAsset1.version = 2; + + const installedRule1 = getRulesSchemaMock(); + installedRule1.rule_id = 'rule-1'; + installedRule1.version = 1; + installedRule1.exceptions_list = []; + + const update = mergeExceptionLists(ruleAsset1, installedRule1); + expect(update.exceptions_list).toEqual(ruleAsset1.exceptions_list); + }); + + test('should not remove an additional exception_list if an additional one was added by the end user on an immutable rule during an upgrade', () => { + const ruleAsset1 = getRulesSchemaMock(); + ruleAsset1.exceptions_list = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; + ruleAsset1.rule_id = 'rule-1'; + ruleAsset1.version = 2; + + const installedRule1 = getRulesSchemaMock(); + installedRule1.rule_id = 'rule-1'; + installedRule1.version = 1; + installedRule1.exceptions_list = [ + { + id: 'second_exception_list', + list_id: 'some-other-id', + namespace_type: 'single', + type: 'detection', + }, + ]; + + const update = mergeExceptionLists(ruleAsset1, installedRule1); + expect(update.exceptions_list).toEqual([ + ...ruleAsset1.exceptions_list, + ...installedRule1.exceptions_list, + ]); + }); + + test('should not remove an existing exception_list if they are the same between the current installed one and the upgraded one', () => { + const ruleAsset1 = getRulesSchemaMock(); + ruleAsset1.exceptions_list = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; + ruleAsset1.rule_id = 'rule-1'; + ruleAsset1.version = 2; + + const installedRule1 = getRulesSchemaMock(); + installedRule1.rule_id = 'rule-1'; + installedRule1.version = 1; + installedRule1.exceptions_list = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; + + const update = mergeExceptionLists(ruleAsset1, installedRule1); + expect(update.exceptions_list).toEqual(ruleAsset1.exceptions_list); + }); + + test('should not remove an existing exception_list if the rule has an empty exceptions list', () => { + const ruleAsset1 = getRulesSchemaMock(); + ruleAsset1.exceptions_list = []; + ruleAsset1.rule_id = 'rule-1'; + ruleAsset1.version = 2; + + const installedRule1 = getRulesSchemaMock(); + installedRule1.rule_id = 'rule-1'; + installedRule1.version = 1; + installedRule1.exceptions_list = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; + + const update = mergeExceptionLists(ruleAsset1, installedRule1); + expect(update.exceptions_list).toEqual(installedRule1.exceptions_list); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/utils.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/utils.ts index 3b0fcace501cd..61e1b73ec8c51 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/utils.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/utils.ts @@ -61,3 +61,35 @@ export class RuleResponseValidationError extends Error { this.ruleId = ruleId; } } + +/** + * Given a rule from the file system and the set of installed rules this will merge the exception lists + * from the installed rules onto the rules from the file system. + * @param latestPrebuiltRule The latest prepackaged rule version that might have exceptions_lists + * @param existingRule The installed rules which might have user driven exceptions_lists + */ +export const mergeExceptionLists = ( + latestPrebuiltRule: RuleResponse, + existingRule: RuleResponse +): RuleResponse => { + if (latestPrebuiltRule.exceptions_list != null) { + if (existingRule.exceptions_list != null) { + const uniqueExceptionsLists = latestPrebuiltRule.exceptions_list.filter( + (potentialDuplicateList) => + existingRule.exceptions_list.every( + (list) => list.list_id !== potentialDuplicateList.list_id + ) + ); + return { + ...latestPrebuiltRule, + exceptions_list: [...uniqueExceptionsLists, ...existingRule.exceptions_list], + }; + } else { + return latestPrebuiltRule; + } + } else { + // Carry over the previous version's exception list + latestPrebuiltRule.exceptions_list = existingRule.exceptions_list; + return latestPrebuiltRule; + } +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/install_prebuilt_rules_with_historical_versions.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/install_prebuilt_rules_with_historical_versions.ts index 21e546ff91bdc..79180e6ef2f2f 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/install_prebuilt_rules_with_historical_versions.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/install_prebuilt_rules_with_historical_versions.ts @@ -16,6 +16,7 @@ import { getPrebuiltRulesStatus, installPrebuiltRules, getInstalledRules, + getWebHookAction, } from '../../../../utils'; import { deleteAllRules, deleteRule } from '../../../../../../../common/utils/security_solution'; @@ -23,6 +24,7 @@ export default ({ getService }: FtrProviderContext): void => { const es = getService('es'); const supertest = getService('supertest'); const log = getService('log'); + const securitySolutionApi = getService('securitySolutionApi'); describe('@ess @serverless @skipInServerlessMKI install prebuilt rules from package with historical versions with mock rule assets', () => { const getRuleAssetSavedObjects = () => [ @@ -98,6 +100,117 @@ export default ({ getService }: FtrProviderContext): void => { expect(response.rules_installed).toBe(1); expect(response.rules_updated).toBe(0); }); + + it('should not overwrite existing actions', async () => { + // Install prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects(es, [ + createRuleAssetSavedObject({ rule_id: 'rule-1', version: 1 }), + ]); + await installPrebuiltRulesAndTimelines(es, supertest); + + // create connector/action + const createConnector = async (payload: Record) => + ( + await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'true') + .send(payload) + .expect(200) + ).body; + + const createWebHookConnector = () => createConnector(getWebHookAction()); + + const webHookAction = await createWebHookConnector(); + + const defaultRuleAction = { + id: webHookAction.id, + action_type_id: '.webhook' as const, + group: 'default' as const, + params: { + body: '{"test":"a default action"}', + }, + frequency: { + notifyWhen: 'onThrottleInterval' as const, + summary: true, + throttle: '1h' as const, + }, + uuid: 'd487ec3d-05f2-44ad-8a68-11c97dc92202', + }; + + await securitySolutionApi + .patchRule({ + body: { + rule_id: 'rule-1', + actions: [defaultRuleAction], + }, + }) + .expect(200); + + // Install new rule version + await createHistoricalPrebuiltRuleAssetSavedObjects(es, [ + createRuleAssetSavedObject({ rule_id: 'rule-1', version: 2 }), + ]); + + // Install/update prebuilt rules again + const response = await installPrebuiltRulesAndTimelines(es, supertest); + expect(response.rules_installed).toBe(0); + expect(response.rules_updated).toBe(1); + + const { body: prebuiltRule } = await securitySolutionApi.readRule({ + query: { rule_id: 'rule-1' }, + }); + + // Check the actions field of existing prebuilt rules is not overwritten + expect(prebuiltRule.actions).toEqual([defaultRuleAction]); + }); + + it('should not overwrite existing exceptions lists', async () => { + // Install prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects(es, [ + createRuleAssetSavedObject({ rule_id: 'rule-1', version: 1 }), + ]); + await installPrebuiltRulesAndTimelines(es, supertest); + + await securitySolutionApi + .patchRule({ + body: { + rule_id: 'rule-1', + exceptions_list: [ + { + id: 'some_uuid', + list_id: 'list_id_single', + namespace_type: 'single', + type: 'detection', + }, + ], + }, + }) + .expect(200); + + // Install new rule version + await createHistoricalPrebuiltRuleAssetSavedObjects(es, [ + createRuleAssetSavedObject({ rule_id: 'rule-1', version: 2 }), + ]); + + // Install/update prebuilt rules again + const response = await installPrebuiltRulesAndTimelines(es, supertest); + expect(response.rules_installed).toBe(0); + expect(response.rules_updated).toBe(1); + + const { body: prebuiltRule } = await securitySolutionApi.readRule({ + query: { rule_id: 'rule-1' }, + }); + + // Check the exceptions_list field of existing prebuilt rules is not overwritten + expect(prebuiltRule.exceptions_list).toEqual([ + expect.objectContaining({ + id: 'some_uuid', + list_id: 'list_id_single', + namespace_type: 'single', + type: 'detection', + }), + ]); + }); }); describe('using current endpoint', () => {