Skip to content

Commit

Permalink
[Security Solution] Rule type field diff algorithm (elastic#193369)
Browse files Browse the repository at this point in the history
## Summary

Addresses elastic#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 18465e7)
  • Loading branch information
dplumlee committed Sep 30, 2024
1 parent e6593a0 commit 14cec3a
Show file tree
Hide file tree
Showing 3 changed files with 264 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
@@ -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<DiffableRuleTypes> = {
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<DiffableRuleTypes> = {
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<DiffableRuleTypes> = {
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<DiffableRuleTypes> = {
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<DiffableRuleTypes> = {
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<DiffableRuleTypes> = {
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<DiffableRuleTypes> = {
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,
})
);
});
});
});
Original file line number Diff line number Diff line change
@@ -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 = <TValue extends DiffableRuleTypes>(
versions: ThreeVersionsOf<TValue>
): ThreeWayDiff<TValue> => {
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<TValue> {
mergeOutcome: ThreeWayMergeOutcome;
mergedVersion: TValue;
conflict: ThreeWayDiffConflict;
}

interface MergeArgs<TValue> {
targetVersion: TValue;
diffOutcome: ThreeWayDiffOutcome;
}

const mergeVersions = <TValue>({
targetVersion,
diffOutcome,
}: MergeArgs<TValue>): MergeResult<TValue> => {
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);
}
};

0 comments on commit 14cec3a

Please sign in to comment.