diff --git a/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/installation_and_upgrade.md b/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/installation_and_upgrade.md index e6454eb424141..e2f98296e199c 100644 --- a/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/installation_and_upgrade.md +++ b/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/installation_and_upgrade.md @@ -893,8 +893,8 @@ And field is customized by the user (current version != base versio And field is updated by Elastic in this upgrade (target version != base version) And customized field is the same as the Elastic update in this upgrade (current version == target version) Then for field the diff algorithm should output the current version as the merged one without a conflict -And field should not be returned from the `upgrade/_review` API endpoint -And field should not be shown in the upgrade preview UI +And field should be returned from the `upgrade/_review` API endpoint +And field should be shown in the upgrade preview UI Examples: | field_name | base_version | current_version | target_version | diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/schema.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/schema.tsx index b4fa6376ea818..83d79debb757d 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/schema.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/schema.tsx @@ -217,7 +217,7 @@ export const schema: FormSchema = { label: i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldMitreThreatLabel', { - defaultMessage: 'MITRE ATT&CK\\u2122', + defaultMessage: 'MITRE ATT&CK\u2122', } ), labelAppend: OptionalFieldLabel, diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/helpers.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/helpers.ts index f08187800789d..fd933ba33ae7e 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/helpers.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/helpers.ts @@ -5,6 +5,8 @@ * 2.0. */ +import type { RuleFieldsDiff, ThreeWayDiff } from '../../../../../common/api/detection_engine'; +import { ThreeWayDiffOutcome } from '../../../../../common/api/detection_engine'; import type { FieldsGroupDiff } from '../../model/rule_details/rule_field_diff'; import { ABOUT_UPGRADE_FIELD_ORDER, @@ -36,3 +38,21 @@ export const getSectionedFieldDiffs = (fields: FieldsGroupDiff[]) => { setupFields, }; }; + +/** + * Filters out any fields that have a `diff_outcome` of `CustomizedValueNoUpdate` + * or `CustomizedValueSameUpdate` as they are not supported for display in the + * current per-field rule diff flyout + */ +export const filterUnsupportedDiffOutcomes = ( + fields: Partial +): Partial => + Object.fromEntries( + Object.entries(fields).filter(([key, value]) => { + const diff = value as ThreeWayDiff; + return ( + diff.diff_outcome !== ThreeWayDiffOutcome.CustomizedValueNoUpdate && + diff.diff_outcome !== ThreeWayDiffOutcome.CustomizedValueSameUpdate + ); + }) + ); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/per_field_rule_diff_tab.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/per_field_rule_diff_tab.tsx index 4a90f8624d21e..f03cd8ed23bbf 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/per_field_rule_diff_tab.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/per_field_rule_diff_tab.tsx @@ -10,7 +10,7 @@ import type { PartialRuleDiff, RuleFieldsDiff } from '../../../../../common/api/ import { getFormattedFieldDiffGroups } from './per_field_diff/get_formatted_field_diff'; import { UPGRADE_FIELD_ORDER } from './constants'; import { RuleDiffHeaderBar, RuleDiffSection } from './diff_components'; -import { getSectionedFieldDiffs } from './helpers'; +import { filterUnsupportedDiffOutcomes, getSectionedFieldDiffs } from './helpers'; import type { FieldsGroupDiff } from '../../model/rule_details/rule_field_diff'; import * as i18n from './translations'; @@ -21,9 +21,11 @@ interface PerFieldRuleDiffTabProps { export const PerFieldRuleDiffTab = ({ ruleDiff }: PerFieldRuleDiffTabProps) => { const fieldsToRender = useMemo(() => { const fields: FieldsGroupDiff[] = []; - for (const field of Object.keys(ruleDiff.fields)) { + // Filter out diff outcomes that we don't support displaying in the per-field diff flyout + const filteredFieldDiffs = filterUnsupportedDiffOutcomes(ruleDiff.fields); + for (const field of Object.keys(filteredFieldDiffs)) { const typedField = field as keyof RuleFieldsDiff; - const formattedDiffs = getFormattedFieldDiffGroups(typedField, ruleDiff.fields); + const formattedDiffs = getFormattedFieldDiffGroups(typedField, filteredFieldDiffs); fields.push({ formattedDiffs, fieldsGroupName: typedField }); } const sortedFields = fields.sort( diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/translations.ts index 9025b184af1d3..3e75677d54da9 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/translations.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/translations.ts @@ -171,7 +171,7 @@ export const RULE_NAME_OVERRIDE_FIELD_LABEL = i18n.translate( export const THREAT_FIELD_LABEL = i18n.translate( 'xpack.securitySolution.detectionEngine.ruleDetails.threatFieldLabel', { - defaultMessage: 'MITRE ATT&CK\\u2122', + defaultMessage: 'MITRE ATT&CK\u2122', } ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_route.ts index 2270c6fea7396..de7db929790de 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_route.ts @@ -7,7 +7,10 @@ import { transformError } from '@kbn/securitysolution-es-utils'; import { pickBy } from 'lodash'; -import { REVIEW_RULE_UPGRADE_URL } from '../../../../../../common/api/detection_engine/prebuilt_rules'; +import { + REVIEW_RULE_UPGRADE_URL, + ThreeWayDiffOutcome, +} from '../../../../../../common/api/detection_engine/prebuilt_rules'; import type { ReviewRuleUpgradeResponseBody, RuleUpgradeInfoForReview, @@ -120,7 +123,7 @@ const calculateRuleInfos = (results: CalculateRuleDiffResult[]): RuleUpgradeInfo diff: { fields: pickBy>( ruleDiff.fields, - (fieldDiff) => fieldDiff.has_update || fieldDiff.has_conflict + (fieldDiff) => fieldDiff.diff_outcome !== ThreeWayDiffOutcome.StockValueNoUpdate ), has_conflict: ruleDiff.has_conflict, }, 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 new file mode 100644 index 0000000000000..57dd3553cb4c7 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/index.ts @@ -0,0 +1,10 @@ +/* + * 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. + */ + +export { numberDiffAlgorithm } from './number_diff_algorithm'; +export { singleLineStringDiffAlgorithm } from './single_line_string_diff_algorithm'; +export { simpleDiffAlgorithm } from './simple_diff_algorithm'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/number_diff_algorithm.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/number_diff_algorithm.ts index 513d9047c7a5c..30c32a475ecfb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/number_diff_algorithm.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/number_diff_algorithm.ts @@ -5,6 +5,9 @@ * 2.0. */ +import type { ThreeVersionsOf } from '../../../../../../../../common/api/detection_engine'; import { simpleDiffAlgorithm } from './simple_diff_algorithm'; -export const numberDiffAlgorithm = simpleDiffAlgorithm; +export const numberDiffAlgorithm = ( + versions: ThreeVersionsOf +) => simpleDiffAlgorithm(versions); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/single_line_string_diff_algorithm.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/single_line_string_diff_algorithm.ts index 901bb6c050e51..f80d8b63c8da8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/single_line_string_diff_algorithm.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/single_line_string_diff_algorithm.ts @@ -5,6 +5,9 @@ * 2.0. */ +import type { ThreeVersionsOf } from '../../../../../../../../common/api/detection_engine'; import { simpleDiffAlgorithm } from './simple_diff_algorithm'; -export const singleLineStringDiffAlgorithm = simpleDiffAlgorithm; +export const singleLineStringDiffAlgorithm = ( + versions: ThreeVersionsOf +) => simpleDiffAlgorithm(versions); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/calculate_rule_fields_diff.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/calculate_rule_fields_diff.ts index 5639cc07ebedd..ea482858650fb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/calculate_rule_fields_diff.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/calculate_rule_fields_diff.ts @@ -37,7 +37,11 @@ import type { FieldsDiffAlgorithmsFor } from '../../../../../../../common/api/de import type { ThreeVersionsOf } from '../../../../../../../common/api/detection_engine/prebuilt_rules/model/diff/three_way_diff/three_way_diff'; import { MissingVersion } from '../../../../../../../common/api/detection_engine/prebuilt_rules/model/diff/three_way_diff/three_way_diff'; import { calculateFieldsDiffFor } from './diff_calculation_helpers'; -import { simpleDiffAlgorithm } from './algorithms/simple_diff_algorithm'; +import { + numberDiffAlgorithm, + simpleDiffAlgorithm, + singleLineStringDiffAlgorithm, +} from './algorithms'; 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`; @@ -168,14 +172,14 @@ const calculateCommonFieldsDiff = ( const commonFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor = { rule_id: simpleDiffAlgorithm, - version: simpleDiffAlgorithm, + version: numberDiffAlgorithm, meta: simpleDiffAlgorithm, - name: simpleDiffAlgorithm, + name: singleLineStringDiffAlgorithm, tags: simpleDiffAlgorithm, description: simpleDiffAlgorithm, - severity: simpleDiffAlgorithm, + severity: singleLineStringDiffAlgorithm, severity_mapping: simpleDiffAlgorithm, - risk_score: simpleDiffAlgorithm, + risk_score: numberDiffAlgorithm, risk_score_mapping: simpleDiffAlgorithm, references: simpleDiffAlgorithm, false_positives: simpleDiffAlgorithm, @@ -185,12 +189,12 @@ const commonFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor related_integrations: simpleDiffAlgorithm, required_fields: simpleDiffAlgorithm, author: simpleDiffAlgorithm, - license: simpleDiffAlgorithm, + license: singleLineStringDiffAlgorithm, rule_schedule: simpleDiffAlgorithm, actions: simpleDiffAlgorithm, throttle: simpleDiffAlgorithm, exceptions_list: simpleDiffAlgorithm, - max_signals: simpleDiffAlgorithm, + max_signals: numberDiffAlgorithm, rule_name_override: simpleDiffAlgorithm, timestamp_override: simpleDiffAlgorithm, timeline_template: simpleDiffAlgorithm, @@ -233,9 +237,9 @@ const eqlFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor = { type: simpleDiffAlgorithm, eql_query: simpleDiffAlgorithm, data_source: simpleDiffAlgorithm, - event_category_override: simpleDiffAlgorithm, - timestamp_field: simpleDiffAlgorithm, - tiebreaker_field: simpleDiffAlgorithm, + event_category_override: singleLineStringDiffAlgorithm, + timestamp_field: singleLineStringDiffAlgorithm, + tiebreaker_field: singleLineStringDiffAlgorithm, }; const calculateEsqlFieldsDiff = ( @@ -262,7 +266,7 @@ const threatMatchFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor { loadTestFile(require.resolve('./upgrade_prebuilt_rules')); loadTestFile(require.resolve('./upgrade_prebuilt_rules_with_historical_versions')); loadTestFile(require.resolve('./fleet_integration')); + loadTestFile(require.resolve('./upgrade_review_prebuilt_rules')); }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_review_prebuilt_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_review_prebuilt_rules.ts new file mode 100644 index 0000000000000..9ad266e20740d --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_review_prebuilt_rules.ts @@ -0,0 +1,715 @@ +/* + * 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 expect from 'expect'; +import { + ThreeWayDiffOutcome, + ThreeWayMergeOutcome, +} from '@kbn/security-solution-plugin/common/api/detection_engine'; +import { FtrProviderContext } from '../../../../../../ftr_provider_context'; +import { + deleteAllTimelines, + deleteAllPrebuiltRuleAssets, + createRuleAssetSavedObject, + installPrebuiltRules, + createPrebuiltRuleAssetSavedObjects, + reviewPrebuiltRulesToUpgrade, + patchRule, + createHistoricalPrebuiltRuleAssetSavedObjects, +} from '../../../../utils'; +import { deleteAllRules } from '../../../../../../../common/utils/security_solution'; + +export default ({ getService }: FtrProviderContext): void => { + const es = getService('es'); + const supertest = getService('supertest'); + const log = getService('log'); + + describe('@ess @serverless @skipInServerlessMKI review prebuilt rules updates from package with mock rule assets', () => { + beforeEach(async () => { + await deleteAllRules(supertest, log); + await deleteAllTimelines(es, log); + await deleteAllPrebuiltRuleAssets(es, log); + }); + + describe(`single line string fields`, () => { + const getRuleAssetSavedObjects = () => [ + createRuleAssetSavedObject({ rule_id: 'rule-1', version: 1, name: 'A' }), + ]; + + describe("when rule field doesn't have an update and has no custom value", () => { + it('should not show in the upgrade/_review API response', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + await installPrebuiltRules(es, supertest); + + // Increment the version of the installed rule, do NOT update the related single line string field, and create the new rule assets + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + name: 'A', + version: 2, + }), + ]; + await createHistoricalPrebuiltRuleAssetSavedObjects(es, updatedRuleAssetSavedObjects); + + // Call the upgrade review prebuilt rules endpoint and check that there is 1 rule eligable for update but single line string field is NOT returned + const reviewResponse = await reviewPrebuiltRulesToUpgrade(supertest); + expect(reviewResponse.rules[0].diff.fields).toEqual({ + version: { + base_version: 1, + current_version: 1, + target_version: 2, + merged_version: 2, + diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + has_conflict: false, + has_update: true, + }, + }); + expect(reviewResponse.rules[0].diff.has_conflict).toBe(false); + expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); + }); + }); + + describe("when rule field doesn't have an update but has a custom value", () => { + it('should not show in the upgrade/_review API response', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + await installPrebuiltRules(es, supertest); + + // Customize a single line string field on the installed rule + await patchRule(supertest, log, { + rule_id: 'rule-1', + name: 'B', + }); + + // Increment the version of the installed rule, do NOT update the related single line string field, and create the new rule assets + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + name: 'A', + version: 2, + }), + ]; + await createHistoricalPrebuiltRuleAssetSavedObjects(es, updatedRuleAssetSavedObjects); + + // Call the upgrade review prebuilt rules endpoint and check that single line string diff field is returned but field does not have an update + const reviewResponse = await reviewPrebuiltRulesToUpgrade(supertest); + expect(reviewResponse.rules[0].diff.fields).toEqual({ + name: { + base_version: 'A', + current_version: 'B', + target_version: 'A', + merged_version: 'B', + diff_outcome: ThreeWayDiffOutcome.CustomizedValueNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Current, + has_conflict: false, + has_update: false, + }, + version: { + base_version: 1, + current_version: 1, + target_version: 2, + merged_version: 2, + diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + has_conflict: false, + has_update: true, + }, + }); + expect(reviewResponse.rules[0].diff.has_conflict).toBe(false); + expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); + }); + }); + + describe('when rule field has an update but does not have a custom value', () => { + it('should show in the upgrade/_review API response', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + await installPrebuiltRules(es, supertest); + + // Increment the version of the installed rule, update a single line string field, and create the new rule assets + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 2, + name: 'B', + }), + ]; + await createHistoricalPrebuiltRuleAssetSavedObjects(es, updatedRuleAssetSavedObjects); + + // Call the upgrade review prebuilt rules endpoint and check that one rule is eligible for update + const reviewResponse = await reviewPrebuiltRulesToUpgrade(supertest); + expect(reviewResponse.rules[0].diff.fields).toEqual({ + name: { + base_version: 'A', + current_version: 'A', + target_version: 'B', + merged_version: 'B', + diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + has_conflict: false, + has_update: true, + }, + version: { + base_version: 1, + current_version: 1, + target_version: 2, + merged_version: 2, + diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + has_conflict: false, + has_update: true, + }, + }); + expect(reviewResponse.rules[0].diff.has_conflict).toBe(false); + expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); + }); + + describe('when rule field has an update and a custom value that are the same', () => { + it('should show in the upgrade/_review API response', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + await installPrebuiltRules(es, supertest); + + // Customize a single line string field on the installed rule + await patchRule(supertest, log, { + rule_id: 'rule-1', + name: 'B', + }); + + // Increment the version of the installed rule, update a single line string field, and create the new rule assets + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 2, + name: 'B', + }), + ]; + await createHistoricalPrebuiltRuleAssetSavedObjects(es, updatedRuleAssetSavedObjects); + + // Call the upgrade review prebuilt rules endpoint and check that one rule is eligible for update + const reviewResponse = await reviewPrebuiltRulesToUpgrade(supertest); + expect(reviewResponse.rules[0].diff.fields).toEqual({ + name: { + base_version: 'A', + current_version: 'B', + target_version: 'B', + merged_version: 'B', + diff_outcome: ThreeWayDiffOutcome.CustomizedValueSameUpdate, + merge_outcome: ThreeWayMergeOutcome.Current, + has_conflict: false, + has_update: false, + }, + version: { + base_version: 1, + current_version: 1, + target_version: 2, + merged_version: 2, + diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + has_conflict: false, + has_update: true, + }, + }); + expect(reviewResponse.rules[0].diff.has_conflict).toBe(false); + expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); + }); + }); + + describe('when rule field has an update and a custom value that are different', () => { + it('should show in the upgrade/_review API response', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + await installPrebuiltRules(es, supertest); + + // Customize a single line string field on the installed rule + await patchRule(supertest, log, { + rule_id: 'rule-1', + name: 'B', + }); + + // Increment the version of the installed rule, update a single line string field, and create the new rule assets + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 2, + name: 'C', + }), + ]; + await createHistoricalPrebuiltRuleAssetSavedObjects(es, updatedRuleAssetSavedObjects); + + // Call the upgrade review prebuilt rules endpoint and check that one rule is eligible for update + // and single line string field update has conflict + const reviewResponse = await reviewPrebuiltRulesToUpgrade(supertest); + expect(reviewResponse.rules[0].diff.fields).toEqual({ + name: { + base_version: 'A', + current_version: 'B', + target_version: 'C', + merged_version: 'B', + diff_outcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Conflict, + has_conflict: true, + has_update: true, + }, + version: { + base_version: 1, + current_version: 1, + target_version: 2, + merged_version: 2, + diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + has_conflict: false, + has_update: true, + }, + }); + expect(reviewResponse.rules[0].diff.has_conflict).toBe(true); + expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); + }); + }); + + describe('when rule base version does not exist', () => { + describe('when rule field has an update and a custom value that are the same', () => { + it('should not show in the upgrade/_review API response', async () => { + // Install base prebuilt detection rule + await createPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + await installPrebuiltRules(es, supertest); + + // Clear previous rule assets + await deleteAllPrebuiltRuleAssets(es, log); + + // Customize a single line string field on the installed rule + await patchRule(supertest, log, { + rule_id: 'rule-1', + name: 'B', + }); + + // Increment the version of the installed rule, update a single line string field, and create the new rule assets + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 2, + name: 'B', + }), + ]; + await createPrebuiltRuleAssetSavedObjects(es, updatedRuleAssetSavedObjects); + + // Call the upgrade review prebuilt rules endpoint and check that one rule is eligible for update + // but does NOT contain single line string field + const reviewResponse = await reviewPrebuiltRulesToUpgrade(supertest); + expect(reviewResponse.rules[0].diff.fields).toEqual({ + version: { + current_version: 1, + target_version: 2, + merged_version: 2, + diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + has_conflict: false, + has_update: true, + }, + }); + expect(reviewResponse.rules[0].diff.has_conflict).toBe(false); + expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); + }); + }); + + describe('when rule field has an update and a custom value that are different', () => { + it('should show in the upgrade/_review API response', async () => { + // Install base prebuilt detection rule + await createPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + await installPrebuiltRules(es, supertest); + + // Clear previous rule assets + await deleteAllPrebuiltRuleAssets(es, log); + + // Customize a single line string field on the installed rule + await patchRule(supertest, log, { + rule_id: 'rule-1', + name: 'B', + }); + + // Increment the version of the installed rule, update a single line string field, and create the new rule assets + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 2, + name: 'C', + }), + ]; + await createPrebuiltRuleAssetSavedObjects(es, updatedRuleAssetSavedObjects); + + // Call the upgrade review prebuilt rules endpoint and check that one rule is eligible for update + // and single line string field update does not have a conflict + const reviewResponse = await reviewPrebuiltRulesToUpgrade(supertest); + expect(reviewResponse.rules[0].diff.fields).toEqual({ + name: { + current_version: 'B', + target_version: 'C', + merged_version: 'C', + diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + has_conflict: false, + has_update: true, + }, + version: { + current_version: 1, + target_version: 2, + merged_version: 2, + diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + has_conflict: false, + has_update: true, + }, + }); + expect(reviewResponse.rules[0].diff.has_conflict).toBe(false); + expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); + }); + }); + }); + }); + }); + + describe(`number fields`, () => { + const getRuleAssetSavedObjects = () => [ + createRuleAssetSavedObject({ rule_id: 'rule-1', version: 1, risk_score: 1 }), + ]; + + describe("when rule field doesn't have an update and has no custom value", () => { + it('should not show in the upgrade/_review API response', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + await installPrebuiltRules(es, supertest); + + // Increment the version of the installed rule, do NOT update the related number field, and create the new rule assets + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + risk_score: 1, + version: 2, + }), + ]; + await createHistoricalPrebuiltRuleAssetSavedObjects(es, updatedRuleAssetSavedObjects); + + // Call the upgrade review prebuilt rules endpoint and check that there is 1 rule eligable for update but number field is NOT returned + const reviewResponse = await reviewPrebuiltRulesToUpgrade(supertest); + expect(reviewResponse.rules[0].diff.fields).toEqual({ + version: { + base_version: 1, + current_version: 1, + target_version: 2, + merged_version: 2, + diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + has_conflict: false, + has_update: true, + }, + }); + expect(reviewResponse.rules[0].diff.has_conflict).toBe(false); + expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); + }); + }); + + describe("when rule field doesn't have an update but has a custom value", () => { + it('should not show in the upgrade/_review API response', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + await installPrebuiltRules(es, supertest); + + // Customize a number field on the installed rule + await patchRule(supertest, log, { + rule_id: 'rule-1', + risk_score: 2, + }); + + // Increment the version of the installed rule, do NOT update the related number field, and create the new rule assets + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + risk_score: 1, + version: 2, + }), + ]; + await createHistoricalPrebuiltRuleAssetSavedObjects(es, updatedRuleAssetSavedObjects); + + // Call the upgrade review prebuilt rules endpoint and check that number diff field is returned but field does not have an update + const reviewResponse = await reviewPrebuiltRulesToUpgrade(supertest); + expect(reviewResponse.rules[0].diff.fields).toEqual({ + risk_score: { + base_version: 1, + current_version: 2, + target_version: 1, + merged_version: 2, + diff_outcome: ThreeWayDiffOutcome.CustomizedValueNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Current, + has_conflict: false, + has_update: false, + }, + version: { + base_version: 1, + current_version: 1, + target_version: 2, + merged_version: 2, + diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + has_conflict: false, + has_update: true, + }, + }); + expect(reviewResponse.rules[0].diff.has_conflict).toBe(false); + expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); + }); + }); + + describe('when rule field has an update but does not have a custom value', () => { + it('should show in the upgrade/_review API response', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + await installPrebuiltRules(es, supertest); + + // Increment the version of the installed rule, update a number field, and create the new rule assets + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 2, + risk_score: 2, + }), + ]; + await createHistoricalPrebuiltRuleAssetSavedObjects(es, updatedRuleAssetSavedObjects); + + // Call the upgrade review prebuilt rules endpoint and check that one rule is eligible for update + const reviewResponse = await reviewPrebuiltRulesToUpgrade(supertest); + expect(reviewResponse.rules[0].diff.fields).toEqual({ + risk_score: { + base_version: 1, + current_version: 1, + target_version: 2, + merged_version: 2, + diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + has_conflict: false, + has_update: true, + }, + version: { + base_version: 1, + current_version: 1, + target_version: 2, + merged_version: 2, + diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + has_conflict: false, + has_update: true, + }, + }); + expect(reviewResponse.rules[0].diff.has_conflict).toBe(false); + expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); + }); + + describe('when rule field has an update and a custom value that are the same', () => { + it('should show in the upgrade/_review API response', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + await installPrebuiltRules(es, supertest); + + // Customize a number field on the installed rule + await patchRule(supertest, log, { + rule_id: 'rule-1', + risk_score: 2, + }); + + // Increment the version of the installed rule, update a number field, and create the new rule assets + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 2, + risk_score: 2, + }), + ]; + await createHistoricalPrebuiltRuleAssetSavedObjects(es, updatedRuleAssetSavedObjects); + + // Call the upgrade review prebuilt rules endpoint and check that one rule is eligible for update and contains number field + const reviewResponse = await reviewPrebuiltRulesToUpgrade(supertest); + expect(reviewResponse.rules[0].diff.fields).toEqual({ + risk_score: { + base_version: 1, + current_version: 2, + target_version: 2, + merged_version: 2, + diff_outcome: ThreeWayDiffOutcome.CustomizedValueSameUpdate, + merge_outcome: ThreeWayMergeOutcome.Current, + has_conflict: false, + has_update: false, + }, + version: { + base_version: 1, + current_version: 1, + target_version: 2, + merged_version: 2, + diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + has_conflict: false, + has_update: true, + }, + }); + expect(reviewResponse.rules[0].diff.has_conflict).toBe(false); + expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); + }); + }); + + describe('when rule field has an update and a custom value that are different', () => { + it('should show in the upgrade/_review API response', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + await installPrebuiltRules(es, supertest); + + // Customize a number field on the installed rule + await patchRule(supertest, log, { + rule_id: 'rule-1', + risk_score: 2, + }); + + // Increment the version of the installed rule, update a number field, and create the new rule assets + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 2, + risk_score: 3, + }), + ]; + await createHistoricalPrebuiltRuleAssetSavedObjects(es, updatedRuleAssetSavedObjects); + + // Call the upgrade review prebuilt rules endpoint and check that one rule is eligible for update + // and number field update has conflict + const reviewResponse = await reviewPrebuiltRulesToUpgrade(supertest); + expect(reviewResponse.rules[0].diff.fields).toEqual({ + risk_score: { + base_version: 1, + current_version: 2, + target_version: 3, + merged_version: 2, + diff_outcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Conflict, + has_conflict: true, + has_update: true, + }, + version: { + base_version: 1, + current_version: 1, + target_version: 2, + merged_version: 2, + diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + has_conflict: false, + has_update: true, + }, + }); + expect(reviewResponse.rules[0].diff.has_conflict).toBe(true); + expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); + }); + }); + + describe('when rule base version does not exist', () => { + describe('when rule field has an update and a custom value that are the same', () => { + it('should not show in the upgrade/_review API response', async () => { + // Install base prebuilt detection rule + await createPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + await installPrebuiltRules(es, supertest); + + // Clear previous rule assets + await deleteAllPrebuiltRuleAssets(es, log); + + // Customize a number field on the installed rule + await patchRule(supertest, log, { + rule_id: 'rule-1', + risk_score: 2, + }); + + // Increment the version of the installed rule, update a number field, and create the new rule assets + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 2, + risk_score: 2, + }), + ]; + await createPrebuiltRuleAssetSavedObjects(es, updatedRuleAssetSavedObjects); + + // Call the upgrade review prebuilt rules endpoint and check that one rule is eligible for update + // but does NOT contain number field + const reviewResponse = await reviewPrebuiltRulesToUpgrade(supertest); + expect(reviewResponse.rules[0].diff.fields).toEqual({ + version: { + current_version: 1, + target_version: 2, + merged_version: 2, + diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + has_conflict: false, + has_update: true, + }, + }); + expect(reviewResponse.rules[0].diff.has_conflict).toBe(false); + expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); + }); + }); + + describe('when rule field has an update and a custom value that are different', () => { + it('should show in the upgrade/_review API response', async () => { + // Install base prebuilt detection rule + await createPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + await installPrebuiltRules(es, supertest); + + // Clear previous rule assets + await deleteAllPrebuiltRuleAssets(es, log); + + // Customize a number field on the installed rule + await patchRule(supertest, log, { + rule_id: 'rule-1', + risk_score: 2, + }); + + // Increment the version of the installed rule, update a number field, and create the new rule assets + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 2, + risk_score: 3, + }), + ]; + await createPrebuiltRuleAssetSavedObjects(es, updatedRuleAssetSavedObjects); + + // Call the upgrade review prebuilt rules endpoint and check that one rule is eligible for update + // and number field update does not have a conflict + const reviewResponse = await reviewPrebuiltRulesToUpgrade(supertest); + expect(reviewResponse.rules[0].diff.fields).toEqual({ + risk_score: { + current_version: 2, + target_version: 3, + merged_version: 3, + diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + has_conflict: false, + has_update: true, + }, + version: { + current_version: 1, + target_version: 2, + merged_version: 2, + diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + has_conflict: false, + has_update: true, + }, + }); + expect(reviewResponse.rules[0].diff.has_conflict).toBe(false); + expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); + }); + }); + }); + }); + }); + }); +};