diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/__mocks__/prebuilt_rule_objects_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/__mocks__/prebuilt_rule_objects_client.ts new file mode 100644 index 0000000000000..c6077050ed7ca --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/__mocks__/prebuilt_rule_objects_client.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const createPrebuiltRuleObjectsClient = () => { + return { + fetchInstalledRulesByIds: jest.fn(), + fetchInstalledRules: jest.fn(), + fetchInstalledRuleVersionsByIds: jest.fn(), + fetchInstalledRuleVersions: jest.fn(), + }; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/import_rules/route.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/import_rules/route.ts index de2c05802df1c..e71d891d409b2 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/import_rules/route.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/import_rules/route.ts @@ -40,6 +40,7 @@ import { } from '../../../utils/utils'; import { RULE_MANAGEMENT_IMPORT_EXPORT_SOCKET_TIMEOUT_MS } from '../../timeouts'; import { PrebuiltRulesCustomizationDisabledReason } from '../../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status'; +import { createPrebuiltRuleObjectsClient } from '../../../../prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client'; const CHUNK_PARSED_OBJECT_SIZE = 50; @@ -86,6 +87,7 @@ export const importRulesRoute = (router: SecuritySolutionPluginRouter, config: C 'licensing', ]); + const rulesClient = await ctx.alerting.getRulesClient(); const detectionRulesClient = ctx.securitySolution.getDetectionRulesClient(); const ruleCustomizationStatus = detectionRulesClient.getRuleCustomizationStatus(); const actionsClient = ctx.actions.getActionsClient(); @@ -159,6 +161,7 @@ export const importRulesRoute = (router: SecuritySolutionPluginRouter, config: C config, context: ctx.securitySolution, prebuiltRuleAssetsClient: createPrebuiltRuleAssetsClient(savedObjectsClient), + prebuiltRuleObjectsClient: createPrebuiltRuleObjectsClient(rulesClient), ruleCustomizationStatus: detectionRulesClient.getRuleCustomizationStatus(), }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/bulk_edit_rules.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/bulk_edit_rules.ts index c907b90bb2ebe..4a440c4b211c2 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/bulk_edit_rules.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/bulk_edit_rules.ts @@ -70,8 +70,8 @@ export const bulkEditRules = async ({ const result = await rulesClient.bulkEdit({ ids: rules.map((rule) => rule.id), operations, - paramsModifier: async (rule) => { - const ruleParams = rule.params; + paramsModifier: async (currentRule) => { + const ruleParams = currentRule.params; await validateBulkEditRule({ mlAuthz, @@ -85,23 +85,23 @@ export const bulkEditRules = async ({ paramsActions ); - // Update rule source - const updatedRule = { - ...rule, + const nextRule = convertAlertingRuleToRuleResponse({ + ...currentRule, params: modifiedParams, - }; - const ruleResponse = convertAlertingRuleToRuleResponse(updatedRule); + }); + let isCustomized = false; - if (ruleResponse.immutable === true) { + if (nextRule.immutable === true) { isCustomized = calculateIsCustomized({ - baseRule: baseVersionsMap.get(ruleResponse.rule_id), - nextRule: ruleResponse, + baseRule: baseVersionsMap.get(nextRule.rule_id), + currentRule: convertAlertingRuleToRuleResponse(currentRule), + nextRule, ruleCustomizationStatus, }); } const ruleSource = - ruleResponse.immutable === true + nextRule.immutable === true ? { type: 'external' as const, isCustomized, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_patch.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_patch.ts index 8468eec5dbc64..b8ab25992e0b9 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_patch.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_patch.ts @@ -123,7 +123,8 @@ export const applyRulePatch = async ({ }; nextRule.rule_source = await calculateRuleSource({ - rule: nextRule, + nextRule, + currentRule: existingRule, prebuiltRuleAssetClient, ruleCustomizationStatus, }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_update.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_update.ts index 9475a3f5b7f55..886d7e2222664 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_update.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_update.ts @@ -47,7 +47,8 @@ export const applyRuleUpdate = async ({ }; nextRule.rule_source = await calculateRuleSource({ - rule: nextRule, + nextRule, + currentRule: existingRule, prebuiltRuleAssetClient, ruleCustomizationStatus, }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_is_customized.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_is_customized.ts index 8b0c5a23a337d..e784c150e83ce 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_is_customized.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_is_customized.ts @@ -19,34 +19,68 @@ import { interface CalculateIsCustomizedArgs { baseRule: PrebuiltRuleAsset | undefined; nextRule: RuleResponse; + // Current rule can be undefined in case of importing a prebuilt rule that is not installed + currentRule: RuleResponse | undefined; ruleCustomizationStatus: PrebuiltRulesCustomizationStatus; } export function calculateIsCustomized({ baseRule, nextRule, + currentRule, ruleCustomizationStatus, }: CalculateIsCustomizedArgs) { if ( ruleCustomizationStatus.customizationDisabledReason === PrebuiltRulesCustomizationDisabledReason.FeatureFlag ) { - // We don't want to accidentally mark rules as customized when customization is disabled. + // We don't want to accidentally mark rules as customized when customization + // is disabled. return false; } - if (baseRule == null) { - // If the base version is missing, we consider the rule to be customized + if (baseRule) { + // Base version is available, so we can determine the customization status + // by comparing the base version with the next version + return areRulesEqual(convertPrebuiltRuleAssetToRuleResponse(baseRule), nextRule) === false; + } + // Base version is not available, apply a heuristic to determine the + // customization status + + if (currentRule == null) { + // Current rule is not installed and base rule is not available, so we can't + // determine if the rule is customized. Defaulting to false. + return false; + } + + if ( + currentRule.rule_source.type === 'external' && + currentRule.rule_source.is_customized === true + ) { + // If the rule was previously customized, there's no way to determine + // whether the customization remained or was reverted. Keeping it as + // customized in this case. return true; } - const baseRuleWithDefaults = convertPrebuiltRuleAssetToRuleResponse(baseRule); + // If the rule has not been customized before, its customization status can be + // determined by comparing the current version with the next version. + return areRulesEqual(currentRule, nextRule) === false; +} +/** + * A helper function to determine if two rules are equal + * + * @param ruleA + * @param ruleB + * @returns true if all rule fields are equal, false otherwise + */ +function areRulesEqual(ruleA: RuleResponse, ruleB: RuleResponse) { const fieldsDiff = calculateRuleFieldsDiff({ base_version: MissingVersion, - current_version: convertRuleToDiffable(baseRuleWithDefaults), - target_version: convertRuleToDiffable(nextRule), + current_version: convertRuleToDiffable(ruleA), + target_version: convertRuleToDiffable(ruleB), }); - return Object.values(fieldsDiff).some((diff) => diff.has_update); + return Object.values(fieldsDiff).every((diff) => diff.has_update === false); } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_rule_source.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_rule_source.test.ts index 0a7d51895e4bb..b1b9ff306a68b 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_rule_source.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_rule_source.test.ts @@ -48,7 +48,8 @@ describe('calculateRuleSource', () => { const result = await calculateRuleSource({ prebuiltRuleAssetClient, - rule, + nextRule: rule, + currentRule: undefined, ruleCustomizationStatus, }); expect(result).toEqual({ @@ -65,7 +66,8 @@ describe('calculateRuleSource', () => { const result = await calculateRuleSource({ prebuiltRuleAssetClient, - rule, + nextRule: rule, + currentRule: rule, ruleCustomizationStatus, }); expect(result).toEqual( @@ -86,7 +88,8 @@ describe('calculateRuleSource', () => { const result = await calculateRuleSource({ prebuiltRuleAssetClient, - rule, + nextRule: rule, + currentRule: rule, ruleCustomizationStatus, }); expect(result).toEqual( @@ -109,7 +112,8 @@ describe('calculateRuleSource', () => { const result = await calculateRuleSource({ prebuiltRuleAssetClient, - rule, + nextRule: rule, + currentRule: rule, ruleCustomizationStatus, }); expect(result).toEqual( @@ -130,7 +134,8 @@ describe('calculateRuleSource', () => { const result = await calculateRuleSource({ prebuiltRuleAssetClient, - rule, + nextRule: rule, + currentRule: rule, ruleCustomizationStatus: { isRulesCustomizationEnabled: false, customizationDisabledReason: PrebuiltRulesCustomizationDisabledReason.FeatureFlag, @@ -154,7 +159,8 @@ describe('calculateRuleSource', () => { const result = await calculateRuleSource({ prebuiltRuleAssetClient, - rule, + nextRule: rule, + currentRule: rule, ruleCustomizationStatus: { isRulesCustomizationEnabled: false, customizationDisabledReason: PrebuiltRulesCustomizationDisabledReason.License, @@ -167,4 +173,107 @@ describe('calculateRuleSource', () => { }) ); }); + + describe('missing base versions', () => { + it('return is_customized false when the base version and current version are missing', async () => { + const rule = getSampleRule(); + rule.immutable = true; + + // No base version + prebuiltRuleAssetClient.fetchAssetsByVersion.mockResolvedValueOnce([]); + + const result = await calculateRuleSource({ + prebuiltRuleAssetClient, + nextRule: rule, + currentRule: undefined, + ruleCustomizationStatus, + }); + expect(result).toEqual( + expect.objectContaining({ + type: 'external', + is_customized: false, + }) + ); + }); + + it('returns is_customized true when the current version is already customized', async () => { + const rule = getSampleRule(); + rule.immutable = true; + rule.rule_source = { + type: 'external', + is_customized: true, + }; + + // No base version + prebuiltRuleAssetClient.fetchAssetsByVersion.mockResolvedValueOnce([]); + + const result = await calculateRuleSource({ + prebuiltRuleAssetClient, + nextRule: rule, + currentRule: rule, + ruleCustomizationStatus, + }); + expect(result).toEqual( + expect.objectContaining({ + type: 'external', + is_customized: true, + }) + ); + }); + + it('returns is_customized false when the current version is not customized and the next version has no changes', async () => { + const rule = getSampleRule(); + rule.immutable = true; + rule.rule_source = { + type: 'external', + is_customized: false, + }; + + // No base version + prebuiltRuleAssetClient.fetchAssetsByVersion.mockResolvedValueOnce([]); + + const result = await calculateRuleSource({ + prebuiltRuleAssetClient, + nextRule: rule, + currentRule: rule, + ruleCustomizationStatus, + }); + expect(result).toEqual( + expect.objectContaining({ + type: 'external', + is_customized: false, + }) + ); + }); + + it('returns is_customized true when the current version is not customized and the next version has changes', async () => { + const rule = getSampleRule(); + rule.immutable = true; + rule.rule_source = { + type: 'external', + is_customized: false, + }; + + const nextRule = { + ...rule, + name: 'Updated name', + }; + + // No base version + prebuiltRuleAssetClient.fetchAssetsByVersion.mockResolvedValueOnce([]); + + const result = await calculateRuleSource({ + prebuiltRuleAssetClient, + nextRule, + currentRule: rule, + ruleCustomizationStatus, + }); + expect(result).toEqual( + expect.objectContaining({ + type: 'external', + is_customized: true, + }) + ); + }); + }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_rule_source.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_rule_source.ts index fe628430ef300..3603a7aa7ff6c 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_rule_source.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_rule_source.ts @@ -16,29 +16,32 @@ import { calculateIsCustomized } from './calculate_is_customized'; interface CalculateRuleSourceProps { prebuiltRuleAssetClient: IPrebuiltRuleAssetsClient; - rule: RuleResponse; + nextRule: RuleResponse; + currentRule: RuleResponse | undefined; ruleCustomizationStatus: PrebuiltRulesCustomizationStatus; } export async function calculateRuleSource({ prebuiltRuleAssetClient, - rule, + nextRule, + currentRule, ruleCustomizationStatus, }: CalculateRuleSourceProps): Promise { - if (rule.immutable) { + if (nextRule.immutable) { // This is a prebuilt rule and, despite the name, they are not immutable. So // we need to recalculate `ruleSource.isCustomized` based on the rule's contents. const prebuiltRulesResponse = await prebuiltRuleAssetClient.fetchAssetsByVersion([ { - rule_id: rule.rule_id, - version: rule.version, + rule_id: nextRule.rule_id, + version: nextRule.version, }, ]); const baseRule: PrebuiltRuleAsset | undefined = prebuiltRulesResponse.at(0); const isCustomized = calculateIsCustomized({ baseRule, - nextRule: rule, + nextRule, + currentRule, ruleCustomizationStatus, }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/calculate_rule_source_for_import.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/calculate_rule_source_for_import.test.ts index 734093e44576f..49ab9e1da23fb 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/calculate_rule_source_for_import.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/calculate_rule_source_for_import.test.ts @@ -17,7 +17,8 @@ const ruleCustomizationStatus: PrebuiltRulesCustomizationStatus = { describe('calculateRuleSourceForImport', () => { it('calculates as internal if no asset is found', () => { const result = calculateRuleSourceForImport({ - rule: getRulesSchemaMock(), + importedRule: getRulesSchemaMock(), + currentRule: undefined, prebuiltRuleAssetsByRuleId: {}, isKnownPrebuiltRule: false, ruleCustomizationStatus, @@ -31,12 +32,58 @@ describe('calculateRuleSourceForImport', () => { }); }); - it('calculates as modified external type if an asset is found without a matching version', () => { + it('calculates as not modified external type if an asset is found without a matching version and no current rule present', () => { const rule = getRulesSchemaMock(); rule.rule_id = 'rule_id'; const result = calculateRuleSourceForImport({ - rule, + importedRule: rule, + currentRule: undefined, + prebuiltRuleAssetsByRuleId: {}, + isKnownPrebuiltRule: true, + ruleCustomizationStatus, + }); + + expect(result).toEqual({ + ruleSource: { + type: 'external', + is_customized: false, + }, + immutable: true, + }); + }); + + it('calculates as non modified external type if an asset is found without a matching version and current rule present without changes', () => { + const rule = getRulesSchemaMock(); + rule.rule_id = 'rule_id'; + + const result = calculateRuleSourceForImport({ + importedRule: rule, + currentRule: rule, + prebuiltRuleAssetsByRuleId: {}, + isKnownPrebuiltRule: true, + ruleCustomizationStatus, + }); + + expect(result).toEqual({ + ruleSource: { + type: 'external', + is_customized: false, + }, + immutable: true, + }); + }); + + it('calculates as modified external type if an asset is found without a matching version and current rule present with changes', () => { + const rule = getRulesSchemaMock(); + rule.rule_id = 'rule_id'; + + const result = calculateRuleSourceForImport({ + importedRule: rule, + currentRule: { + ...rule, + name: 'new name', + }, prebuiltRuleAssetsByRuleId: {}, isKnownPrebuiltRule: true, ruleCustomizationStatus, @@ -57,7 +104,8 @@ describe('calculateRuleSourceForImport', () => { const prebuiltRuleAssetsByRuleId = { rule_id: getPrebuiltRuleMock({ rule_id: 'rule_id' }) }; const result = calculateRuleSourceForImport({ - rule, + importedRule: rule, + currentRule: undefined, prebuiltRuleAssetsByRuleId, isKnownPrebuiltRule: true, ruleCustomizationStatus, @@ -78,7 +126,8 @@ describe('calculateRuleSourceForImport', () => { const prebuiltRuleAssetsByRuleId = { rule_id: getPrebuiltRuleMock(rule) }; const result = calculateRuleSourceForImport({ - rule, + importedRule: rule, + currentRule: undefined, prebuiltRuleAssetsByRuleId, isKnownPrebuiltRule: true, ruleCustomizationStatus, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/calculate_rule_source_for_import.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/calculate_rule_source_for_import.ts index 6476c4dbd5779..440bad4f95325 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/calculate_rule_source_for_import.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/calculate_rule_source_for_import.ts @@ -6,12 +6,13 @@ */ import type { + RuleResponse, RuleSource, ValidatedRuleToImport, } from '../../../../../../common/api/detection_engine'; import type { PrebuiltRulesCustomizationStatus } from '../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status'; import type { PrebuiltRuleAsset } from '../../../prebuilt_rules'; -import { calculateRuleSourceFromAsset } from './calculate_rule_source_from_asset'; +import { calculateIsCustomized } from '../detection_rules_client/mergers/rule_source/calculate_is_customized'; import { convertRuleToImportToRuleResponse } from './converters/convert_rule_to_import_to_rule_response'; /** @@ -26,31 +27,44 @@ import { convertRuleToImportToRuleResponse } from './converters/convert_rule_to_ * @returns The calculated rule_source and immutable fields for the rule */ export const calculateRuleSourceForImport = ({ - rule, + importedRule, + currentRule, prebuiltRuleAssetsByRuleId, isKnownPrebuiltRule, ruleCustomizationStatus, }: { - rule: ValidatedRuleToImport; + importedRule: ValidatedRuleToImport; + currentRule: RuleResponse | undefined; prebuiltRuleAssetsByRuleId: Record; isKnownPrebuiltRule: boolean; ruleCustomizationStatus: PrebuiltRulesCustomizationStatus; }): { ruleSource: RuleSource; immutable: boolean } => { - const assetWithMatchingVersion = prebuiltRuleAssetsByRuleId[rule.rule_id]; + if (!isKnownPrebuiltRule) { + return { + ruleSource: { type: 'internal' }, + immutable: false, + }; + } + + const baseRule = prebuiltRuleAssetsByRuleId[importedRule.rule_id]; // We convert here so that RuleSource calculation can // continue to deal only with RuleResponses. The fields missing from the // incoming rule are not actually needed for the calculation, but only to // satisfy the type system. - const ruleResponseForImport = convertRuleToImportToRuleResponse(rule); - const ruleSource = calculateRuleSourceFromAsset({ - rule: ruleResponseForImport, - assetWithMatchingVersion, - isKnownPrebuiltRule, + const nextRule = convertRuleToImportToRuleResponse(importedRule); + + const isCustomized = calculateIsCustomized({ + baseRule, + nextRule, + currentRule, ruleCustomizationStatus, }); return { - ruleSource, - immutable: ruleSource.type === 'external', + ruleSource: { + type: 'external', + is_customized: isCustomized, + }, + immutable: true, }; }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/calculate_rule_source_from_asset.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/calculate_rule_source_from_asset.test.ts deleted file mode 100644 index 04074d65eab80..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/calculate_rule_source_from_asset.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * 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 { calculateRuleSourceFromAsset } from './calculate_rule_source_from_asset'; -import { getRulesSchemaMock } from '../../../../../../common/api/detection_engine/model/rule_schema/mocks'; -import { getPrebuiltRuleMock } from '../../../prebuilt_rules/mocks'; -import type { PrebuiltRulesCustomizationStatus } from '../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status'; - -const ruleCustomizationStatus: PrebuiltRulesCustomizationStatus = { - isRulesCustomizationEnabled: true, -}; - -describe('calculateRuleSourceFromAsset', () => { - it('calculates as internal if no asset is found', () => { - const result = calculateRuleSourceFromAsset({ - rule: getRulesSchemaMock(), - assetWithMatchingVersion: undefined, - isKnownPrebuiltRule: false, - ruleCustomizationStatus, - }); - - expect(result).toEqual({ - type: 'internal', - }); - }); - - it('calculates as customized external type if an asset is found matching rule_id but not version', () => { - const ruleToImport = getRulesSchemaMock(); - const result = calculateRuleSourceFromAsset({ - rule: ruleToImport, - assetWithMatchingVersion: undefined, - isKnownPrebuiltRule: true, - ruleCustomizationStatus, - }); - - expect(result).toEqual({ - type: 'external', - is_customized: true, - }); - }); - - describe('matching rule_id and version is found', () => { - it('calculates as customized external type if the imported rule has all fields unchanged from the asset', () => { - const ruleToImport = getRulesSchemaMock(); - const result = calculateRuleSourceFromAsset({ - rule: getRulesSchemaMock(), // version 1 - assetWithMatchingVersion: getPrebuiltRuleMock({ - ...ruleToImport, - version: 1, // version 1 (same version as imported rule) - // no other overwrites -> no differences - }), - isKnownPrebuiltRule: true, - ruleCustomizationStatus, - }); - - expect(result).toEqual({ - type: 'external', - is_customized: false, - }); - }); - - it('calculates as non-customized external type the imported rule has fields which differ from the asset', () => { - const ruleToImport = getRulesSchemaMock(); - const result = calculateRuleSourceFromAsset({ - rule: getRulesSchemaMock(), // version 1 - assetWithMatchingVersion: getPrebuiltRuleMock({ - ...ruleToImport, - version: 1, // version 1 (same version as imported rule) - name: 'Customized name', // mock a customization - }), - isKnownPrebuiltRule: true, - ruleCustomizationStatus, - }); - - expect(result).toEqual({ - type: 'external', - is_customized: true, - }); - }); - }); -}); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/calculate_rule_source_from_asset.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/calculate_rule_source_from_asset.ts deleted file mode 100644 index 190c21747b09d..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/calculate_rule_source_from_asset.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { RuleResponse, RuleSource } from '../../../../../../common/api/detection_engine'; -import type { PrebuiltRulesCustomizationStatus } from '../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status'; -import type { PrebuiltRuleAsset } from '../../../prebuilt_rules'; -import { calculateIsCustomized } from '../detection_rules_client/mergers/rule_source/calculate_is_customized'; - -/** - * Calculates rule_source for a rule based on two pieces of information: - * 1. The prebuilt rule asset that matches the specified rule_id and version - * 2. Whether a prebuilt rule with the specified rule_id is currently installed - * - * @param rule The rule for which rule_source is being calculated - * @param assetWithMatchingVersion The prebuilt rule asset that matches the specified rule_id and version - * @param isKnownPrebuiltRule Whether a prebuilt rule with the specified rule_id is currently installed - * - * @returns The calculated rule_source - */ -export const calculateRuleSourceFromAsset = ({ - rule, - assetWithMatchingVersion, - isKnownPrebuiltRule, - ruleCustomizationStatus, -}: { - rule: RuleResponse; - assetWithMatchingVersion: PrebuiltRuleAsset | undefined; - isKnownPrebuiltRule: boolean; - ruleCustomizationStatus: PrebuiltRulesCustomizationStatus; -}): RuleSource => { - if (!isKnownPrebuiltRule) { - return { - type: 'internal', - }; - } - - if (assetWithMatchingVersion == null) { - return { - type: 'external', - is_customized: ruleCustomizationStatus.isRulesCustomizationEnabled ? true : false, - }; - } - - const isCustomized = calculateIsCustomized({ - baseRule: assetWithMatchingVersion, - nextRule: rule, - ruleCustomizationStatus, - }); - - return { - type: 'external', - is_customized: isCustomized, - }; -}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/rule_source_importer/rule_source_importer.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/rule_source_importer/rule_source_importer.test.ts index 17d1854d65281..ccc0b062c747f 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/rule_source_importer/rule_source_importer.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/rule_source_importer/rule_source_importer.test.ts @@ -10,13 +10,14 @@ import type { ValidatedRuleToImport, } from '../../../../../../../common/api/detection_engine'; import { createPrebuiltRuleAssetsClient as createPrebuiltRuleAssetsClientMock } from '../../../../prebuilt_rules/logic/rule_assets/__mocks__/prebuilt_rule_assets_client'; +import { createPrebuiltRuleObjectsClient as createPrebuiltRuleObjectsClientMock } from '../../../../prebuilt_rules/logic/rule_objects/__mocks__/prebuilt_rule_objects_client'; import { createMockConfig, requestContextMock } from '../../../../routes/__mocks__'; import { getPrebuiltRuleMock } from '../../../../prebuilt_rules/mocks'; import { createRuleSourceImporter } from './rule_source_importer'; -import * as calculateRuleSourceModule from '../calculate_rule_source_for_import'; describe('ruleSourceImporter', () => { let ruleAssetsClientMock: ReturnType; + let ruleObjectsClientMock: ReturnType; let config: ReturnType; let context: ReturnType['securitySolution']; let ruleToImport: RuleToImport; @@ -30,12 +31,15 @@ describe('ruleSourceImporter', () => { ruleAssetsClientMock.fetchLatestAssets.mockResolvedValue([{}]); ruleAssetsClientMock.fetchLatestVersions.mockResolvedValue([]); ruleAssetsClientMock.fetchAssetsByVersion.mockResolvedValue([]); + ruleObjectsClientMock = createPrebuiltRuleObjectsClientMock(); + ruleObjectsClientMock.fetchInstalledRulesByIds.mockResolvedValue([]); ruleToImport = { rule_id: 'rule-1', version: 1 } as RuleToImport; subject = createRuleSourceImporter({ context, config, prebuiltRuleAssetsClient: ruleAssetsClientMock, + prebuiltRuleObjectsClient: ruleObjectsClientMock, ruleCustomizationStatus: { isRulesCustomizationEnabled: true }, }); }); @@ -112,7 +116,6 @@ describe('ruleSourceImporter', () => { describe('#calculateRuleSource()', () => { let rule: ValidatedRuleToImport; - let calculatorSpy: jest.SpyInstance; beforeEach(() => { rule = { rule_id: 'validated-rule', version: 1 } as ValidatedRuleToImport; @@ -124,23 +127,6 @@ describe('ruleSourceImporter', () => { getPrebuiltRuleMock({ rule_id: 'rule-2' }), getPrebuiltRuleMock({ rule_id: 'validated-rule' }), ]); - calculatorSpy = jest - .spyOn(calculateRuleSourceModule, 'calculateRuleSourceForImport') - .mockReturnValue({ ruleSource: { type: 'internal' }, immutable: false }); - }); - - it('invokes calculateRuleSourceForImport with the correct arguments', async () => { - await subject.setup([rule]); - await subject.calculateRuleSource(rule); - - expect(calculatorSpy).toHaveBeenCalledTimes(1); - expect(calculatorSpy).toHaveBeenCalledWith( - expect.objectContaining({ - rule, - prebuiltRuleAssetsByRuleId: { 'rule-1': expect.objectContaining({ rule_id: 'rule-1' }) }, - isKnownPrebuiltRule: true, - }) - ); }); it('throws an error if the rule is not known to the calculator', async () => { @@ -157,23 +143,5 @@ describe('ruleSourceImporter', () => { `"Rule validated-rule was not registered during setup."` ); }); - - describe('for rules set up without a version', () => { - it('invokes the calculator with the correct arguments', async () => { - await subject.setup([{ ...rule, version: undefined }]); - await subject.calculateRuleSource(rule); - - expect(calculatorSpy).toHaveBeenCalledTimes(1); - expect(calculatorSpy).toHaveBeenCalledWith( - expect.objectContaining({ - rule, - prebuiltRuleAssetsByRuleId: { - 'rule-1': expect.objectContaining({ rule_id: 'rule-1' }), - }, - isKnownPrebuiltRule: true, - }) - ); - }); - }); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/rule_source_importer/rule_source_importer.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/rule_source_importer/rule_source_importer.ts index af80d7f5e67fa..28388ce4d104f 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/rule_source_importer/rule_source_importer.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/rule_source_importer/rule_source_importer.ts @@ -15,6 +15,7 @@ import type { SecuritySolutionApiRequestHandlerContext } from '../../../../../../types'; import type { ConfigType } from '../../../../../../config'; import type { + RuleResponse, RuleToImport, ValidatedRuleToImport, } from '../../../../../../../common/api/detection_engine'; @@ -24,6 +25,7 @@ import { ensureLatestRulesPackageInstalled } from '../../../../prebuilt_rules/lo import { calculateRuleSourceForImport } from '../calculate_rule_source_for_import'; import type { CalculatedRuleSource, IRuleSourceImporter } from './rule_source_importer_interface'; import type { PrebuiltRulesCustomizationStatus } from '../../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status'; +import type { IPrebuiltRuleObjectsClient } from '../../../../prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client'; interface RuleSpecifier { rule_id: string; @@ -96,25 +98,30 @@ export class RuleSourceImporter implements IRuleSourceImporter { private context: SecuritySolutionApiRequestHandlerContext; private config: ConfigType; private ruleAssetsClient: IPrebuiltRuleAssetsClient; + private ruleObjectsClient: IPrebuiltRuleObjectsClient; private ruleCustomizationStatus: PrebuiltRulesCustomizationStatus; private latestPackagesInstalled: boolean = false; private matchingAssetsByRuleId: Record = {}; - private knownRules: RuleSpecifier[] = []; + private currentRulesById: Record = {}; + private rulesToImport: RuleSpecifier[] = []; private availableRuleAssetIds: Set = new Set(); constructor({ config, context, prebuiltRuleAssetsClient, + prebuiltRuleObjectsClient, ruleCustomizationStatus, }: { config: ConfigType; context: SecuritySolutionApiRequestHandlerContext; prebuiltRuleAssetsClient: IPrebuiltRuleAssetsClient; + prebuiltRuleObjectsClient: IPrebuiltRuleObjectsClient; ruleCustomizationStatus: PrebuiltRulesCustomizationStatus; }) { this.config = config; this.ruleAssetsClient = prebuiltRuleAssetsClient; + this.ruleObjectsClient = prebuiltRuleObjectsClient; this.context = context; this.ruleCustomizationStatus = ruleCustomizationStatus; } @@ -130,9 +137,12 @@ export class RuleSourceImporter implements IRuleSourceImporter { this.latestPackagesInstalled = true; } - this.knownRules = rules.map((rule) => ({ rule_id: rule.rule_id, version: rule.version })); + this.rulesToImport = rules.map((rule) => ({ rule_id: rule.rule_id, version: rule.version })); this.matchingAssetsByRuleId = await this.fetchMatchingAssetsByRuleId(); this.availableRuleAssetIds = new Set(await this.fetchAvailableRuleAssetIds()); + this.currentRulesById = await this.fetchInstalledRulesByIds( + this.rulesToImport.map((rule) => rule.rule_id) + ); } public isPrebuiltRule(rule: RuleToImport): boolean { @@ -145,7 +155,8 @@ export class RuleSourceImporter implements IRuleSourceImporter { this.validateRuleInput(rule); return calculateRuleSourceForImport({ - rule, + importedRule: rule, + currentRule: this.currentRulesById[rule.rule_id], prebuiltRuleAssetsByRuleId: this.matchingAssetsByRuleId, isKnownPrebuiltRule: this.availableRuleAssetIds.has(rule.rule_id), ruleCustomizationStatus: this.ruleCustomizationStatus, @@ -155,7 +166,7 @@ export class RuleSourceImporter implements IRuleSourceImporter { private async fetchMatchingAssetsByRuleId(): Promise> { this.validateSetupState(); const matchingAssets = await fetchMatchingAssets({ - rules: this.knownRules, + rules: this.rulesToImport, ruleAssetsClient: this.ruleAssetsClient, }); @@ -165,11 +176,18 @@ export class RuleSourceImporter implements IRuleSourceImporter { }, {}); } + private async fetchInstalledRulesByIds(ruleIds: string[]): Promise> { + const currentRules = await this.ruleObjectsClient.fetchInstalledRulesByIds({ + ruleIds, + }); + return Object.fromEntries(currentRules.map((rule) => [rule.rule_id, rule])); + } + private async fetchAvailableRuleAssetIds(): Promise { this.validateSetupState(); return fetchAvailableRuleAssetIds({ - rules: this.knownRules, + rules: this.rulesToImport, ruleAssetsClient: this.ruleAssetsClient, }); } @@ -185,7 +203,7 @@ export class RuleSourceImporter implements IRuleSourceImporter { private validateRuleInput(rule: RuleToImport) { if ( - !this.knownRules.some( + !this.rulesToImport.some( (knownRule) => knownRule.rule_id === rule.rule_id && (knownRule.version === rule.version || knownRule.version == null) @@ -200,17 +218,20 @@ export const createRuleSourceImporter = ({ config, context, prebuiltRuleAssetsClient, + prebuiltRuleObjectsClient, ruleCustomizationStatus, }: { config: ConfigType; context: SecuritySolutionApiRequestHandlerContext; prebuiltRuleAssetsClient: IPrebuiltRuleAssetsClient; + prebuiltRuleObjectsClient: IPrebuiltRuleObjectsClient; ruleCustomizationStatus: PrebuiltRulesCustomizationStatus; }): RuleSourceImporter => { return new RuleSourceImporter({ config, context, prebuiltRuleAssetsClient, + prebuiltRuleObjectsClient, ruleCustomizationStatus, }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/import_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/import_rules.ts index 009a88239df31..38d3a40c7ffe2 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/import_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/import_rules.ts @@ -271,7 +271,7 @@ export default ({ getService }: FtrProviderContext): void => { expect(importedRule).toMatchObject({ rule_id: rule.rule_id, version: 9999, - rule_source: { type: 'external', is_customized: true }, + rule_source: { type: 'external', is_customized: false }, immutable: true, }); });