mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Security Solution] Adds diff algorithm and unit tests for multi-line string fields (#188022)
## Summary Related ticket: https://github.com/elastic/kibana/issues/180159 Adds diff algorithm for multi-line string fields and unit tests to cover all use cases. Also adds the [`node-diff3`](https://www.npmjs.com/package/node-diff3#3-way-diff-and-merging) package to utilize in the diffing logic to both determine if conflicts exist and merge the 3 rule versions together to create the returned merged version. ### 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)
This commit is contained in:
parent
39017342ed
commit
5f843a81ef
5 changed files with 323 additions and 3 deletions
|
@ -1099,6 +1099,7 @@
|
|||
"monaco-editor": "^0.44.0",
|
||||
"monaco-yaml": "^5.1.0",
|
||||
"mustache": "^2.3.2",
|
||||
"node-diff3": "^3.1.2",
|
||||
"node-fetch": "^2.6.7",
|
||||
"node-forge": "^1.3.1",
|
||||
"nodemailer": "^6.9.14",
|
||||
|
|
|
@ -9,3 +9,4 @@ export { numberDiffAlgorithm } from './number_diff_algorithm';
|
|||
export { singleLineStringDiffAlgorithm } from './single_line_string_diff_algorithm';
|
||||
export { scalarArrayDiffAlgorithm } from './scalar_array_diff_algorithm';
|
||||
export { simpleDiffAlgorithm } from './simple_diff_algorithm';
|
||||
export { multiLineStringDiffAlgorithm } from './multi_line_string_diff_algorithm';
|
||||
|
|
|
@ -0,0 +1,174 @@
|
|||
/*
|
||||
* 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 { ThreeVersionsOf } from '../../../../../../../../common/api/detection_engine';
|
||||
import {
|
||||
ThreeWayDiffOutcome,
|
||||
ThreeWayMergeOutcome,
|
||||
MissingVersion,
|
||||
} from '../../../../../../../../common/api/detection_engine';
|
||||
import { multiLineStringDiffAlgorithm } from './multi_line_string_diff_algorithm';
|
||||
|
||||
describe('multiLineStringDiffAlgorithm', () => {
|
||||
it('returns current_version as merged output if there is no update - scenario AAA', () => {
|
||||
const mockVersions: ThreeVersionsOf<string> = {
|
||||
base_version: 'My description.\nThis is a second line.',
|
||||
current_version: 'My description.\nThis is a second line.',
|
||||
target_version: 'My description.\nThis is a second line.',
|
||||
};
|
||||
|
||||
const result = multiLineStringDiffAlgorithm(mockVersions);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
merged_version: mockVersions.current_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.StockValueNoUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Current,
|
||||
has_conflict: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
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: 'My description.\nThis is a second line.',
|
||||
current_version: 'My GREAT description.\nThis is a second line.',
|
||||
target_version: 'My description.\nThis is a second line.',
|
||||
};
|
||||
|
||||
const result = multiLineStringDiffAlgorithm(mockVersions);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
merged_version: mockVersions.current_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.CustomizedValueNoUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Current,
|
||||
has_conflict: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
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: 'My description.\nThis is a second line.',
|
||||
current_version: 'My description.\nThis is a second line.',
|
||||
target_version: 'My GREAT description.\nThis is a second line.',
|
||||
};
|
||||
|
||||
const result = multiLineStringDiffAlgorithm(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<string> = {
|
||||
base_version: 'My description.\nThis is a second line.',
|
||||
current_version: 'My GREAT description.\nThis is a second line.',
|
||||
target_version: 'My GREAT description.\nThis is a second line.',
|
||||
};
|
||||
|
||||
const result = multiLineStringDiffAlgorithm(mockVersions);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
merged_version: mockVersions.current_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.CustomizedValueSameUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Current,
|
||||
has_conflict: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
describe('if all three versions are different - scenario ABC', () => {
|
||||
it('returns a computated merged version without a conflict if 3 way merge is possible', () => {
|
||||
const mockVersions: ThreeVersionsOf<string> = {
|
||||
base_version: `My description.\f\nThis is a second\u2001 line.\f\nThis is a third line.`,
|
||||
current_version: `My GREAT description.\f\nThis is a second\u2001 line.\f\nThis is a third line.`,
|
||||
target_version: `My description.\f\nThis is a second\u2001 line.\f\nThis is a GREAT line.`,
|
||||
};
|
||||
|
||||
const expectedMergedVersion = `My GREAT description.\f\nThis is a second\u2001 line.\f\nThis is a GREAT line.`;
|
||||
|
||||
const result = multiLineStringDiffAlgorithm(mockVersions);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
merged_version: expectedMergedVersion,
|
||||
diff_outcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Merged,
|
||||
has_conflict: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the current_version with a conflict if 3 way merge is not possible', () => {
|
||||
const mockVersions: ThreeVersionsOf<string> = {
|
||||
base_version: 'My description.\nThis is a second line.',
|
||||
current_version: 'My GREAT description.\nThis is a third line.',
|
||||
target_version: 'My EXCELLENT description.\nThis is a fourth.',
|
||||
};
|
||||
|
||||
const result = multiLineStringDiffAlgorithm(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<string> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: 'My description.\nThis is a second line.',
|
||||
target_version: 'My description.\nThis is a second line.',
|
||||
};
|
||||
|
||||
const result = multiLineStringDiffAlgorithm(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<string> = {
|
||||
base_version: MissingVersion,
|
||||
current_version: `My GREAT description.\nThis is a second line.`,
|
||||
target_version: `My description.\nThis is a second line, now longer.`,
|
||||
};
|
||||
|
||||
const result = multiLineStringDiffAlgorithm(mockVersions);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
merged_version: mockVersions.target_version,
|
||||
diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate,
|
||||
merge_outcome: ThreeWayMergeOutcome.Target,
|
||||
has_conflict: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* 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 { merge } from 'node-diff3';
|
||||
import { assertUnreachable } from '../../../../../../../../common/utility_types';
|
||||
import type {
|
||||
ThreeVersionsOf,
|
||||
ThreeWayDiff,
|
||||
} from '../../../../../../../../common/api/detection_engine/prebuilt_rules';
|
||||
import {
|
||||
determineDiffOutcome,
|
||||
determineIfValueCanUpdate,
|
||||
ThreeWayDiffOutcome,
|
||||
ThreeWayMergeOutcome,
|
||||
MissingVersion,
|
||||
} from '../../../../../../../../common/api/detection_engine/prebuilt_rules';
|
||||
|
||||
/**
|
||||
* Diff algorithm used for string fields that contain multiple lines
|
||||
*/
|
||||
export const multiLineStringDiffAlgorithm = (
|
||||
versions: ThreeVersionsOf<string>
|
||||
): ThreeWayDiff<string> => {
|
||||
const {
|
||||
base_version: baseVersion,
|
||||
current_version: currentVersion,
|
||||
target_version: targetVersion,
|
||||
} = versions;
|
||||
|
||||
const 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: string;
|
||||
}
|
||||
|
||||
interface MergeArgs {
|
||||
baseVersion: string | MissingVersion;
|
||||
currentVersion: string;
|
||||
targetVersion: string;
|
||||
diffOutcome: ThreeWayDiffOutcome;
|
||||
}
|
||||
|
||||
const mergeVersions = ({
|
||||
baseVersion,
|
||||
currentVersion,
|
||||
targetVersion,
|
||||
diffOutcome,
|
||||
}: MergeArgs): MergeResult => {
|
||||
switch (diffOutcome) {
|
||||
case ThreeWayDiffOutcome.StockValueNoUpdate:
|
||||
case ThreeWayDiffOutcome.CustomizedValueNoUpdate:
|
||||
case ThreeWayDiffOutcome.CustomizedValueSameUpdate: {
|
||||
return {
|
||||
mergeOutcome: ThreeWayMergeOutcome.Current,
|
||||
mergedVersion: currentVersion,
|
||||
};
|
||||
}
|
||||
case ThreeWayDiffOutcome.StockValueCanUpdate: {
|
||||
return {
|
||||
mergeOutcome: ThreeWayMergeOutcome.Target,
|
||||
mergedVersion: targetVersion,
|
||||
};
|
||||
}
|
||||
case ThreeWayDiffOutcome.CustomizedValueCanUpdate: {
|
||||
if (baseVersion === MissingVersion) {
|
||||
return {
|
||||
mergeOutcome: ThreeWayMergeOutcome.Conflict,
|
||||
mergedVersion: currentVersion,
|
||||
};
|
||||
}
|
||||
const mergedVersion = merge(currentVersion, baseVersion, targetVersion, {
|
||||
stringSeparator: /(\S+|\s+)/g, // Retains all whitespace, which we keep to preserve formatting
|
||||
});
|
||||
|
||||
return mergedVersion.conflict
|
||||
? {
|
||||
mergeOutcome: ThreeWayMergeOutcome.Conflict,
|
||||
mergedVersion: currentVersion,
|
||||
}
|
||||
: {
|
||||
mergeOutcome: ThreeWayMergeOutcome.Merged,
|
||||
mergedVersion: mergedVersion.result.join(''),
|
||||
};
|
||||
}
|
||||
default:
|
||||
return assertUnreachable(diffOutcome);
|
||||
}
|
||||
};
|
36
yarn.lock
36
yarn.lock
|
@ -23772,6 +23772,11 @@ node-cache@^5.1.0:
|
|||
dependencies:
|
||||
clone "2.x"
|
||||
|
||||
node-diff3@^3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/node-diff3/-/node-diff3-3.1.2.tgz#49df8d821dc9cbab87bfd6182171d90169613a97"
|
||||
integrity sha512-wUd9TWy059I8mZdH6G3LPNlAEfxDvXtn/RcyFrbqL3v34WlDxn+Mh4HDhOwWuaMk/ROVepe5tTpnGHbve6Db2g==
|
||||
|
||||
node-dir@^0.1.10:
|
||||
version "0.1.17"
|
||||
resolved "https://registry.yarnpkg.com/node-dir/-/node-dir-0.1.17.tgz#5f5665d93351335caabef8f1c554516cf5f1e4e5"
|
||||
|
@ -29229,7 +29234,7 @@ string-replace-loader@^2.2.0:
|
|||
loader-utils "^1.2.3"
|
||||
schema-utils "^1.0.0"
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
|
@ -29247,6 +29252,15 @@ string-width@^1.0.1:
|
|||
is-fullwidth-code-point "^1.0.0"
|
||||
strip-ansi "^3.0.0"
|
||||
|
||||
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
string-width@^5.0.1, string-width@^5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794"
|
||||
|
@ -29357,7 +29371,7 @@ stringify-object@^3.2.1:
|
|||
is-obj "^1.0.1"
|
||||
is-regexp "^1.0.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
|
@ -29371,6 +29385,13 @@ strip-ansi@^3.0.0, strip-ansi@^3.0.1:
|
|||
dependencies:
|
||||
ansi-regex "^2.0.0"
|
||||
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^7.0.1, strip-ansi@^7.1.0:
|
||||
version "7.1.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
|
||||
|
@ -32244,7 +32265,7 @@ workerpool@6.2.1:
|
|||
resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343"
|
||||
integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==
|
||||
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
|
@ -32270,6 +32291,15 @@ wrap-ansi@^6.2.0:
|
|||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^8.1.0:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue