[9.0] [Security Solution] Disallow merging critical rule field values upon rule upgrade when base version is missing (#213757) (#213999)

# Backport

This will backport the following commits from `main` to `9.0`:
- [[Security Solution] Disallow merging critical rule field values upon
rule upgrade when base version is missing
#213757](https://github.com/elastic/kibana/pull/213757)
This commit is contained in:
Maxim Palenov 2025-03-11 22:24:21 +01:00 committed by GitHub
parent 4885f7d354
commit e9fb83e565
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 504 additions and 354 deletions

View file

@ -7,7 +7,7 @@
export { numberDiffAlgorithm } from './number_diff_algorithm';
export { singleLineStringDiffAlgorithm } from './single_line_string_diff_algorithm';
export { scalarArrayDiffAlgorithm } from './scalar_array_diff_algorithm';
export { createScalarArrayDiffAlgorithm } 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';

View file

@ -12,324 +12,45 @@ import {
MissingVersion,
ThreeWayDiffConflict,
} from '../../../../../../../../common/api/detection_engine';
import { scalarArrayDiffAlgorithm } from './scalar_array_diff_algorithm';
import {
ScalarArrayDiffMissingBaseVersionStrategy,
createScalarArrayDiffAlgorithm,
} from './scalar_array_diff_algorithm';
describe('scalarArrayDiffAlgorithm', () => {
it('returns current_version as merged output if there is no update - scenario AAA', () => {
const mockVersions: ThreeVersionsOf<string[]> = {
base_version: ['one', 'two', 'three'],
current_version: ['one', 'two', 'three'],
target_version: ['one', 'two', 'three'],
};
describe.each([
[ScalarArrayDiffMissingBaseVersionStrategy.Merge],
[ScalarArrayDiffMissingBaseVersionStrategy.UseTarget],
])('with missingBaseCanUpdateMergeStrategy = %s', (mergeStrategy) => {
const scalarArrayDiffAlgorithm = createScalarArrayDiffAlgorithm({
missingBaseVersionStrategy: mergeStrategy,
});
const result = scalarArrayDiffAlgorithm(mockVersions, false);
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<string[]> = {
base_version: ['one', 'two', 'three'],
current_version: ['one', 'three', 'four'],
target_version: ['one', 'two', 'three'],
};
const result = scalarArrayDiffAlgorithm(mockVersions, false);
expect(result).toEqual(
expect.objectContaining({
merged_version: mockVersions.current_version,
diff_outcome: ThreeWayDiffOutcome.CustomizedValueNoUpdate,
merge_outcome: ThreeWayMergeOutcome.Current,
conflict: ThreeWayDiffConflict.NONE,
})
);
});
it('returns target_version as merged output if current_version is the same and there is an update - scenario AAB', () => {
const mockVersions: ThreeVersionsOf<string[]> = {
base_version: ['one', 'two', 'three'],
current_version: ['one', 'two', 'three'],
target_version: ['one', 'four', 'three'],
};
const result = scalarArrayDiffAlgorithm(mockVersions, false);
expect(result).toEqual(
expect.objectContaining({
merged_version: mockVersions.target_version,
diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate,
merge_outcome: ThreeWayMergeOutcome.Target,
conflict: ThreeWayDiffConflict.NONE,
})
);
});
it('returns current_version as merged output if current version is different but it matches the update - scenario ABB', () => {
const mockVersions: ThreeVersionsOf<string[]> = {
base_version: ['one', 'two', 'three'],
current_version: ['one', 'three', 'four'],
target_version: ['one', 'four', 'three'],
};
const result = scalarArrayDiffAlgorithm(mockVersions, false);
expect(result).toEqual(
expect.objectContaining({
merged_version: mockVersions.current_version,
diff_outcome: ThreeWayDiffOutcome.CustomizedValueSameUpdate,
merge_outcome: ThreeWayMergeOutcome.Current,
conflict: ThreeWayDiffConflict.NONE,
})
);
});
it('returns custom merged version as merged output if all three versions are different - scenario ABC', () => {
const mockVersions: ThreeVersionsOf<string[]> = {
base_version: ['one', 'two', 'three'],
current_version: ['two', 'three', 'four', 'five'],
target_version: ['one', 'three', 'four', 'six'],
};
const expectedMergedVersion = ['three', 'four', 'five', 'six'];
const result = scalarArrayDiffAlgorithm(mockVersions, false);
expect(result).toEqual(
expect.objectContaining({
merged_version: expectedMergedVersion,
diff_outcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate,
merge_outcome: ThreeWayMergeOutcome.Merged,
conflict: ThreeWayDiffConflict.SOLVABLE,
})
);
});
describe('if base_version is missing', () => {
describe('returns target_version as merged output if current_version and target_version are the same - scenario -AA', () => {
it('returns NONE conflict if rule is not customized', () => {
describe('base cases', () => {
it('returns current_version as merged output if there is no update - scenario AAA', () => {
const mockVersions: ThreeVersionsOf<string[]> = {
base_version: MissingVersion,
base_version: ['one', 'two', 'three'],
current_version: ['one', 'two', 'three'],
target_version: ['one', 'two', 'three'],
};
const result = scalarArrayDiffAlgorithm(mockVersions, false);
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 NONE conflict if rule is customized', () => {
const mockVersions: ThreeVersionsOf<string[]> = {
base_version: MissingVersion,
current_version: ['one', 'two', 'three'],
target_version: ['one', 'two', 'three'],
};
const result = scalarArrayDiffAlgorithm(mockVersions, true);
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,
})
);
});
});
describe('if current_version and target_version are different - scenario -AB', () => {
it('returns target_version as merged output and NONE conflict if rule is not customized', () => {
const mockVersions: ThreeVersionsOf<string[]> = {
base_version: MissingVersion,
current_version: ['one', 'two', 'three'],
target_version: ['one', 'four', 'three'],
};
const result = scalarArrayDiffAlgorithm(mockVersions, false);
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.NONE,
})
);
});
it('returns merged version of current and target as merged output if rule is customized', () => {
const mockVersions: ThreeVersionsOf<string[]> = {
base_version: MissingVersion,
current_version: ['one', 'two', 'three'],
target_version: ['one', 'four', 'three'],
};
const expectedMergedVersion = ['one', 'two', 'three', 'four'];
const result = scalarArrayDiffAlgorithm(mockVersions, true);
expect(result).toEqual(
expect.objectContaining({
has_base_version: false,
base_version: undefined,
merged_version: expectedMergedVersion,
diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
merge_outcome: ThreeWayMergeOutcome.Merged,
conflict: ThreeWayDiffConflict.SOLVABLE,
})
);
});
});
});
describe('edge cases', () => {
it('compares arrays agnostic of order', () => {
const mockVersions: ThreeVersionsOf<string[]> = {
base_version: ['one', 'two', 'three'],
current_version: ['one', 'three', 'two'],
target_version: ['three', 'one', 'two'],
};
const result = scalarArrayDiffAlgorithm(mockVersions, false);
expect(result).toEqual(
expect.objectContaining({
merged_version: mockVersions.current_version,
diff_outcome: ThreeWayDiffOutcome.StockValueNoUpdate,
merge_outcome: ThreeWayMergeOutcome.Current,
conflict: ThreeWayDiffConflict.NONE,
})
);
});
describe('compares arrays deduplicated', () => {
it('when values duplicated in base version', () => {
const mockVersions: ThreeVersionsOf<string[]> = {
base_version: ['one', 'two', 'two'],
current_version: ['one', 'two'],
target_version: ['one', 'two'],
};
const expectedMergedVersion = ['one', 'two'];
const result = scalarArrayDiffAlgorithm(mockVersions, false);
expect(result).toEqual(
expect.objectContaining({
merged_version: expectedMergedVersion,
diff_outcome: ThreeWayDiffOutcome.StockValueNoUpdate,
merge_outcome: ThreeWayMergeOutcome.Current,
conflict: ThreeWayDiffConflict.NONE,
})
);
});
it('when values are duplicated in current version', () => {
const mockVersions: ThreeVersionsOf<string[]> = {
base_version: ['one', 'two'],
current_version: ['one', 'two', 'two'],
target_version: ['one', 'two'],
};
const expectedMergedVersion = ['one', 'two'];
const result = scalarArrayDiffAlgorithm(mockVersions, false);
expect(result).toEqual(
expect.objectContaining({
merged_version: expectedMergedVersion,
diff_outcome: ThreeWayDiffOutcome.StockValueNoUpdate,
merge_outcome: ThreeWayMergeOutcome.Current,
conflict: ThreeWayDiffConflict.NONE,
})
);
});
it('when values are duplicated in target version', () => {
const mockVersions: ThreeVersionsOf<string[]> = {
base_version: ['one', 'two'],
current_version: ['one', 'two'],
target_version: ['one', 'two', 'two'],
};
const expectedMergedVersion = ['one', 'two'];
const result = scalarArrayDiffAlgorithm(mockVersions, false);
expect(result).toEqual(
expect.objectContaining({
merged_version: expectedMergedVersion,
diff_outcome: ThreeWayDiffOutcome.StockValueNoUpdate,
merge_outcome: ThreeWayMergeOutcome.Current,
conflict: ThreeWayDiffConflict.NONE,
})
);
});
it('when values are duplicated in all versions', () => {
const mockVersions: ThreeVersionsOf<string[]> = {
base_version: ['one', 'two', 'two'],
current_version: ['two', 'two', 'three'],
target_version: ['one', 'one', 'three', 'three'],
};
const expectedMergedVersion = ['three'];
const result = scalarArrayDiffAlgorithm(mockVersions, false);
expect(result).toEqual(
expect.objectContaining({
merged_version: expectedMergedVersion,
diff_outcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate,
merge_outcome: ThreeWayMergeOutcome.Merged,
conflict: ThreeWayDiffConflict.SOLVABLE,
})
);
});
});
describe('compares empty arrays', () => {
it('when base version is empty', () => {
const mockVersions: ThreeVersionsOf<string[]> = {
base_version: [],
current_version: ['one', 'two'],
target_version: ['one', 'two'],
};
const result = scalarArrayDiffAlgorithm(mockVersions, false);
expect(result).toEqual(
expect.objectContaining({
merged_version: mockVersions.current_version,
diff_outcome: ThreeWayDiffOutcome.CustomizedValueSameUpdate,
diff_outcome: ThreeWayDiffOutcome.StockValueNoUpdate,
merge_outcome: ThreeWayMergeOutcome.Current,
conflict: ThreeWayDiffConflict.NONE,
})
);
});
it('when current version is empty', () => {
it('returns current_version as merged output if current_version is different and there is no update - scenario ABA', () => {
const mockVersions: ThreeVersionsOf<string[]> = {
base_version: ['one', 'two'],
current_version: [],
target_version: ['one', 'two'],
base_version: ['one', 'two', 'three'],
current_version: ['one', 'three', 'four'],
target_version: ['one', 'two', 'three'],
};
const result = scalarArrayDiffAlgorithm(mockVersions, false);
@ -344,11 +65,11 @@ describe('scalarArrayDiffAlgorithm', () => {
);
});
it('when target version is empty', () => {
it('returns target_version as merged output if current_version is the same and there is an update - scenario AAB', () => {
const mockVersions: ThreeVersionsOf<string[]> = {
base_version: ['one', 'two'],
current_version: ['one', 'two'],
target_version: [],
base_version: ['one', 'two', 'three'],
current_version: ['one', 'two', 'three'],
target_version: ['one', 'four', 'three'],
};
const result = scalarArrayDiffAlgorithm(mockVersions, false);
@ -363,24 +84,348 @@ describe('scalarArrayDiffAlgorithm', () => {
);
});
it('when all versions are empty', () => {
it('returns current_version as merged output if current version is different but it matches the update - scenario ABB', () => {
const mockVersions: ThreeVersionsOf<string[]> = {
base_version: [],
current_version: [],
target_version: [],
base_version: ['one', 'two', 'three'],
current_version: ['one', 'three', 'four'],
target_version: ['one', 'four', 'three'],
};
const result = scalarArrayDiffAlgorithm(mockVersions, false);
expect(result).toEqual(
expect.objectContaining({
merged_version: [],
merged_version: mockVersions.current_version,
diff_outcome: ThreeWayDiffOutcome.CustomizedValueSameUpdate,
merge_outcome: ThreeWayMergeOutcome.Current,
conflict: ThreeWayDiffConflict.NONE,
})
);
});
it('returns custom merged version as merged output if all three versions are different - scenario ABC', () => {
const mockVersions: ThreeVersionsOf<string[]> = {
base_version: ['one', 'two', 'three'],
current_version: ['two', 'three', 'four', 'five'],
target_version: ['one', 'three', 'four', 'six'],
};
const expectedMergedVersion = ['three', 'four', 'five', 'six'];
const result = scalarArrayDiffAlgorithm(mockVersions, false);
expect(result).toEqual(
expect.objectContaining({
merged_version: expectedMergedVersion,
diff_outcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate,
merge_outcome: ThreeWayMergeOutcome.Merged,
conflict: ThreeWayDiffConflict.SOLVABLE,
})
);
});
});
describe('edge cases', () => {
it('compares arrays agnostic of order', () => {
const mockVersions: ThreeVersionsOf<string[]> = {
base_version: ['one', 'two', 'three'],
current_version: ['one', 'three', 'two'],
target_version: ['three', 'one', 'two'],
};
const result = scalarArrayDiffAlgorithm(mockVersions, false);
expect(result).toEqual(
expect.objectContaining({
merged_version: mockVersions.current_version,
diff_outcome: ThreeWayDiffOutcome.StockValueNoUpdate,
merge_outcome: ThreeWayMergeOutcome.Current,
conflict: ThreeWayDiffConflict.NONE,
})
);
});
describe('compares arrays deduplicated', () => {
it('when values duplicated in base version', () => {
const mockVersions: ThreeVersionsOf<string[]> = {
base_version: ['one', 'two', 'two'],
current_version: ['one', 'two'],
target_version: ['one', 'two'],
};
const expectedMergedVersion = ['one', 'two'];
const result = scalarArrayDiffAlgorithm(mockVersions, false);
expect(result).toEqual(
expect.objectContaining({
merged_version: expectedMergedVersion,
diff_outcome: ThreeWayDiffOutcome.StockValueNoUpdate,
merge_outcome: ThreeWayMergeOutcome.Current,
conflict: ThreeWayDiffConflict.NONE,
})
);
});
it('when values are duplicated in current version', () => {
const mockVersions: ThreeVersionsOf<string[]> = {
base_version: ['one', 'two'],
current_version: ['one', 'two', 'two'],
target_version: ['one', 'two'],
};
const expectedMergedVersion = ['one', 'two'];
const result = scalarArrayDiffAlgorithm(mockVersions, false);
expect(result).toEqual(
expect.objectContaining({
merged_version: expectedMergedVersion,
diff_outcome: ThreeWayDiffOutcome.StockValueNoUpdate,
merge_outcome: ThreeWayMergeOutcome.Current,
conflict: ThreeWayDiffConflict.NONE,
})
);
});
it('when values are duplicated in target version', () => {
const mockVersions: ThreeVersionsOf<string[]> = {
base_version: ['one', 'two'],
current_version: ['one', 'two'],
target_version: ['one', 'two', 'two'],
};
const expectedMergedVersion = ['one', 'two'];
const result = scalarArrayDiffAlgorithm(mockVersions, false);
expect(result).toEqual(
expect.objectContaining({
merged_version: expectedMergedVersion,
diff_outcome: ThreeWayDiffOutcome.StockValueNoUpdate,
merge_outcome: ThreeWayMergeOutcome.Current,
conflict: ThreeWayDiffConflict.NONE,
})
);
});
it('when values are duplicated in all versions', () => {
const mockVersions: ThreeVersionsOf<string[]> = {
base_version: ['one', 'two', 'two'],
current_version: ['two', 'two', 'three'],
target_version: ['one', 'one', 'three', 'three'],
};
const expectedMergedVersion = ['three'];
const result = scalarArrayDiffAlgorithm(mockVersions, false);
expect(result).toEqual(
expect.objectContaining({
merged_version: expectedMergedVersion,
diff_outcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate,
merge_outcome: ThreeWayMergeOutcome.Merged,
conflict: ThreeWayDiffConflict.SOLVABLE,
})
);
});
});
describe('compares empty arrays', () => {
it('when base version is empty', () => {
const mockVersions: ThreeVersionsOf<string[]> = {
base_version: [],
current_version: ['one', 'two'],
target_version: ['one', 'two'],
};
const result = scalarArrayDiffAlgorithm(mockVersions, false);
expect(result).toEqual(
expect.objectContaining({
merged_version: mockVersions.current_version,
diff_outcome: ThreeWayDiffOutcome.CustomizedValueSameUpdate,
merge_outcome: ThreeWayMergeOutcome.Current,
conflict: ThreeWayDiffConflict.NONE,
})
);
});
it('when current version is empty', () => {
const mockVersions: ThreeVersionsOf<string[]> = {
base_version: ['one', 'two'],
current_version: [],
target_version: ['one', 'two'],
};
const result = scalarArrayDiffAlgorithm(mockVersions, false);
expect(result).toEqual(
expect.objectContaining({
merged_version: mockVersions.current_version,
diff_outcome: ThreeWayDiffOutcome.CustomizedValueNoUpdate,
merge_outcome: ThreeWayMergeOutcome.Current,
conflict: ThreeWayDiffConflict.NONE,
})
);
});
it('when target version is empty', () => {
const mockVersions: ThreeVersionsOf<string[]> = {
base_version: ['one', 'two'],
current_version: ['one', 'two'],
target_version: [],
};
const result = scalarArrayDiffAlgorithm(mockVersions, false);
expect(result).toEqual(
expect.objectContaining({
merged_version: mockVersions.target_version,
diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate,
merge_outcome: ThreeWayMergeOutcome.Target,
conflict: ThreeWayDiffConflict.NONE,
})
);
});
it('when all versions are empty', () => {
const mockVersions: ThreeVersionsOf<string[]> = {
base_version: [],
current_version: [],
target_version: [],
};
const result = scalarArrayDiffAlgorithm(mockVersions, false);
expect(result).toEqual(
expect.objectContaining({
merged_version: [],
diff_outcome: ThreeWayDiffOutcome.StockValueNoUpdate,
merge_outcome: ThreeWayMergeOutcome.Current,
conflict: ThreeWayDiffConflict.NONE,
})
);
});
});
});
describe('and base_version is missing', () => {
describe('returns target_version as merged output if current_version and target_version are the same - scenario -AA', () => {
it('returns NONE conflict if rule is not customized', () => {
const mockVersions: ThreeVersionsOf<string[]> = {
base_version: MissingVersion,
current_version: ['one', 'two', 'three'],
target_version: ['one', 'two', 'three'],
};
const result = scalarArrayDiffAlgorithm(mockVersions, false);
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 NONE conflict if rule is customized', () => {
const mockVersions: ThreeVersionsOf<string[]> = {
base_version: MissingVersion,
current_version: ['one', 'two', 'three'],
target_version: ['one', 'two', 'three'],
};
const result = scalarArrayDiffAlgorithm(mockVersions, true);
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,
})
);
});
});
describe('if current_version and target_version are different - scenario -AB', () => {
it('returns target_version as merged output and NONE conflict if rule is not customized', () => {
const mockVersions: ThreeVersionsOf<string[]> = {
base_version: MissingVersion,
current_version: ['one', 'two', 'three'],
target_version: ['one', 'four', 'three'],
};
const result = scalarArrayDiffAlgorithm(mockVersions, false);
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.NONE,
})
);
});
});
});
});
describe('when base_version is missing', () => {
it('returns merged version of current and target as merged output if rule is customized', () => {
const mockVersions: ThreeVersionsOf<string[]> = {
base_version: MissingVersion,
current_version: ['one', 'two', 'three'],
target_version: ['one', 'four', 'three'],
};
const expectedMergedVersion = ['one', 'two', 'three', 'four'];
const scalarArrayDiffAlgorithm = createScalarArrayDiffAlgorithm({
missingBaseVersionStrategy: ScalarArrayDiffMissingBaseVersionStrategy.Merge,
});
const result = scalarArrayDiffAlgorithm(mockVersions, true);
expect(result).toEqual(
expect.objectContaining({
has_base_version: false,
base_version: undefined,
merged_version: expectedMergedVersion,
diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
merge_outcome: ThreeWayMergeOutcome.Merged,
conflict: ThreeWayDiffConflict.SOLVABLE,
})
);
});
it('returns target version of current and target as merged output if rule is customized', () => {
const targetVersion = ['one', 'four', 'three'];
const mockVersions: ThreeVersionsOf<string[]> = {
base_version: MissingVersion,
current_version: ['one', 'two', 'three'],
target_version: targetVersion,
};
const scalarArrayDiffAlgorithm = createScalarArrayDiffAlgorithm({
missingBaseVersionStrategy: ScalarArrayDiffMissingBaseVersionStrategy.UseTarget,
});
const result = scalarArrayDiffAlgorithm(mockVersions, true);
expect(result).toEqual(
expect.objectContaining({
has_base_version: false,
base_version: undefined,
merged_version: targetVersion,
diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
merge_outcome: ThreeWayMergeOutcome.Target,
conflict: ThreeWayDiffConflict.SOLVABLE,
})
);
});
});
});

View file

@ -21,47 +21,90 @@ import {
} from '../../../../../../../../common/api/detection_engine/prebuilt_rules';
import { mergeDedupedArrays } from './helpers';
type ScalarArrayDiffAlgorithm<TValue> = (
versions: ThreeVersionsOf<TValue[]>,
isRuleCustomized: boolean
) => ThreeWayDiff<TValue[]>;
/**
* This strategy applies when all these conditions are met:
* 1) when the base version is missing;
* 2) and there is an update from Elastic (current version != target version);
* 3) and the rule IS marked as customized.
*
* When all that is true, the scalar array diff algorithm uses this strategy
* to determine what to do, exactly.
*/
export enum ScalarArrayDiffMissingBaseVersionStrategy {
/**
* Merge the current and target versions and return the result as the merged version.
*/
Merge = 'Merge',
/**
* Return the target version as the merged version.
*/
UseTarget = 'UseTarget',
}
interface ScalarArrayDiffAlgorithmOptions {
/**
* Algorithm's behavior when the base version is missing and current field's
* value differs from the target value.
*/
missingBaseVersionStrategy: ScalarArrayDiffMissingBaseVersionStrategy;
}
/**
* Diff algorithm used for arrays of scalar values (eg. numbers, strings, booleans, etc.)
*
* NOTE: Diffing logic will be agnostic to array order
*/
export const scalarArrayDiffAlgorithm = <TValue>(
versions: ThreeVersionsOf<TValue[]>,
isRuleCustomized: boolean
): ThreeWayDiff<TValue[]> => {
const {
base_version: baseVersion,
current_version: currentVersion,
target_version: targetVersion,
} = versions;
export function createScalarArrayDiffAlgorithm<TValue>(
options: ScalarArrayDiffAlgorithmOptions
): ScalarArrayDiffAlgorithm<TValue> {
return function scalarArrayDiffAlgorithm(
versions: ThreeVersionsOf<TValue[]>,
isRuleCustomized: boolean
) {
const {
base_version: baseVersion,
current_version: currentVersion,
target_version: targetVersion,
} = versions;
const diffOutcome = determineOrderAgnosticDiffOutcome(baseVersion, currentVersion, targetVersion);
const valueCanUpdate = determineIfValueCanUpdate(diffOutcome);
const diffOutcome = determineOrderAgnosticDiffOutcome(
baseVersion,
currentVersion,
targetVersion
);
const valueCanUpdate = determineIfValueCanUpdate(diffOutcome);
const hasBaseVersion = baseVersion !== MissingVersion;
const hasBaseVersion = baseVersion !== MissingVersion;
const { mergeOutcome, conflict, mergedVersion } = mergeVersions({
baseVersion: hasBaseVersion ? baseVersion : undefined,
currentVersion,
targetVersion,
diffOutcome,
isRuleCustomized,
});
const { mergeOutcome, conflict, mergedVersion } = mergeVersions({
baseVersion: hasBaseVersion ? baseVersion : undefined,
currentVersion,
targetVersion,
diffOutcome,
isRuleCustomized,
options,
});
return {
has_base_version: hasBaseVersion,
base_version: hasBaseVersion ? baseVersion : undefined,
current_version: currentVersion,
target_version: targetVersion,
merged_version: mergedVersion,
merge_outcome: mergeOutcome,
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,
conflict,
has_update: valueCanUpdate,
diff_outcome: diffOutcome,
conflict,
has_update: valueCanUpdate,
};
};
};
}
interface MergeResult<TValue> {
mergeOutcome: ThreeWayMergeOutcome;
@ -75,6 +118,7 @@ interface MergeArgs<TValue> {
targetVersion: TValue[];
diffOutcome: ThreeWayDiffOutcome;
isRuleCustomized: boolean;
options: ScalarArrayDiffAlgorithmOptions;
}
const mergeVersions = <TValue>({
@ -83,6 +127,7 @@ const mergeVersions = <TValue>({
targetVersion,
diffOutcome,
isRuleCustomized,
options,
}: MergeArgs<TValue>): MergeResult<TValue> => {
const dedupedBaseVersion = uniq(baseVersion);
const dedupedCurrentVersion = uniq(currentVersion);
@ -135,17 +180,35 @@ const mergeVersions = <TValue>({
// Otherwise we treat scenario -AB as AAB
// https://github.com/elastic/kibana/issues/210358#issuecomment-2654492854
case ThreeWayDiffOutcome.MissingBaseCanUpdate: {
return isRuleCustomized
? {
if (!isRuleCustomized) {
return {
mergedVersion: targetVersion,
mergeOutcome: ThreeWayMergeOutcome.Target,
conflict: ThreeWayDiffConflict.NONE,
};
}
switch (options.missingBaseVersionStrategy) {
case ScalarArrayDiffMissingBaseVersionStrategy.Merge: {
return {
mergedVersion: union(dedupedCurrentVersion, dedupedTargetVersion),
mergeOutcome: ThreeWayMergeOutcome.Merged,
conflict: ThreeWayDiffConflict.SOLVABLE,
}
: {
};
}
case ScalarArrayDiffMissingBaseVersionStrategy.UseTarget: {
return {
mergedVersion: targetVersion,
mergeOutcome: ThreeWayMergeOutcome.Target,
conflict: ThreeWayDiffConflict.NONE,
conflict: ThreeWayDiffConflict.SOLVABLE,
};
}
default: {
return assertUnreachable(options.missingBaseVersionStrategy);
}
}
}
default:
return assertUnreachable(diffOutcome);

View file

@ -41,7 +41,6 @@ import {
dataSourceDiffAlgorithm,
multiLineStringDiffAlgorithm,
numberDiffAlgorithm,
scalarArrayDiffAlgorithm,
simpleDiffAlgorithm,
singleLineStringDiffAlgorithm,
kqlQueryDiffAlgorithm,
@ -50,6 +49,10 @@ import {
ruleTypeDiffAlgorithm,
forceTargetVersionDiffAlgorithm,
} from './algorithms';
import {
ScalarArrayDiffMissingBaseVersionStrategy,
createScalarArrayDiffAlgorithm,
} from './algorithms/scalar_array_diff_algorithm';
const BASE_TYPE_ERROR = `Base version can't be of different rule type`;
const TARGET_TYPE_ERROR = `Target version can't be of different rule type`;
@ -215,13 +218,17 @@ const commonFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor<DiffableCommonFields>
*/
version: forceTargetVersionDiffAlgorithm,
name: singleLineStringDiffAlgorithm,
tags: scalarArrayDiffAlgorithm,
tags: createScalarArrayDiffAlgorithm({
missingBaseVersionStrategy: ScalarArrayDiffMissingBaseVersionStrategy.Merge,
}),
description: multiLineStringDiffAlgorithm,
severity: singleLineStringDiffAlgorithm,
severity_mapping: simpleDiffAlgorithm,
risk_score: numberDiffAlgorithm,
risk_score_mapping: simpleDiffAlgorithm,
references: scalarArrayDiffAlgorithm,
references: createScalarArrayDiffAlgorithm({
missingBaseVersionStrategy: ScalarArrayDiffMissingBaseVersionStrategy.Merge,
}),
false_positives: simpleDiffAlgorithm,
threat: simpleDiffAlgorithm,
note: multiLineStringDiffAlgorithm,
@ -304,7 +311,9 @@ const threatMatchFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor<DiffableThreatMat
kql_query: kqlQueryDiffAlgorithm,
data_source: dataSourceDiffAlgorithm,
threat_query: kqlQueryDiffAlgorithm,
threat_index: scalarArrayDiffAlgorithm,
threat_index: createScalarArrayDiffAlgorithm({
missingBaseVersionStrategy: ScalarArrayDiffMissingBaseVersionStrategy.UseTarget,
}),
threat_mapping: simpleDiffAlgorithm,
threat_indicator_path: singleLineStringDiffAlgorithm,
alert_suppression: simpleDiffAlgorithm,
@ -355,7 +364,9 @@ const newTermsFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor<DiffableNewTermsFiel
type: ruleTypeDiffAlgorithm,
kql_query: kqlQueryDiffAlgorithm,
data_source: dataSourceDiffAlgorithm,
new_terms_fields: scalarArrayDiffAlgorithm,
new_terms_fields: createScalarArrayDiffAlgorithm({
missingBaseVersionStrategy: ScalarArrayDiffMissingBaseVersionStrategy.UseTarget,
}),
history_window_start: singleLineStringDiffAlgorithm,
alert_suppression: simpleDiffAlgorithm,
};

View file

@ -10,7 +10,9 @@ import { isUndefined, omitBy } from 'lodash';
import type {
PartialRuleDiff,
RuleResponse,
UpgradeConflictResolution,
} from '@kbn/security-solution-plugin/common/api/detection_engine';
import { UpgradeConflictResolutionEnum } from '@kbn/security-solution-plugin/common/api/detection_engine';
import { ModeEnum } from '@kbn/security-solution-plugin/common/api/detection_engine';
import {
ThreeWayDiffConflict,
@ -170,6 +172,7 @@ export function testFieldUpgradeReview(
interface TestFieldUpgradesToMergedValueParams {
ruleUpgradeAssets: TestFieldRuleUpgradeAssets;
onConflict?: UpgradeConflictResolution;
diffableRuleFieldName: string;
expectedFieldsAfterUpgrade: Partial<RuleResponse>;
}
@ -187,6 +190,7 @@ interface TestFieldUpgradesToMergedValueParams {
export function testFieldUpgradesToMergedValue(
{
ruleUpgradeAssets,
onConflict = UpgradeConflictResolutionEnum.SKIP,
diffableRuleFieldName,
expectedFieldsAfterUpgrade,
}: TestFieldUpgradesToMergedValueParams,
@ -215,6 +219,7 @@ export function testFieldUpgradesToMergedValue(
const response = await performUpgradePrebuiltRules(es, supertest, {
mode: ModeEnum.SPECIFIC_RULES,
on_conflict: onConflict,
rules: [
{
rule_id: ruleUpgradeAssets.upgrade.rule_id ?? DEFAULT_TEST_RULE_ID,

View file

@ -5,7 +5,10 @@
* 2.0.
*/
import { ThreeWayDiffOutcome } from '@kbn/security-solution-plugin/common/api/detection_engine';
import {
ThreeWayDiffOutcome,
UpgradeConflictResolutionEnum,
} from '@kbn/security-solution-plugin/common/api/detection_engine';
import { FtrProviderContext } from '../../../../../../../../ftr_provider_context';
import type { TestFieldRuleUpgradeAssets } from '../test_helpers';
import {
@ -302,16 +305,26 @@ export function newTermsFieldsField({ getService }: FtrProviderContext): void {
ruleUpgradeAssets,
diffableRuleFieldName: 'new_terms_fields',
expectedDiffOutcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
isMergableField: true,
isMergableField: false,
expectedFieldDiffValues: {
current: ['fieldB'],
target: ['fieldA', 'fieldC'],
merged: ['fieldB', 'fieldA', 'fieldC'],
merged: ['fieldA', 'fieldC'],
},
},
getService
);
testFieldUpgradesToMergedValue(
{
ruleUpgradeAssets,
onConflict: UpgradeConflictResolutionEnum.UPGRADE_SOLVABLE,
diffableRuleFieldName: 'new_terms_fields',
expectedFieldsAfterUpgrade: { new_terms_fields: ['fieldA', 'fieldC'] },
},
getService
);
testFieldUpgradesToResolvedValue(
{
ruleUpgradeAssets,

View file

@ -5,7 +5,10 @@
* 2.0.
*/
import { ThreeWayDiffOutcome } from '@kbn/security-solution-plugin/common/api/detection_engine';
import {
ThreeWayDiffOutcome,
UpgradeConflictResolutionEnum,
} from '@kbn/security-solution-plugin/common/api/detection_engine';
import { FtrProviderContext } from '../../../../../../../../ftr_provider_context';
import type { TestFieldRuleUpgradeAssets } from '../test_helpers';
import {
@ -302,16 +305,26 @@ export function threatIndexField({ getService }: FtrProviderContext): void {
ruleUpgradeAssets,
diffableRuleFieldName: 'threat_index',
expectedDiffOutcome: ThreeWayDiffOutcome.MissingBaseCanUpdate,
isMergableField: true,
isMergableField: false,
expectedFieldDiffValues: {
current: ['indexD'],
target: ['indexB', 'indexC'],
merged: ['indexD', 'indexB', 'indexC'],
merged: ['indexB', 'indexC'],
},
},
getService
);
testFieldUpgradesToMergedValue(
{
ruleUpgradeAssets,
onConflict: UpgradeConflictResolutionEnum.UPGRADE_SOLVABLE,
diffableRuleFieldName: 'threat_index',
expectedFieldsAfterUpgrade: { threat_index: ['indexB', 'indexC'] },
},
getService
);
testFieldUpgradesToResolvedValue(
{
ruleUpgradeAssets,