diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/data_source_diff_algorithm.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/data_source_diff_algorithm.test.ts new file mode 100644 index 0000000000000..f249a037790c7 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/data_source_diff_algorithm.test.ts @@ -0,0 +1,370 @@ +/* + * 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 { + RuleDataSource, + ThreeVersionsOf, +} from '../../../../../../../../common/api/detection_engine'; +import { + ThreeWayDiffOutcome, + ThreeWayMergeOutcome, + MissingVersion, + DataSourceType, +} from '../../../../../../../../common/api/detection_engine'; +import { dataSourceDiffAlgorithm } from './data_source_diff_algorithm'; + +describe('dataSourceDiffAlgorithm', () => { + describe('returns current_version as merged output if there is no update - scenario AAA', () => { + it('if all versions are index patterns', () => { + const mockVersions: ThreeVersionsOf = { + base_version: { + type: DataSourceType.index_patterns, + index_patterns: ['one', 'two', 'three'], + }, + current_version: { + type: DataSourceType.index_patterns, + index_patterns: ['one', 'two', 'three'], + }, + target_version: { + type: DataSourceType.index_patterns, + index_patterns: ['one', 'two', 'three'], + }, + }; + + const result = dataSourceDiffAlgorithm(mockVersions); + + expect(result).toEqual( + expect.objectContaining({ + merged_version: mockVersions.current_version, + diff_outcome: ThreeWayDiffOutcome.StockValueNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Current, + has_conflict: false, + }) + ); + }); + + it('if all versions are data views', () => { + const mockVersions: ThreeVersionsOf = { + base_version: { type: DataSourceType.data_view, data_view_id: '123' }, + current_version: { type: DataSourceType.data_view, data_view_id: '123' }, + target_version: { type: DataSourceType.data_view, data_view_id: '123' }, + }; + + const result = dataSourceDiffAlgorithm(mockVersions); + + expect(result).toEqual( + expect.objectContaining({ + merged_version: mockVersions.current_version, + diff_outcome: ThreeWayDiffOutcome.StockValueNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Current, + has_conflict: false, + }) + ); + }); + }); + + describe('returns current_version as merged output if current_version is different and there is no update - scenario ABA', () => { + it('if current version is different data type than base and target', () => { + const mockVersions: ThreeVersionsOf = { + base_version: { + type: DataSourceType.index_patterns, + index_patterns: ['one', 'two', 'three'], + }, + current_version: { type: DataSourceType.data_view, data_view_id: '123' }, + target_version: { + type: DataSourceType.index_patterns, + index_patterns: ['one', 'two', 'three'], + }, + }; + + const result = dataSourceDiffAlgorithm(mockVersions); + + expect(result).toEqual( + expect.objectContaining({ + merged_version: mockVersions.current_version, + diff_outcome: ThreeWayDiffOutcome.CustomizedValueNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Current, + has_conflict: false, + }) + ); + }); + + it('if all versions are same data type', () => { + const mockVersions: ThreeVersionsOf = { + base_version: { + type: DataSourceType.index_patterns, + index_patterns: ['one', 'two', 'three'], + }, + current_version: { + type: DataSourceType.index_patterns, + index_patterns: ['one', 'three', 'four'], + }, + target_version: { + type: DataSourceType.index_patterns, + index_patterns: ['one', 'two', 'three'], + }, + }; + + const result = dataSourceDiffAlgorithm(mockVersions); + + expect(result).toEqual( + expect.objectContaining({ + merged_version: mockVersions.current_version, + diff_outcome: ThreeWayDiffOutcome.CustomizedValueNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Current, + has_conflict: false, + }) + ); + }); + }); + + describe('returns target_version as merged output if current_version is the same and there is an update - scenario AAB', () => { + it('if target version is different data type than base and current', () => { + const mockVersions: ThreeVersionsOf = { + base_version: { type: DataSourceType.data_view, data_view_id: '123' }, + current_version: { type: DataSourceType.data_view, data_view_id: '123' }, + target_version: { + type: DataSourceType.index_patterns, + index_patterns: ['one', 'two', 'three'], + }, + }; + + const result = dataSourceDiffAlgorithm(mockVersions); + + expect(result).toEqual( + expect.objectContaining({ + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + has_conflict: false, + }) + ); + }); + + it('if all versions are same data type', () => { + const mockVersions: ThreeVersionsOf = { + base_version: { type: DataSourceType.data_view, data_view_id: '123' }, + current_version: { type: DataSourceType.data_view, data_view_id: '123' }, + target_version: { type: DataSourceType.data_view, data_view_id: '456' }, + }; + + const result = dataSourceDiffAlgorithm(mockVersions); + + expect(result).toEqual( + expect.objectContaining({ + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + has_conflict: false, + }) + ); + }); + }); + + it('returns current_version as merged output if current version is different but it matches the update - scenario ABB', () => { + const mockVersions: ThreeVersionsOf = { + base_version: { + type: DataSourceType.index_patterns, + index_patterns: ['one', 'two', 'three'], + }, + current_version: { + type: DataSourceType.index_patterns, + index_patterns: ['one', 'three', 'four'], + }, + target_version: { + type: DataSourceType.index_patterns, + index_patterns: ['one', 'three', 'four'], + }, + }; + + const result = dataSourceDiffAlgorithm(mockVersions); + + expect(result).toEqual( + expect.objectContaining({ + merged_version: mockVersions.current_version, + diff_outcome: ThreeWayDiffOutcome.CustomizedValueSameUpdate, + merge_outcome: ThreeWayMergeOutcome.Current, + has_conflict: false, + }) + ); + }); + + describe('returns current_version as merged output if all three versions are different - scenario ABC', () => { + it('if all versions are index patterns', () => { + const mockVersions: ThreeVersionsOf = { + base_version: { + type: DataSourceType.index_patterns, + index_patterns: ['one', 'two', 'three'], + }, + current_version: { + type: DataSourceType.index_patterns, + index_patterns: ['one', 'three', 'four'], + }, + target_version: { + type: DataSourceType.index_patterns, + index_patterns: ['one', 'two', 'five'], + }, + }; + + const expectedMergedVersion: RuleDataSource = { + type: DataSourceType.index_patterns, + index_patterns: ['one', 'four', 'five'], + }; + + const result = dataSourceDiffAlgorithm(mockVersions); + + expect(result).toEqual( + expect.objectContaining({ + merged_version: expectedMergedVersion, + diff_outcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Merged, + has_conflict: false, + }) + ); + }); + + it('if all versions are data views', () => { + const mockVersions: ThreeVersionsOf = { + base_version: { type: DataSourceType.data_view, data_view_id: '123' }, + current_version: { type: DataSourceType.data_view, data_view_id: '456' }, + target_version: { type: DataSourceType.data_view, data_view_id: '789' }, + }; + + const result = dataSourceDiffAlgorithm(mockVersions); + + expect(result).toEqual( + expect.objectContaining({ + merged_version: mockVersions.current_version, + diff_outcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Conflict, + has_conflict: true, + }) + ); + }); + + it('if base version is a different data type', () => { + const mockVersions: ThreeVersionsOf = { + base_version: { type: DataSourceType.data_view, data_view_id: '123' }, + current_version: { + type: DataSourceType.index_patterns, + index_patterns: ['one', 'three', 'four'], + }, + target_version: { + type: DataSourceType.index_patterns, + index_patterns: ['one', 'two', 'five'], + }, + }; + + const result = dataSourceDiffAlgorithm(mockVersions); + + expect(result).toEqual( + expect.objectContaining({ + merged_version: mockVersions.current_version, + diff_outcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Conflict, + has_conflict: true, + }) + ); + }); + + it('if currrent version is a different data type', () => { + const mockVersions: ThreeVersionsOf = { + base_version: { type: DataSourceType.data_view, data_view_id: '123' }, + current_version: { + type: DataSourceType.index_patterns, + index_patterns: ['one', 'three', 'four'], + }, + target_version: { type: DataSourceType.data_view, data_view_id: '789' }, + }; + + const result = dataSourceDiffAlgorithm(mockVersions); + + expect(result).toEqual( + expect.objectContaining({ + merged_version: mockVersions.current_version, + diff_outcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Conflict, + has_conflict: true, + }) + ); + }); + + it('if target version is a different data type', () => { + const mockVersions: ThreeVersionsOf = { + base_version: { + type: DataSourceType.index_patterns, + index_patterns: ['one', 'two', 'three'], + }, + current_version: { + type: DataSourceType.index_patterns, + index_patterns: ['one', 'three', 'four'], + }, + target_version: { type: DataSourceType.data_view, data_view_id: '789' }, + }; + + const result = dataSourceDiffAlgorithm(mockVersions); + + expect(result).toEqual( + expect.objectContaining({ + merged_version: mockVersions.current_version, + diff_outcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Conflict, + has_conflict: true, + }) + ); + }); + }); + + 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: { + type: DataSourceType.index_patterns, + index_patterns: ['one', 'three', 'four'], + }, + target_version: { + type: DataSourceType.index_patterns, + index_patterns: ['one', 'three', 'four'], + }, + }; + + const result = dataSourceDiffAlgorithm(mockVersions); + + expect(result).toEqual( + expect.objectContaining({ + merged_version: mockVersions.current_version, + diff_outcome: ThreeWayDiffOutcome.StockValueNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Current, + has_conflict: false, + }) + ); + }); + + 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: { type: DataSourceType.data_view, data_view_id: '456' }, + target_version: { + type: DataSourceType.index_patterns, + index_patterns: ['one', 'three', 'four'], + }, + }; + + const result = dataSourceDiffAlgorithm(mockVersions); + + expect(result).toEqual( + expect.objectContaining({ + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + has_conflict: false, + }) + ); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/data_source_diff_algorithm.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/data_source_diff_algorithm.ts new file mode 100644 index 0000000000000..93496c51fd57a --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/data_source_diff_algorithm.ts @@ -0,0 +1,184 @@ +/* + * 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 { union, uniq } from 'lodash'; +import { assertUnreachable } from '../../../../../../../../common/utility_types'; +import type { + RuleDataSource, + ThreeVersionsOf, + ThreeWayDiff, +} from '../../../../../../../../common/api/detection_engine/prebuilt_rules'; +import { + determineDiffOutcome, + determineIfValueCanUpdate, + ThreeWayDiffOutcome, + ThreeWayMergeOutcome, + MissingVersion, + DataSourceType, + determineOrderAgnosticDiffOutcome, +} from '../../../../../../../../common/api/detection_engine/prebuilt_rules'; +import { mergeDedupedArrays } from './helpers'; + +export const dataSourceDiffAlgorithm = ( + versions: ThreeVersionsOf +): ThreeWayDiff => { + const { + base_version: baseVersion, + current_version: currentVersion, + target_version: targetVersion, + } = versions; + + let diffOutcome: ThreeWayDiffOutcome; + + if (baseVersion === MissingVersion) { + if ( + currentVersion.type === DataSourceType.index_patterns && + targetVersion.type === DataSourceType.index_patterns + ) { + diffOutcome = determineOrderAgnosticDiffOutcome( + MissingVersion, + currentVersion.index_patterns, + targetVersion.index_patterns + ); + } else { + diffOutcome = determineDiffOutcome(baseVersion, currentVersion, targetVersion); + } + } else { + if ( + baseVersion.type === DataSourceType.index_patterns && + currentVersion.type === DataSourceType.index_patterns && + targetVersion.type === DataSourceType.index_patterns + ) { + diffOutcome = determineOrderAgnosticDiffOutcome( + baseVersion.index_patterns, + currentVersion.index_patterns, + targetVersion.index_patterns + ); + } else { + diffOutcome = determineDiffOutcome(baseVersion, currentVersion, targetVersion); + } + } + + const valueCanUpdate = determineIfValueCanUpdate(diffOutcome); + + const { mergeOutcome, mergedVersion } = mergeVersions({ + baseVersion, + currentVersion, + targetVersion, + diffOutcome, + }); + + return { + base_version: baseVersion, + current_version: currentVersion, + target_version: targetVersion, + merged_version: mergedVersion, + + diff_outcome: diffOutcome, + merge_outcome: mergeOutcome, + has_update: valueCanUpdate, + has_conflict: mergeOutcome === ThreeWayMergeOutcome.Conflict, + }; +}; + +interface MergeResult { + mergeOutcome: ThreeWayMergeOutcome; + mergedVersion: RuleDataSource; +} + +interface MergeArgs { + baseVersion: RuleDataSource | MissingVersion; + currentVersion: RuleDataSource; + targetVersion: RuleDataSource; + diffOutcome: ThreeWayDiffOutcome; +} + +const mergeVersions = ({ + baseVersion, + currentVersion, + targetVersion, + diffOutcome, +}: MergeArgs): MergeResult => { + const dedupedBaseVersion = + baseVersion !== MissingVersion ? getDedupedDataSourceVersion(baseVersion) : MissingVersion; + const dedupedCurrentVersion = getDedupedDataSourceVersion(currentVersion); + const dedupedTargetVersion = getDedupedDataSourceVersion(targetVersion); + + switch (diffOutcome) { + case ThreeWayDiffOutcome.StockValueNoUpdate: + case ThreeWayDiffOutcome.CustomizedValueNoUpdate: + case ThreeWayDiffOutcome.CustomizedValueSameUpdate: { + return { + mergeOutcome: ThreeWayMergeOutcome.Current, + mergedVersion: dedupedCurrentVersion, + }; + } + case ThreeWayDiffOutcome.StockValueCanUpdate: { + return { + mergeOutcome: ThreeWayMergeOutcome.Target, + mergedVersion: dedupedTargetVersion, + }; + } + case ThreeWayDiffOutcome.CustomizedValueCanUpdate: { + if (dedupedBaseVersion === MissingVersion) { + if ( + dedupedCurrentVersion.type === DataSourceType.index_patterns && + dedupedTargetVersion.type === DataSourceType.index_patterns + ) { + return { + mergeOutcome: ThreeWayMergeOutcome.Merged, + mergedVersion: { + type: DataSourceType.index_patterns, + index_patterns: union( + dedupedCurrentVersion.index_patterns, + dedupedTargetVersion.index_patterns + ), + }, + }; + } + return { + mergeOutcome: ThreeWayMergeOutcome.Conflict, + mergedVersion: dedupedCurrentVersion, + }; + } + + if ( + dedupedBaseVersion.type === DataSourceType.index_patterns && + dedupedCurrentVersion.type === DataSourceType.index_patterns && + dedupedTargetVersion.type === DataSourceType.index_patterns + ) { + return { + mergeOutcome: ThreeWayMergeOutcome.Merged, + mergedVersion: { + type: DataSourceType.index_patterns, + index_patterns: mergeDedupedArrays( + dedupedBaseVersion.index_patterns, + dedupedCurrentVersion.index_patterns, + dedupedTargetVersion.index_patterns + ), + }, + }; + } + return { + mergeOutcome: ThreeWayMergeOutcome.Conflict, + mergedVersion: dedupedCurrentVersion, + }; + } + default: + return assertUnreachable(diffOutcome); + } +}; + +const getDedupedDataSourceVersion = (version: RuleDataSource): RuleDataSource => { + if (version.type === DataSourceType.index_patterns) { + return { + ...version, + index_patterns: uniq(version.index_patterns), + }; + } + return version; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/helpers.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/helpers.ts new file mode 100644 index 0000000000000..9db1d0040085e --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/helpers.ts @@ -0,0 +1,25 @@ +/* + * 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 { difference, union } from 'lodash'; + +export const mergeDedupedArrays = ( + dedupedBaseVersion: T[], + dedupedCurrentVersion: T[], + dedupedTargetVersion: T[] +) => { + const addedCurrent = difference(dedupedCurrentVersion, dedupedBaseVersion); + const removedCurrent = difference(dedupedBaseVersion, dedupedCurrentVersion); + + const addedTarget = difference(dedupedTargetVersion, dedupedBaseVersion); + const removedTarget = difference(dedupedBaseVersion, dedupedTargetVersion); + + const bothAdded = union(addedCurrent, addedTarget); + const bothRemoved = union(removedCurrent, removedTarget); + + return difference(union(dedupedBaseVersion, bothAdded), bothRemoved); +}; 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 b7c0a1143f1a7..fc895543e66b2 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 @@ -10,3 +10,4 @@ export { singleLineStringDiffAlgorithm } from './single_line_string_diff_algorit export { scalarArrayDiffAlgorithm } from './scalar_array_diff_algorithm'; export { simpleDiffAlgorithm } from './simple_diff_algorithm'; export { multiLineStringDiffAlgorithm } from './multi_line_string_diff_algorithm'; +export { dataSourceDiffAlgorithm } from './data_source_diff_algorithm'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/scalar_array_diff_algorithm.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/scalar_array_diff_algorithm.ts index 18cf7f4f8b2cd..f398b6bf4793e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/scalar_array_diff_algorithm.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/scalar_array_diff_algorithm.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { difference, union, uniq } from 'lodash'; +import { union, uniq } from 'lodash'; import { assertUnreachable } from '../../../../../../../../common/utility_types'; import type { ThreeVersionsOf, @@ -18,6 +18,7 @@ import { ThreeWayMergeOutcome, MissingVersion, } from '../../../../../../../../common/api/detection_engine/prebuilt_rules'; +import { mergeDedupedArrays } from './helpers'; /** * Diff algorithm used for arrays of scalar values (eg. numbers, strings, booleans, etc.) @@ -101,16 +102,11 @@ const mergeVersions = ({ }; } - const addedCurrent = difference(dedupedCurrentVersion, dedupedBaseVersion); - const removedCurrent = difference(dedupedBaseVersion, dedupedCurrentVersion); - - const addedTarget = difference(dedupedTargetVersion, dedupedBaseVersion); - const removedTarget = difference(dedupedBaseVersion, dedupedTargetVersion); - - const bothAdded = union(addedCurrent, addedTarget); - const bothRemoved = union(removedCurrent, removedTarget); - - const merged = difference(union(dedupedBaseVersion, bothAdded), bothRemoved); + const merged = mergeDedupedArrays( + dedupedBaseVersion, + dedupedCurrentVersion, + dedupedTargetVersion + ); return { mergeOutcome: ThreeWayMergeOutcome.Merged,