From 14cec3a31a954510bac12852f66433564de92912 Mon Sep 17 00:00:00 2001 From: Davis Plumlee <56367316+dplumlee@users.noreply.github.com> Date: Mon, 30 Sep 2024 13:27:29 -0400 Subject: [PATCH] [Security Solution] Rule `type` field diff algorithm (#193369) ## Summary Addresses https://github.com/elastic/kibana/issues/190482 Adds the diff algorithm implementation for the prebuilt rule `type` field. Returns `target_version` and a `NON_SOLVABLE` conflict for every outcome that changes the field. ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) (cherry picked from commit 18465e7f7e5d9912e61da68873045f0db984fa2b) --- .../diff/calculation/algorithms/index.ts | 1 + .../rule_type_diff_algorithm.test.ts | 165 ++++++++++++++++++ .../algorithms/rule_type_diff_algorithm.ts | 98 +++++++++++ 3 files changed, 264 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..accf133ac71b3 --- /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,165 @@ +/* + * 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.target_version, + diff_outcome: ThreeWayDiffOutcome.StockValueNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); + + 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: 'saved_query', + 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', () => { + // 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: 'saved_query', + }; + + 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', () => { + // 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: 'saved_query', + target_version: 'saved_query', + }; + + 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', () => { + // 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', + 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.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); + + 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: 'saved_query', + }; + + 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..0701d1e46d251 --- /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,98 @@ +/* + * 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: + // 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 + case ThreeWayDiffOutcome.MissingBaseCanUpdate: { + return { + mergedVersion: targetVersion, + mergeOutcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NON_SOLVABLE, + }; + } + default: + return assertUnreachable(diffOutcome); + } +};