From fe40d7a2f46e1bd3fc0ae6e559bf5c68a7b95fd2 Mon Sep 17 00:00:00 2001 From: Davis Plumlee Date: Wed, 18 Sep 2024 18:02:12 -0400 Subject: [PATCH 1/3] adds diff algorithm --- .../diff/calculation/algorithms/index.ts | 1 + .../rule_type_diff_algorithm.test.ts | 159 ++++++++++++++++++ .../algorithms/rule_type_diff_algorithm.ts | 97 +++++++++++ 3 files changed, 257 insertions(+) create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.ts diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/index.ts index 629f329c72b9b..c8b55a49edc00 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/index.ts @@ -14,3 +14,4 @@ export { dataSourceDiffAlgorithm } from './data_source_diff_algorithm'; export { kqlQueryDiffAlgorithm } from './kql_query_diff_algorithm'; export { eqlQueryDiffAlgorithm } from './eql_query_diff_algorithm'; export { esqlQueryDiffAlgorithm } from './esql_query_diff_algorithm'; +export { ruleTypeDiffAlgorithm } from './rule_type_diff_algorithm'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.test.ts new file mode 100644 index 0000000000000..f9cc060210c87 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.test.ts @@ -0,0 +1,159 @@ +/* + * 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 { + DiffableRuleTypes, + ThreeVersionsOf, +} from '../../../../../../../../common/api/detection_engine'; +import { + ThreeWayDiffOutcome, + ThreeWayMergeOutcome, + MissingVersion, + ThreeWayDiffConflict, +} from '../../../../../../../../common/api/detection_engine'; +import { ruleTypeDiffAlgorithm } from './rule_type_diff_algorithm'; + +describe('ruleTypeDiffAlgorithm', () => { + it('returns current_version as merged output if there is no update - scenario AAA', () => { + const mockVersions: ThreeVersionsOf = { + base_version: 'query', + current_version: 'query', + target_version: 'query', + }; + + const result = ruleTypeDiffAlgorithm(mockVersions); + + expect(result).toEqual( + expect.objectContaining({ + merged_version: mockVersions.current_version, + diff_outcome: ThreeWayDiffOutcome.StockValueNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Current, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); + + it('returns current_version as merged output if current_version is different and there is no update - scenario ABA', () => { + const mockVersions: ThreeVersionsOf = { + base_version: 'query', + current_version: 'eql', + target_version: 'query', + }; + + const result = ruleTypeDiffAlgorithm(mockVersions); + + expect(result).toEqual( + expect.objectContaining({ + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.CustomizedValueNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NON_SOLVABLE, + }) + ); + }); + + it('returns target_version as merged output if current_version is the same and there is an update - scenario AAB', () => { + const mockVersions: ThreeVersionsOf = { + base_version: 'query', + current_version: 'query', + target_version: 'eql', + }; + + const result = ruleTypeDiffAlgorithm(mockVersions); + + expect(result).toEqual( + expect.objectContaining({ + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NON_SOLVABLE, + }) + ); + }); + + it('returns current_version as merged output if current version is different but it matches the update - scenario ABB', () => { + const mockVersions: ThreeVersionsOf = { + base_version: 'query', + current_version: 'eql', + target_version: 'eql', + }; + + const result = ruleTypeDiffAlgorithm(mockVersions); + + expect(result).toEqual( + expect.objectContaining({ + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.CustomizedValueSameUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NON_SOLVABLE, + }) + ); + }); + + it('returns current_version as merged output if all three versions are different - scenario ABC', () => { + const mockVersions: ThreeVersionsOf = { + base_version: 'query', + current_version: 'eql', + target_version: 'saved_query', + }; + + const result = ruleTypeDiffAlgorithm(mockVersions); + + expect(result).toEqual( + expect.objectContaining({ + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NON_SOLVABLE, + }) + ); + }); + + describe('if base_version is missing', () => { + it('returns current_version as merged output if current_version and target_version are the same - scenario -AA', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: 'query', + target_version: 'query', + }; + + const result = ruleTypeDiffAlgorithm(mockVersions); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.current_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Current, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); + + it('returns target_version as merged output if current_version and target_version are different - scenario -AB', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: 'query', + target_version: 'eql', + }; + + const result = ruleTypeDiffAlgorithm(mockVersions); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NON_SOLVABLE, + }) + ); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.ts new file mode 100644 index 0000000000000..5a9d5584cd0db --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.ts @@ -0,0 +1,97 @@ +/* + * 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 { assertUnreachable } from '../../../../../../../../common/utility_types'; +import type { + DiffableRuleTypes, + ThreeVersionsOf, + ThreeWayDiff, +} from '../../../../../../../../common/api/detection_engine/prebuilt_rules'; +import { + determineDiffOutcome, + determineIfValueCanUpdate, + MissingVersion, + ThreeWayDiffConflict, + ThreeWayDiffOutcome, + ThreeWayMergeOutcome, +} from '../../../../../../../../common/api/detection_engine/prebuilt_rules'; + +export const ruleTypeDiffAlgorithm = ( + versions: ThreeVersionsOf +): ThreeWayDiff => { + const { + base_version: baseVersion, + current_version: currentVersion, + target_version: targetVersion, + } = versions; + + const diffOutcome = determineDiffOutcome(baseVersion, currentVersion, targetVersion); + const valueCanUpdate = determineIfValueCanUpdate(diffOutcome); + + const hasBaseVersion = baseVersion !== MissingVersion; + + const { mergeOutcome, conflict, mergedVersion } = mergeVersions({ + targetVersion, + diffOutcome, + }); + + return { + has_base_version: hasBaseVersion, + base_version: hasBaseVersion ? baseVersion : undefined, + current_version: currentVersion, + target_version: targetVersion, + merged_version: mergedVersion, + merge_outcome: mergeOutcome, + + diff_outcome: diffOutcome, + has_update: valueCanUpdate, + conflict, + }; +}; + +interface MergeResult { + mergeOutcome: ThreeWayMergeOutcome; + mergedVersion: TValue; + conflict: ThreeWayDiffConflict; +} + +interface MergeArgs { + targetVersion: TValue; + diffOutcome: ThreeWayDiffOutcome; +} + +const mergeVersions = ({ + targetVersion, + diffOutcome, +}: MergeArgs): MergeResult => { + switch (diffOutcome) { + // Scenario -AA is treated as scenario AAA: + // https://github.com/elastic/kibana/pull/184889#discussion_r1636421293 + case ThreeWayDiffOutcome.MissingBaseNoUpdate: + case ThreeWayDiffOutcome.StockValueNoUpdate: + return { + conflict: ThreeWayDiffConflict.NONE, + mergedVersion: targetVersion, + mergeOutcome: ThreeWayMergeOutcome.Target, + }; + case ThreeWayDiffOutcome.CustomizedValueNoUpdate: + case ThreeWayDiffOutcome.CustomizedValueSameUpdate: + case ThreeWayDiffOutcome.StockValueCanUpdate: + case ThreeWayDiffOutcome.CustomizedValueCanUpdate: + // Scenario -AB is treated as scenario ABC: + // https://github.com/elastic/kibana/pull/184889#discussion_r1636421293 + case ThreeWayDiffOutcome.MissingBaseCanUpdate: { + return { + mergedVersion: targetVersion, + mergeOutcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NON_SOLVABLE, + }; + } + default: + return assertUnreachable(diffOutcome); + } +}; From f1d6b91543f7ef7c76238792cc61295699f90d56 Mon Sep 17 00:00:00 2001 From: Davis Plumlee Date: Wed, 18 Sep 2024 20:12:53 -0400 Subject: [PATCH 2/3] fixes tests --- .../algorithms/rule_type_diff_algorithm.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.test.ts index f9cc060210c87..2dc468e7a6608 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.test.ts @@ -29,9 +29,9 @@ describe('ruleTypeDiffAlgorithm', () => { expect(result).toEqual( expect.objectContaining({ - merged_version: mockVersions.current_version, + merged_version: mockVersions.target_version, diff_outcome: ThreeWayDiffOutcome.StockValueNoUpdate, - merge_outcome: ThreeWayMergeOutcome.Current, + merge_outcome: ThreeWayMergeOutcome.Target, conflict: ThreeWayDiffConflict.NONE, }) ); @@ -127,9 +127,9 @@ describe('ruleTypeDiffAlgorithm', () => { expect.objectContaining({ has_base_version: false, base_version: undefined, - merged_version: mockVersions.current_version, + merged_version: mockVersions.target_version, diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, - merge_outcome: ThreeWayMergeOutcome.Current, + merge_outcome: ThreeWayMergeOutcome.Target, conflict: ThreeWayDiffConflict.NONE, }) ); From 448899ba644daa2d11e21732e9262c7b06472815 Mon Sep 17 00:00:00 2001 From: Davis Plumlee Date: Mon, 30 Sep 2024 12:03:32 -0400 Subject: [PATCH 3/3] adds comments --- .../algorithms/rule_type_diff_algorithm.test.ts | 16 +++++++++++----- .../algorithms/rule_type_diff_algorithm.ts | 1 + 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.test.ts index 2dc468e7a6608..accf133ac71b3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.test.ts @@ -38,9 +38,10 @@ describe('ruleTypeDiffAlgorithm', () => { }); it('returns current_version as merged output if current_version is different and there is no update - scenario ABA', () => { + // User can change rule type field between `query` and `saved_query` in the UI, no other rule types const mockVersions: ThreeVersionsOf = { base_version: 'query', - current_version: 'eql', + current_version: 'saved_query', target_version: 'query', }; @@ -57,10 +58,11 @@ describe('ruleTypeDiffAlgorithm', () => { }); it('returns target_version as merged output if current_version is the same and there is an update - scenario AAB', () => { + // User can change rule type field between `query` and `saved_query` in the UI, no other rule types const mockVersions: ThreeVersionsOf = { base_version: 'query', current_version: 'query', - target_version: 'eql', + target_version: 'saved_query', }; const result = ruleTypeDiffAlgorithm(mockVersions); @@ -76,10 +78,11 @@ describe('ruleTypeDiffAlgorithm', () => { }); it('returns current_version as merged output if current version is different but it matches the update - scenario ABB', () => { + // User can change rule type field between `query` and `saved_query` in the UI, no other rule types const mockVersions: ThreeVersionsOf = { base_version: 'query', - current_version: 'eql', - target_version: 'eql', + current_version: 'saved_query', + target_version: 'saved_query', }; const result = ruleTypeDiffAlgorithm(mockVersions); @@ -95,6 +98,8 @@ describe('ruleTypeDiffAlgorithm', () => { }); it('returns current_version as merged output if all three versions are different - scenario ABC', () => { + // User can change rule type field between `query` and `saved_query` in the UI, no other rule types + // NOTE: This test case scenario is currently inaccessible via normal UI or API workflows, but the logic is covered just in case const mockVersions: ThreeVersionsOf = { base_version: 'query', current_version: 'eql', @@ -136,10 +141,11 @@ describe('ruleTypeDiffAlgorithm', () => { }); it('returns target_version as merged output if current_version and target_version are different - scenario -AB', () => { + // User can change rule type field between `query` and `saved_query` in the UI, no other rule types const mockVersions: ThreeVersionsOf = { base_version: MissingVersion, current_version: 'query', - target_version: 'eql', + target_version: 'saved_query', }; const result = ruleTypeDiffAlgorithm(mockVersions); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.ts index 5a9d5584cd0db..0701d1e46d251 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.ts @@ -81,6 +81,7 @@ const mergeVersions = ({ case ThreeWayDiffOutcome.CustomizedValueNoUpdate: case ThreeWayDiffOutcome.CustomizedValueSameUpdate: case ThreeWayDiffOutcome.StockValueCanUpdate: + // NOTE: This scenario is currently inaccessible via normal UI or API workflows, but the logic is covered just in case case ThreeWayDiffOutcome.CustomizedValueCanUpdate: // Scenario -AB is treated as scenario ABC: // https://github.com/elastic/kibana/pull/184889#discussion_r1636421293