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 4f8126d315072..80536451b6f3c 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 @@ -44,6 +44,9 @@ import { scalarArrayDiffAlgorithm, simpleDiffAlgorithm, singleLineStringDiffAlgorithm, + kqlQueryDiffAlgorithm, + eqlQueryDiffAlgorithm, + esqlQueryDiffAlgorithm, } from './algorithms'; const BASE_TYPE_ERROR = `Base version can't be of different rule type`; @@ -210,7 +213,7 @@ const calculateCustomQueryFieldsDiff = ( const customQueryFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor = { type: simpleDiffAlgorithm, - kql_query: simpleDiffAlgorithm, + kql_query: kqlQueryDiffAlgorithm, data_source: dataSourceDiffAlgorithm, alert_suppression: simpleDiffAlgorithm, }; @@ -223,7 +226,7 @@ const calculateSavedQueryFieldsDiff = ( const savedQueryFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor = { type: simpleDiffAlgorithm, - kql_query: simpleDiffAlgorithm, + kql_query: kqlQueryDiffAlgorithm, data_source: dataSourceDiffAlgorithm, alert_suppression: simpleDiffAlgorithm, }; @@ -236,7 +239,7 @@ const calculateEqlFieldsDiff = ( const eqlFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor = { type: simpleDiffAlgorithm, - eql_query: simpleDiffAlgorithm, + eql_query: eqlQueryDiffAlgorithm, data_source: dataSourceDiffAlgorithm, event_category_override: singleLineStringDiffAlgorithm, timestamp_field: singleLineStringDiffAlgorithm, @@ -252,7 +255,7 @@ const calculateEsqlFieldsDiff = ( const esqlFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor = { type: simpleDiffAlgorithm, - esql_query: simpleDiffAlgorithm, + esql_query: esqlQueryDiffAlgorithm, alert_suppression: simpleDiffAlgorithm, }; @@ -264,9 +267,9 @@ const calculateThreatMatchFieldsDiff = ( const threatMatchFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor = { type: simpleDiffAlgorithm, - kql_query: simpleDiffAlgorithm, + kql_query: kqlQueryDiffAlgorithm, data_source: dataSourceDiffAlgorithm, - threat_query: simpleDiffAlgorithm, + threat_query: kqlQueryDiffAlgorithm, threat_index: scalarArrayDiffAlgorithm, threat_mapping: simpleDiffAlgorithm, threat_indicator_path: singleLineStringDiffAlgorithm, @@ -282,7 +285,7 @@ const calculateThresholdFieldsDiff = ( const thresholdFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor = { type: simpleDiffAlgorithm, - kql_query: simpleDiffAlgorithm, + kql_query: kqlQueryDiffAlgorithm, data_source: dataSourceDiffAlgorithm, threshold: simpleDiffAlgorithm, alert_suppression: simpleDiffAlgorithm, @@ -310,7 +313,7 @@ const calculateNewTermsFieldsDiff = ( const newTermsFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor = { type: simpleDiffAlgorithm, - kql_query: simpleDiffAlgorithm, + kql_query: kqlQueryDiffAlgorithm, data_source: dataSourceDiffAlgorithm, new_terms_fields: scalarArrayDiffAlgorithm, history_window_start: singleLineStringDiffAlgorithm, diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/index.ts index 4c8efcfa751e0..819b253dc7a66 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/index.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/index.ts @@ -22,6 +22,9 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./upgrade_review_prebuilt_rules.scalar_array_fields')); loadTestFile(require.resolve('./upgrade_review_prebuilt_rules.multi_line_string_fields')); loadTestFile(require.resolve('./upgrade_review_prebuilt_rules.data_source_fields')); + loadTestFile(require.resolve('./upgrade_review_prebuilt_rules.kql_query_fields')); + loadTestFile(require.resolve('./upgrade_review_prebuilt_rules.eql_query_fields')); + loadTestFile(require.resolve('./upgrade_review_prebuilt_rules.esql_query_fields')); loadTestFile(require.resolve('./upgrade_review_prebuilt_rules.stats')); }); }; 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.eql_query_fields.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.eql_query_fields.ts new file mode 100644 index 0000000000000..f500f8691485b --- /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.eql_query_fields.ts @@ -0,0 +1,465 @@ +/* + * 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 { + AllFieldsDiff, + RuleUpdateProps, + ThreeWayDiffConflict, + ThreeWayDiffOutcome, + ThreeWayMergeOutcome, +} from '@kbn/security-solution-plugin/common/api/detection_engine'; +import { getPrebuiltRuleMock } from '@kbn/security-solution-plugin/server/lib/detection_engine/prebuilt_rules/mocks'; +import { FtrProviderContext } from '../../../../../../ftr_provider_context'; +import { + deleteAllTimelines, + deleteAllPrebuiltRuleAssets, + createRuleAssetSavedObject, + installPrebuiltRules, + createPrebuiltRuleAssetSavedObjects, + reviewPrebuiltRulesToUpgrade, + createHistoricalPrebuiltRuleAssetSavedObjects, + updateRule, +} 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(`eql_query fields`, () => { + const getRuleAssetSavedObjects = () => [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 1, + type: 'eql', + query: 'query where true', + language: 'eql', + filters: [], + }), + ]; + + describe("when rule field doesn't have an update and has no custom value - scenario AAA", () => { + it('should not show in the upgrade/_review API response', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + await installPrebuiltRules(es, supertest); + + // Add a v2 rule asset to make the upgrade possible, do NOT update the related eql_query field, and create the new rule assets + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 2, + type: 'eql', + query: 'query where true', + language: 'eql', + filters: [], + }), + ]; + await createHistoricalPrebuiltRuleAssetSavedObjects(es, updatedRuleAssetSavedObjects); + + // Call the upgrade review prebuilt rules endpoint and check that there is 1 rule eligible for update but eql_query field is NOT returned + const reviewResponse = await reviewPrebuiltRulesToUpgrade(supertest); + const fieldDiffObject = reviewResponse.rules[0].diff.fields as AllFieldsDiff; + expect(fieldDiffObject.eql_query).toBeUndefined(); + + expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(1); // `version` is considered an updated field + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(0); + expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0); + expect(reviewResponse.stats.num_rules_with_conflicts).toBe(0); + expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(0); + }); + }); + + describe("when rule field doesn't have an update but has a custom value - scenario ABA", () => { + it('should show in the upgrade/_review API response', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + await installPrebuiltRules(es, supertest); + + // Customize an eql_query field on the installed rule + await updateRule(supertest, { + ...getPrebuiltRuleMock(), + rule_id: 'rule-1', + type: 'eql', + query: 'query where false', + language: 'eql', + filters: [], + } as RuleUpdateProps); + + // Add a v2 rule asset to make the upgrade possible, do NOT update the related eql_query field, and create the new rule assets + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 2, + type: 'eql', + query: 'query where true', + language: 'eql', + filters: [], + }), + ]; + await createHistoricalPrebuiltRuleAssetSavedObjects(es, updatedRuleAssetSavedObjects); + + // Call the upgrade review prebuilt rules endpoint and check that eql_query diff field is returned but field does not have an update + const reviewResponse = await reviewPrebuiltRulesToUpgrade(supertest); + const fieldDiffObject = reviewResponse.rules[0].diff.fields as AllFieldsDiff; + expect(fieldDiffObject.eql_query).toEqual({ + base_version: { + query: 'query where true', + language: 'eql', + filters: [], + }, + current_version: { + query: 'query where false', + language: 'eql', + filters: [], + }, + target_version: { + query: 'query where true', + language: 'eql', + filters: [], + }, + merged_version: { + query: 'query where false', + language: 'eql', + filters: [], + }, + diff_outcome: ThreeWayDiffOutcome.CustomizedValueNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Current, + conflict: ThreeWayDiffConflict.NONE, + has_update: false, + has_base_version: true, + }); + + expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(1); // `version` is considered an updated field + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(0); + expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0); + + expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); + expect(reviewResponse.stats.num_rules_with_conflicts).toBe(0); + expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(0); + }); + }); + + describe('when rule field has an update but does not have a custom value - scenario AAB', () => { + it('should show in the upgrade/_review API response', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + await installPrebuiltRules(es, supertest); + + // Add a v2 rule asset to make the upgrade possible, update an eql_query field, and create the new rule assets + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 2, + type: 'eql', + query: 'query where false', + language: 'eql', + filters: [], + }), + ]; + 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); + const fieldDiffObject = reviewResponse.rules[0].diff.fields as AllFieldsDiff; + expect(fieldDiffObject.eql_query).toEqual({ + base_version: { + query: 'query where true', + language: 'eql', + filters: [], + }, + current_version: { + query: 'query where true', + language: 'eql', + filters: [], + }, + target_version: { + query: 'query where false', + language: 'eql', + filters: [], + }, + merged_version: { + query: 'query where false', + language: 'eql', + filters: [], + }, + diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + has_update: true, + has_base_version: true, + }); + + expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(2); // `version` is considered an updated field + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(0); + expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0); + + expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); + expect(reviewResponse.stats.num_rules_with_conflicts).toBe(0); + expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(0); + }); + }); + + describe('when rule field has an update and a custom value that are the same - scenario ABB', () => { + it('should show in the upgrade/_review API response', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + await installPrebuiltRules(es, supertest); + + // Customize an eql_query field on the installed rule + await updateRule(supertest, { + ...getPrebuiltRuleMock(), + rule_id: 'rule-1', + type: 'eql', + query: 'query where false', + language: 'eql', + filters: [], + } as RuleUpdateProps); + + // Add a v2 rule asset to make the upgrade possible, update an eql_query field, and create the new rule assets + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 2, + type: 'eql', + query: 'query where false', + language: 'eql', + filters: [], + }), + ]; + await createHistoricalPrebuiltRuleAssetSavedObjects(es, updatedRuleAssetSavedObjects); + + // Call the upgrade review prebuilt rules endpoint and check that one rule is eligible for update and contains eql_query field + const reviewResponse = await reviewPrebuiltRulesToUpgrade(supertest); + const fieldDiffObject = reviewResponse.rules[0].diff.fields as AllFieldsDiff; + expect(fieldDiffObject.eql_query).toEqual({ + base_version: { + query: 'query where true', + language: 'eql', + filters: [], + }, + current_version: { + query: 'query where false', + language: 'eql', + filters: [], + }, + target_version: { + query: 'query where false', + language: 'eql', + filters: [], + }, + merged_version: { + query: 'query where false', + language: 'eql', + filters: [], + }, + diff_outcome: ThreeWayDiffOutcome.CustomizedValueSameUpdate, + merge_outcome: ThreeWayMergeOutcome.Current, + conflict: ThreeWayDiffConflict.NONE, + has_update: false, + has_base_version: true, + }); + expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(1); // `version` is considered an updated field + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(0); + expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0); + + expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); + expect(reviewResponse.stats.num_rules_with_conflicts).toBe(0); + expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(0); + }); + }); + + describe('when rule field has an update and a custom value that are different - scenario ABC', () => { + it('should show a non-solvable conflict in the upgrade/_review API response', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + await installPrebuiltRules(es, supertest); + + // Customize an eql_query field on the installed rule + await updateRule(supertest, { + ...getPrebuiltRuleMock(), + rule_id: 'rule-1', + type: 'eql', + query: 'query where true', + language: 'eql', + filters: [{ field: 'query' }], + } as RuleUpdateProps); + + // Add a v2 rule asset to make the upgrade possible, update an eql_query field, and create the new rule assets + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 2, + type: 'eql', + query: 'query where false', + language: 'eql', + filters: [], + }), + ]; + await createHistoricalPrebuiltRuleAssetSavedObjects(es, updatedRuleAssetSavedObjects); + + // Call the upgrade review prebuilt rules endpoint and check that one rule is eligible for update + // and eql_query field update has conflict + const reviewResponse = await reviewPrebuiltRulesToUpgrade(supertest); + const fieldDiffObject = reviewResponse.rules[0].diff.fields as AllFieldsDiff; + expect(fieldDiffObject.eql_query).toEqual({ + base_version: { + query: 'query where true', + language: 'eql', + filters: [], + }, + current_version: { + query: 'query where true', + language: 'eql', + filters: [{ field: 'query' }], + }, + target_version: { + query: 'query where false', + language: 'eql', + filters: [], + }, + merged_version: { + query: 'query where true', + language: 'eql', + filters: [{ field: 'query' }], + }, + diff_outcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Current, + conflict: ThreeWayDiffConflict.NON_SOLVABLE, + has_update: true, + has_base_version: true, + }); + + expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(2); // `version` is considered an updated field + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1); + expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(1); + + expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); + expect(reviewResponse.stats.num_rules_with_conflicts).toBe(1); + expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).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 - scenario -AA', () => { + 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); + + // Add a v2 rule asset to make the upgrade possible, but keep eql_query field unchanged + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 2, + type: 'eql', + query: 'query where true', + language: 'eql', + filters: [], + }), + ]; + await createPrebuiltRuleAssetSavedObjects(es, updatedRuleAssetSavedObjects); + + // Call the upgrade review prebuilt rules endpoint and check that one rule is eligible for update + // but does NOT contain eql_query field + const reviewResponse = await reviewPrebuiltRulesToUpgrade(supertest); + const fieldDiffObject = reviewResponse.rules[0].diff.fields as AllFieldsDiff; + expect(fieldDiffObject.eql_query).toBeUndefined(); + + expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(1); // `version` is considered an updated field + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1); // `version` is considered conflict + expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0); + + expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); + expect(reviewResponse.stats.num_rules_with_conflicts).toBe(1); + expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(0); + }); + }); + + describe('when rule field has an update and a custom value that are different - scenario -AB', () => { + 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 an eql_query field on the installed rule + await updateRule(supertest, { + ...getPrebuiltRuleMock(), + rule_id: 'rule-1', + type: 'eql', + query: 'query where false', + language: 'eql', + filters: [], + } as RuleUpdateProps); + + // Add a v2 rule asset to make the upgrade possible, update an eql_query field, and create the new rule assets + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 2, + type: 'eql', + query: 'new query where true', + language: 'eql', + filters: [], + }), + ]; + await createPrebuiltRuleAssetSavedObjects(es, updatedRuleAssetSavedObjects); + + // Call the upgrade review prebuilt rules endpoint and check that one rule is eligible for update + // and eql_query field update does not have a conflict + const reviewResponse = await reviewPrebuiltRulesToUpgrade(supertest); + const fieldDiffObject = reviewResponse.rules[0].diff.fields as AllFieldsDiff; + expect(fieldDiffObject.eql_query).toEqual({ + current_version: { + query: 'query where false', + language: 'eql', + filters: [], + }, + target_version: { + query: 'new query where true', + language: 'eql', + filters: [], + }, + merged_version: { + query: 'new query where true', + language: 'eql', + filters: [], + }, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.SOLVABLE, + has_update: true, + has_base_version: false, + }); + + expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(2); // version + query + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(2); // version + query + expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0); + + expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); + expect(reviewResponse.stats.num_rules_with_conflicts).toBe(1); + expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(0); + }); + }); + }); + }); + }); +}; 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.esql_query_fields.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.esql_query_fields.ts new file mode 100644 index 0000000000000..f77f59b16a3e1 --- /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.esql_query_fields.ts @@ -0,0 +1,434 @@ +/* + * 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 { + AllFieldsDiff, + RuleUpdateProps, + ThreeWayDiffConflict, + ThreeWayDiffOutcome, + ThreeWayMergeOutcome, +} from '@kbn/security-solution-plugin/common/api/detection_engine'; +import { getPrebuiltRuleMock } from '@kbn/security-solution-plugin/server/lib/detection_engine/prebuilt_rules/mocks'; +import { FtrProviderContext } from '../../../../../../ftr_provider_context'; +import { + deleteAllTimelines, + deleteAllPrebuiltRuleAssets, + createRuleAssetSavedObject, + installPrebuiltRules, + createPrebuiltRuleAssetSavedObjects, + reviewPrebuiltRulesToUpgrade, + createHistoricalPrebuiltRuleAssetSavedObjects, + updateRule, +} 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(`esql_query fields`, () => { + const getRuleAssetSavedObjects = () => [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 1, + type: 'esql', + query: 'FROM query WHERE true', + language: 'esql', + }), + ]; + + describe("when rule field doesn't have an update and has no custom value - scenario AAA", () => { + it('should not show in the upgrade/_review API response', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + await installPrebuiltRules(es, supertest); + + // Add a v2 rule asset to make the upgrade possible, do NOT update the related esql_query field, and create the new rule assets + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 2, + type: 'esql', + query: 'FROM query WHERE true', + language: 'esql', + }), + ]; + await createHistoricalPrebuiltRuleAssetSavedObjects(es, updatedRuleAssetSavedObjects); + + // Call the upgrade review prebuilt rules endpoint and check that there is 1 rule eligible for update but esql_query field is NOT returned + const reviewResponse = await reviewPrebuiltRulesToUpgrade(supertest); + const fieldDiffObject = reviewResponse.rules[0].diff.fields as AllFieldsDiff; + expect(fieldDiffObject.esql_query).toBeUndefined(); + + expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(1); // `version` is considered an updated field + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(0); + expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0); + expect(reviewResponse.stats.num_rules_with_conflicts).toBe(0); + expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(0); + }); + }); + + describe("when rule field doesn't have an update but has a custom value - scenario ABA", () => { + it('should show in the upgrade/_review API response', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + await installPrebuiltRules(es, supertest); + + // Customize an esql_query field on the installed rule + await updateRule(supertest, { + ...getPrebuiltRuleMock(), + rule_id: 'rule-1', + type: 'esql', + query: 'FROM query WHERE false', + language: 'esql', + } as RuleUpdateProps); + + // Add a v2 rule asset to make the upgrade possible, do NOT update the related esql_query field, and create the new rule assets + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 2, + type: 'esql', + query: 'FROM query WHERE true', + language: 'esql', + }), + ]; + await createHistoricalPrebuiltRuleAssetSavedObjects(es, updatedRuleAssetSavedObjects); + + // Call the upgrade review prebuilt rules endpoint and check that esql_query diff field is returned but field does not have an update + const reviewResponse = await reviewPrebuiltRulesToUpgrade(supertest); + const fieldDiffObject = reviewResponse.rules[0].diff.fields as AllFieldsDiff; + expect(fieldDiffObject.esql_query).toEqual({ + base_version: { + query: 'FROM query WHERE true', + language: 'esql', + }, + current_version: { + query: 'FROM query WHERE false', + language: 'esql', + }, + target_version: { + query: 'FROM query WHERE true', + language: 'esql', + }, + merged_version: { + query: 'FROM query WHERE false', + language: 'esql', + }, + diff_outcome: ThreeWayDiffOutcome.CustomizedValueNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Current, + conflict: ThreeWayDiffConflict.NONE, + has_update: false, + has_base_version: true, + }); + + expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(1); // `version` is considered an updated field + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(0); + expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0); + + expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); + expect(reviewResponse.stats.num_rules_with_conflicts).toBe(0); + expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(0); + }); + }); + + describe('when rule field has an update but does not have a custom value - scenario AAB', () => { + it('should show in the upgrade/_review API response', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + await installPrebuiltRules(es, supertest); + + // Add a v2 rule asset to make the upgrade possible, update an esql_query field, and create the new rule assets + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 2, + type: 'esql', + query: 'FROM query WHERE false', + language: 'esql', + }), + ]; + 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); + const fieldDiffObject = reviewResponse.rules[0].diff.fields as AllFieldsDiff; + expect(fieldDiffObject.esql_query).toEqual({ + base_version: { + query: 'FROM query WHERE true', + language: 'esql', + }, + current_version: { + query: 'FROM query WHERE true', + language: 'esql', + }, + target_version: { + query: 'FROM query WHERE false', + language: 'esql', + }, + merged_version: { + query: 'FROM query WHERE false', + language: 'esql', + }, + diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + has_update: true, + has_base_version: true, + }); + + expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(2); // `version` is considered an updated field + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(0); + expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0); + + expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); + expect(reviewResponse.stats.num_rules_with_conflicts).toBe(0); + expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(0); + }); + }); + + describe('when rule field has an update and a custom value that are the same - scenario ABB', () => { + it('should show in the upgrade/_review API response', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + await installPrebuiltRules(es, supertest); + + // Customize an esql_query field on the installed rule + await updateRule(supertest, { + ...getPrebuiltRuleMock(), + rule_id: 'rule-1', + type: 'esql', + query: 'FROM query WHERE false', + language: 'esql', + } as RuleUpdateProps); + + // Add a v2 rule asset to make the upgrade possible, update an esql_query field, and create the new rule assets + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 2, + type: 'esql', + query: 'FROM query WHERE false', + language: 'esql', + }), + ]; + await createHistoricalPrebuiltRuleAssetSavedObjects(es, updatedRuleAssetSavedObjects); + + // Call the upgrade review prebuilt rules endpoint and check that one rule is eligible for update and contains esql_query field + const reviewResponse = await reviewPrebuiltRulesToUpgrade(supertest); + const fieldDiffObject = reviewResponse.rules[0].diff.fields as AllFieldsDiff; + expect(fieldDiffObject.esql_query).toEqual({ + base_version: { + query: 'FROM query WHERE true', + language: 'esql', + }, + current_version: { + query: 'FROM query WHERE false', + language: 'esql', + }, + target_version: { + query: 'FROM query WHERE false', + language: 'esql', + }, + merged_version: { + query: 'FROM query WHERE false', + language: 'esql', + }, + diff_outcome: ThreeWayDiffOutcome.CustomizedValueSameUpdate, + merge_outcome: ThreeWayMergeOutcome.Current, + conflict: ThreeWayDiffConflict.NONE, + has_update: false, + has_base_version: true, + }); + expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(1); // `version` is considered an updated field + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(0); + expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0); + + expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); + expect(reviewResponse.stats.num_rules_with_conflicts).toBe(0); + expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(0); + }); + }); + + describe('when rule field has an update and a custom value that are different - scenario ABC', () => { + it('should show a non-solvable conflict in the upgrade/_review API response', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + await installPrebuiltRules(es, supertest); + + // Customize an esql_query field on the installed rule + await updateRule(supertest, { + ...getPrebuiltRuleMock(), + rule_id: 'rule-1', + type: 'esql', + query: 'FROM query WHERE false', + language: 'esql', + } as RuleUpdateProps); + + // Add a v2 rule asset to make the upgrade possible, update an esql_query field, and create the new rule assets + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 2, + type: 'esql', + query: 'FROM new query WHERE true', + language: 'esql', + }), + ]; + await createHistoricalPrebuiltRuleAssetSavedObjects(es, updatedRuleAssetSavedObjects); + + // Call the upgrade review prebuilt rules endpoint and check that one rule is eligible for update + // and esql_query field update has conflict + const reviewResponse = await reviewPrebuiltRulesToUpgrade(supertest); + const fieldDiffObject = reviewResponse.rules[0].diff.fields as AllFieldsDiff; + expect(fieldDiffObject.esql_query).toEqual({ + base_version: { + query: 'FROM query WHERE true', + language: 'esql', + }, + current_version: { + query: 'FROM query WHERE false', + language: 'esql', + }, + target_version: { + query: 'FROM new query WHERE true', + language: 'esql', + }, + merged_version: { + query: 'FROM query WHERE false', + language: 'esql', + }, + diff_outcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Current, + conflict: ThreeWayDiffConflict.NON_SOLVABLE, + has_update: true, + has_base_version: true, + }); + + expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(2); // `version` is considered an updated field + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1); + expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(1); + + expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); + expect(reviewResponse.stats.num_rules_with_conflicts).toBe(1); + expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).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 - scenario -AA', () => { + 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); + + // Add a v2 rule asset to make the upgrade possible, but keep esql_query field unchanged + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 2, + type: 'esql', + query: 'FROM query WHERE true', + language: 'esql', + }), + ]; + await createPrebuiltRuleAssetSavedObjects(es, updatedRuleAssetSavedObjects); + + // Call the upgrade review prebuilt rules endpoint and check that one rule is eligible for update + // but does NOT contain esql_query field + const reviewResponse = await reviewPrebuiltRulesToUpgrade(supertest); + const fieldDiffObject = reviewResponse.rules[0].diff.fields as AllFieldsDiff; + expect(fieldDiffObject.esql_query).toBeUndefined(); + + expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(1); + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1); // version is considered conflict + expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0); + + expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); + expect(reviewResponse.stats.num_rules_with_conflicts).toBe(1); + expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(0); + }); + }); + + describe('when rule field has an update and a custom value that are different - scenario -AB', () => { + 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 an esql_query field on the installed rule + await updateRule(supertest, { + ...getPrebuiltRuleMock(), + rule_id: 'rule-1', + type: 'esql', + query: 'FROM query WHERE false', + language: 'esql', + } as RuleUpdateProps); + + // Add a v2 rule asset to make the upgrade possible, update an esql_query field, and create the new rule assets + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 2, + type: 'esql', + query: 'FROM new query WHERE true', + language: 'esql', + }), + ]; + await createPrebuiltRuleAssetSavedObjects(es, updatedRuleAssetSavedObjects); + + // Call the upgrade review prebuilt rules endpoint and check that one rule is eligible for update + // and esql_query field update does not have a conflict + const reviewResponse = await reviewPrebuiltRulesToUpgrade(supertest); + const fieldDiffObject = reviewResponse.rules[0].diff.fields as AllFieldsDiff; + expect(fieldDiffObject.esql_query).toEqual({ + current_version: { + query: 'FROM query WHERE false', + language: 'esql', + }, + target_version: { + query: 'FROM new query WHERE true', + language: 'esql', + }, + merged_version: { + query: 'FROM new query WHERE true', + language: 'esql', + }, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.SOLVABLE, + has_update: true, + has_base_version: false, + }); + + expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(2); + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(2); // version + query + expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0); + + expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); + expect(reviewResponse.stats.num_rules_with_conflicts).toBe(1); + expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(0); + }); + }); + }); + }); + }); +}; 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.kql_query_fields.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.kql_query_fields.ts new file mode 100644 index 0000000000000..3de835ed42772 --- /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.kql_query_fields.ts @@ -0,0 +1,1128 @@ +/* + * 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 { + AllFieldsDiff, + KqlQueryType, + RuleUpdateProps, + ThreeWayDiffConflict, + ThreeWayDiffOutcome, + ThreeWayMergeOutcome, +} from '@kbn/security-solution-plugin/common/api/detection_engine'; +import { + getPrebuiltRuleMock, + getPrebuiltThreatMatchRuleMock, +} from '@kbn/security-solution-plugin/server/lib/detection_engine/prebuilt_rules/mocks'; +import { PrebuiltRuleAsset } from '@kbn/security-solution-plugin/server/lib/detection_engine/prebuilt_rules'; +import { FtrProviderContext } from '../../../../../../ftr_provider_context'; +import { + deleteAllTimelines, + deleteAllPrebuiltRuleAssets, + createRuleAssetSavedObject, + installPrebuiltRules, + createPrebuiltRuleAssetSavedObjects, + reviewPrebuiltRulesToUpgrade, + createHistoricalPrebuiltRuleAssetSavedObjects, + updateRule, + patchRule, +} 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(`kql_query fields`, () => { + const getQueryRuleAssetSavedObjects = () => [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 1, + type: 'query', + query: 'query string = true', + language: 'kuery', + filters: [], + }), + ]; + + const getSavedQueryRuleAssetSavedObjects = () => [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 1, + type: 'saved_query', + saved_id: 'saved-query-id', + }), + ]; + + describe("when rule field doesn't have an update and has no custom value - scenario AAA", () => { + describe('when all versions are inline query types', () => { + it('should not show in the upgrade/_review API response', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects( + es, + getQueryRuleAssetSavedObjects() + ); + await installPrebuiltRules(es, supertest); + + // Add a v2 rule asset to make the upgrade possible, do NOT update the related kql_query field, and create the new rule assets + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 2, + type: 'query', + query: 'query string = true', + language: 'kuery', + filters: [], + }), + ]; + await createHistoricalPrebuiltRuleAssetSavedObjects(es, updatedRuleAssetSavedObjects); + + // Call the upgrade review prebuilt rules endpoint and check that there is 1 rule eligible for update but kql_query field is NOT returned + const reviewResponse = await reviewPrebuiltRulesToUpgrade(supertest); + const fieldDiffObject = reviewResponse.rules[0].diff.fields as AllFieldsDiff; + expect(fieldDiffObject.kql_query).toBeUndefined(); + + expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(1); // `version` is considered an updated field + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(0); + expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0); + expect(reviewResponse.stats.num_rules_with_conflicts).toBe(0); + expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(0); + }); + }); + + describe('when all versions are saved query types', () => { + it('should not show in the upgrade/_review API response', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects( + es, + getSavedQueryRuleAssetSavedObjects() + ); + await installPrebuiltRules(es, supertest); + + // Add a v2 rule asset to make the upgrade possible, do NOT update the related kql_query field, and create the new rule assets + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 2, + type: 'saved_query', + saved_id: 'saved-query-id', + }), + ]; + await createHistoricalPrebuiltRuleAssetSavedObjects(es, updatedRuleAssetSavedObjects); + + // Call the upgrade review prebuilt rules endpoint and check that there is 1 rule eligible for update but kql_query field is NOT returned + const reviewResponse = await reviewPrebuiltRulesToUpgrade(supertest); + const fieldDiffObject = reviewResponse.rules[0].diff.fields as AllFieldsDiff; + expect(fieldDiffObject.kql_query).toBeUndefined(); + + expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(1); // `version` is considered an updated field + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(0); + expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0); + expect(reviewResponse.stats.num_rules_with_conflicts).toBe(0); + expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(0); + }); + }); + }); + + describe("when rule field doesn't have an update but has a custom value - scenario ABA", () => { + describe('when current version is inline query type', () => { + it('should show in the upgrade/_review API response', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects( + es, + getSavedQueryRuleAssetSavedObjects() + ); + await installPrebuiltRules(es, supertest); + + // Customize a kql_query field on the installed rule + await updateRule(supertest, { + ...getPrebuiltRuleMock(), + rule_id: 'rule-1', + type: 'query', + query: 'query string = true', + language: 'kuery', + filters: [], + saved_id: undefined, + } as RuleUpdateProps); + + // Add a v2 rule asset to make the upgrade possible, do NOT update the related kql_query field, and create the new rule assets + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 2, + type: 'saved_query', + saved_id: 'saved-query-id', + }), + ]; + await createHistoricalPrebuiltRuleAssetSavedObjects(es, updatedRuleAssetSavedObjects); + + // Call the upgrade review prebuilt rules endpoint and check that kql_query diff field is returned but field does not have an update + const reviewResponse = await reviewPrebuiltRulesToUpgrade(supertest); + const fieldDiffObject = reviewResponse.rules[0].diff.fields as AllFieldsDiff; + expect(fieldDiffObject.kql_query).toEqual({ + base_version: { + type: KqlQueryType.saved_query, + saved_query_id: 'saved-query-id', + }, + current_version: { + type: KqlQueryType.inline_query, + query: 'query string = true', + language: 'kuery', + filters: [], + }, + target_version: { + type: KqlQueryType.saved_query, + saved_query_id: 'saved-query-id', + }, + merged_version: { + type: KqlQueryType.inline_query, + query: 'query string = true', + language: 'kuery', + filters: [], + }, + diff_outcome: ThreeWayDiffOutcome.CustomizedValueNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Current, + conflict: ThreeWayDiffConflict.NONE, + has_update: false, + has_base_version: true, + }); + + expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(1); // `version` is considered an updated field + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(0); + expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0); + + expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); + expect(reviewResponse.stats.num_rules_with_conflicts).toBe(0); + expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(0); + }); + }); + + describe('when current version is saved query type', () => { + it('should show in the upgrade/_review API response', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects( + es, + getQueryRuleAssetSavedObjects() + ); + await installPrebuiltRules(es, supertest); + + // Customize a kql_query field on the installed rule + await updateRule(supertest, { + ...getPrebuiltRuleMock(), + rule_id: 'rule-1', + type: 'saved_query', + query: undefined, + language: undefined, + filters: undefined, + saved_id: 'saved-query-id', + } as RuleUpdateProps); + + // Add a v2 rule asset to make the upgrade possible, do NOT update the related kql_query field, and create the new rule assets + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 2, + type: 'query', + query: 'query string = true', + language: 'kuery', + filters: [], + }), + ]; + await createHistoricalPrebuiltRuleAssetSavedObjects(es, updatedRuleAssetSavedObjects); + + // Call the upgrade review prebuilt rules endpoint and check that kql_query diff field is returned but field does not have an update + const reviewResponse = await reviewPrebuiltRulesToUpgrade(supertest); + const fieldDiffObject = reviewResponse.rules[0].diff.fields as AllFieldsDiff; + expect(fieldDiffObject.kql_query).toEqual({ + base_version: { + type: KqlQueryType.inline_query, + query: 'query string = true', + language: 'kuery', + filters: [], + }, + current_version: { + type: KqlQueryType.saved_query, + saved_query_id: 'saved-query-id', + }, + target_version: { + type: KqlQueryType.inline_query, + query: 'query string = true', + language: 'kuery', + filters: [], + }, + merged_version: { + type: KqlQueryType.saved_query, + saved_query_id: 'saved-query-id', + }, + diff_outcome: ThreeWayDiffOutcome.CustomizedValueNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Current, + conflict: ThreeWayDiffConflict.NONE, + has_update: false, + has_base_version: true, + }); + + expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(1); // `version` is considered an updated field + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(0); + expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0); + + expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); + expect(reviewResponse.stats.num_rules_with_conflicts).toBe(0); + expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(0); + }); + }); + }); + + describe('when rule field has an update but does not have a custom value - scenario AAB', () => { + describe('when all versions are inline query type', () => { + it('should show in the upgrade/_review API response', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects( + es, + getQueryRuleAssetSavedObjects() + ); + await installPrebuiltRules(es, supertest); + + // Add a v2 rule asset to make the upgrade possible, update a kql_query field, and create the new rule assets + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 2, + type: 'query', + query: 'query string = false', + }), + ]; + 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); + const fieldDiffObject = reviewResponse.rules[0].diff.fields as AllFieldsDiff; + expect(fieldDiffObject.kql_query).toEqual({ + base_version: { + type: KqlQueryType.inline_query, + query: 'query string = true', + language: 'kuery', + filters: [], + }, + current_version: { + type: KqlQueryType.inline_query, + query: 'query string = true', + language: 'kuery', + filters: [], + }, + target_version: { + type: KqlQueryType.inline_query, + query: 'query string = false', + language: 'kuery', + filters: [], + }, + merged_version: { + type: KqlQueryType.inline_query, + query: 'query string = false', + language: 'kuery', + filters: [], + }, + diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + has_update: true, + has_base_version: true, + }); + + expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(2); // `version` is considered an updated field + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(0); + expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0); + + expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); + expect(reviewResponse.stats.num_rules_with_conflicts).toBe(0); + expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(0); + }); + }); + + describe('when all versions are saved query type', () => { + it('should show in the upgrade/_review API response', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects( + es, + getSavedQueryRuleAssetSavedObjects() + ); + await installPrebuiltRules(es, supertest); + + // Add a v2 rule asset to make the upgrade possible, update a kql_query field, and create the new rule assets + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 2, + type: 'saved_query', + saved_id: 'new-saved-query-id', + }), + ]; + 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); + const fieldDiffObject = reviewResponse.rules[0].diff.fields as AllFieldsDiff; + expect(fieldDiffObject.kql_query).toEqual({ + base_version: { + type: KqlQueryType.saved_query, + saved_query_id: 'saved-query-id', + }, + current_version: { + type: KqlQueryType.saved_query, + saved_query_id: 'saved-query-id', + }, + target_version: { + type: KqlQueryType.saved_query, + saved_query_id: 'new-saved-query-id', + }, + merged_version: { + type: KqlQueryType.saved_query, + saved_query_id: 'new-saved-query-id', + }, + diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + has_update: true, + has_base_version: true, + }); + + expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(2); // `version` is considered an updated field + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(0); + expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0); + + expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); + expect(reviewResponse.stats.num_rules_with_conflicts).toBe(0); + expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(0); + }); + }); + }); + + describe('when rule field has an update and a custom value that are the same - scenario ABB', () => { + describe('when all versions are inline query type', () => { + it('should show in the upgrade/_review API response', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects( + es, + getQueryRuleAssetSavedObjects() + ); + await installPrebuiltRules(es, supertest); + + // Customize a kql_query field on the installed rule + await patchRule(supertest, log, { + rule_id: 'rule-1', + query: 'query string = false', + }); + + // Add a v2 rule asset to make the upgrade possible, update a kql_query field, and create the new rule assets + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 2, + type: 'query', + query: 'query string = false', + language: 'kuery', + filters: [], + }), + ]; + await createHistoricalPrebuiltRuleAssetSavedObjects(es, updatedRuleAssetSavedObjects); + + // Call the upgrade review prebuilt rules endpoint and check that one rule is eligible for update and contains kql_query field + const reviewResponse = await reviewPrebuiltRulesToUpgrade(supertest); + const fieldDiffObject = reviewResponse.rules[0].diff.fields as AllFieldsDiff; + expect(fieldDiffObject.kql_query).toEqual({ + base_version: { + type: KqlQueryType.inline_query, + query: 'query string = true', + language: 'kuery', + filters: [], + }, + current_version: { + type: KqlQueryType.inline_query, + query: 'query string = false', + language: 'kuery', + filters: [], + }, + target_version: { + type: KqlQueryType.inline_query, + query: 'query string = false', + language: 'kuery', + filters: [], + }, + merged_version: { + type: KqlQueryType.inline_query, + query: 'query string = false', + language: 'kuery', + filters: [], + }, + diff_outcome: ThreeWayDiffOutcome.CustomizedValueSameUpdate, + merge_outcome: ThreeWayMergeOutcome.Current, + conflict: ThreeWayDiffConflict.NONE, + has_update: false, + has_base_version: true, + }); + expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(1); // `version` is considered an updated field + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(0); + expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0); + + expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); + expect(reviewResponse.stats.num_rules_with_conflicts).toBe(0); + expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(0); + }); + }); + + describe('when all versions are saved query types', () => { + it('should show in the upgrade/_review API response', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects( + es, + getSavedQueryRuleAssetSavedObjects() + ); + await installPrebuiltRules(es, supertest); + + // Customize a kql_query field on the installed rule + await updateRule(supertest, { + ...getPrebuiltRuleMock(), + rule_id: 'rule-1', + type: 'saved_query', + saved_id: 'new-saved-query-id', + } as RuleUpdateProps); + + // Add a v2 rule asset to make the upgrade possible, update a kql_query field, and create the new rule assets + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 2, + type: 'saved_query', + saved_id: 'new-saved-query-id', + }), + ]; + await createHistoricalPrebuiltRuleAssetSavedObjects(es, updatedRuleAssetSavedObjects); + + // Call the upgrade review prebuilt rules endpoint and check that one rule is eligible for update and contains kql_query field + const reviewResponse = await reviewPrebuiltRulesToUpgrade(supertest); + const fieldDiffObject = reviewResponse.rules[0].diff.fields as AllFieldsDiff; + expect(fieldDiffObject.kql_query).toEqual({ + base_version: { + type: KqlQueryType.saved_query, + saved_query_id: 'saved-query-id', + }, + current_version: { + type: KqlQueryType.saved_query, + saved_query_id: 'new-saved-query-id', + }, + target_version: { + type: KqlQueryType.saved_query, + saved_query_id: 'new-saved-query-id', + }, + merged_version: { + type: KqlQueryType.saved_query, + saved_query_id: 'new-saved-query-id', + }, + diff_outcome: ThreeWayDiffOutcome.CustomizedValueSameUpdate, + merge_outcome: ThreeWayMergeOutcome.Current, + conflict: ThreeWayDiffConflict.NONE, + has_update: false, + has_base_version: true, + }); + expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(1); // `version` is considered an updated field + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(0); + expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0); + + expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); + expect(reviewResponse.stats.num_rules_with_conflicts).toBe(0); + expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(0); + }); + }); + }); + + describe('when rule field has an update and a custom value that are different - scenario ABC', () => { + describe('when current version is different type than base and target', () => { + it('should show a non-solvable conflict in the upgrade/_review API response', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects( + es, + getQueryRuleAssetSavedObjects() + ); + await installPrebuiltRules(es, supertest); + + // Customize a kql_query field on the installed rule + await updateRule(supertest, { + ...getPrebuiltRuleMock(), + rule_id: 'rule-1', + type: 'saved_query', + query: undefined, + language: undefined, + filters: undefined, + saved_id: 'saved-query-id', + } as RuleUpdateProps); + + // Add a v2 rule asset to make the upgrade possible, update a kql_query field, and create the new rule assets + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 2, + type: 'query', + query: 'query string = false', + language: 'kuery', + filters: [], + }), + ]; + await createHistoricalPrebuiltRuleAssetSavedObjects(es, updatedRuleAssetSavedObjects); + + // Call the upgrade review prebuilt rules endpoint and check that one rule is eligible for update + // and kql_query field update has conflict + const reviewResponse = await reviewPrebuiltRulesToUpgrade(supertest); + const fieldDiffObject = reviewResponse.rules[0].diff.fields as AllFieldsDiff; + expect(fieldDiffObject.kql_query).toEqual({ + base_version: { + type: KqlQueryType.inline_query, + query: 'query string = true', + language: 'kuery', + filters: [], + }, + current_version: { + type: KqlQueryType.saved_query, + saved_query_id: 'saved-query-id', + }, + target_version: { + type: KqlQueryType.inline_query, + query: 'query string = false', + language: 'kuery', + filters: [], + }, + merged_version: { + type: KqlQueryType.saved_query, + saved_query_id: 'saved-query-id', + }, + diff_outcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Current, + conflict: ThreeWayDiffConflict.NON_SOLVABLE, + has_update: true, + has_base_version: true, + }); + + expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(2); // `version` is considered an updated field + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1); + expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(1); + + expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); + expect(reviewResponse.stats.num_rules_with_conflicts).toBe(1); + expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(1); + }); + }); + + describe('when all versions are inline query type', () => { + it('should show a non-solvable conflict in the upgrade/_review API response', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects( + es, + getQueryRuleAssetSavedObjects() + ); + await installPrebuiltRules(es, supertest); + + // Customize a kql_query on the installed rule + await patchRule(supertest, log, { + rule_id: 'rule-1', + query: 'query string = false', + }); + + // Add a v2 rule asset to make the upgrade possible, update a kql_query field, and create the new rule assets + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 2, + type: 'query', + query: 'query string = true', + language: 'kuery', + filters: [{ field: 'query' }], + }), + ]; + await createHistoricalPrebuiltRuleAssetSavedObjects(es, updatedRuleAssetSavedObjects); + + // Call the upgrade review prebuilt rules endpoint and check that one rule is eligible for update + // and kql_query field update has conflict + const reviewResponse = await reviewPrebuiltRulesToUpgrade(supertest); + const fieldDiffObject = reviewResponse.rules[0].diff.fields as AllFieldsDiff; + expect(fieldDiffObject.kql_query).toEqual({ + base_version: { + type: KqlQueryType.inline_query, + query: 'query string = true', + language: 'kuery', + filters: [], + }, + current_version: { + type: KqlQueryType.inline_query, + query: 'query string = false', + language: 'kuery', + filters: [], + }, + target_version: { + type: KqlQueryType.inline_query, + query: 'query string = true', + language: 'kuery', + filters: [{ field: 'query' }], + }, + merged_version: { + type: KqlQueryType.inline_query, + query: 'query string = false', + language: 'kuery', + filters: [], + }, + diff_outcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Current, + conflict: ThreeWayDiffConflict.NON_SOLVABLE, + has_update: true, + has_base_version: true, + }); + + expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(2); // `version` is considered an updated field + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1); + expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(1); + + expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); + expect(reviewResponse.stats.num_rules_with_conflicts).toBe(1); + expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(1); + }); + }); + + describe('when all versions are saved query type', () => { + it('should show a non-solvable conflict in the upgrade/_review API response', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects( + es, + getSavedQueryRuleAssetSavedObjects() + ); + await installPrebuiltRules(es, supertest); + + // Customize a kql_query field on the installed rule + await updateRule(supertest, { + ...getPrebuiltRuleMock(), + rule_id: 'rule-1', + type: 'saved_query', + saved_id: 'new-saved-query-id', + } as RuleUpdateProps); + + // Add a v2 rule asset to make the upgrade possible, update a kql_query field, and create the new rule assets + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 2, + type: 'saved_query', + saved_id: 'even-newer-saved-query-id', + }), + ]; + await createHistoricalPrebuiltRuleAssetSavedObjects(es, updatedRuleAssetSavedObjects); + + // Call the upgrade review prebuilt rules endpoint and check that one rule is eligible for update + // and kql_query field update has conflict + const reviewResponse = await reviewPrebuiltRulesToUpgrade(supertest); + const fieldDiffObject = reviewResponse.rules[0].diff.fields as AllFieldsDiff; + expect(fieldDiffObject.kql_query).toEqual({ + base_version: { + type: KqlQueryType.saved_query, + saved_query_id: 'saved-query-id', + }, + current_version: { + type: KqlQueryType.saved_query, + saved_query_id: 'new-saved-query-id', + }, + target_version: { + type: KqlQueryType.saved_query, + saved_query_id: 'even-newer-saved-query-id', + }, + merged_version: { + type: KqlQueryType.saved_query, + saved_query_id: 'new-saved-query-id', + }, + diff_outcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Current, + conflict: ThreeWayDiffConflict.NON_SOLVABLE, + has_update: true, + has_base_version: true, + }); + + expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(2); // `version` is considered an updated field + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1); + expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(1); + + expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); + expect(reviewResponse.stats.num_rules_with_conflicts).toBe(1); + expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(1); + }); + }); + + describe('when rule type is threat match', () => { + it('should show a non-solvable conflict in the upgrade/_review API response', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects(es, [ + createRuleAssetSavedObject({ + ...getPrebuiltThreatMatchRuleMock(), + threat_filters: [], + } as PrebuiltRuleAsset), + ]); + await installPrebuiltRules(es, supertest); + + // Customize a threat_query on the installed rule + await updateRule(supertest, { + ...getPrebuiltThreatMatchRuleMock(), + rule_id: 'rule-1', + threat_query: '*', + threat_filters: [], + } as RuleUpdateProps); + + // Add a v2 rule asset to make the upgrade possible, update a threat_query field, and create the new rule assets + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + ...getPrebuiltThreatMatchRuleMock(), + threat_query: `*:'new query'`, + threat_filters: [], + version: 2, + } as PrebuiltRuleAsset), + ]; + await createHistoricalPrebuiltRuleAssetSavedObjects(es, updatedRuleAssetSavedObjects); + + // Call the upgrade review prebuilt rules endpoint and check that one rule is eligible for update + // and threat_query field update has conflict + const reviewResponse = await reviewPrebuiltRulesToUpgrade(supertest); + const fieldDiffObject = reviewResponse.rules[0].diff.fields as AllFieldsDiff; + expect(fieldDiffObject.threat_query).toEqual({ + base_version: { + type: KqlQueryType.inline_query, + query: '*:*', + language: 'kuery', + filters: [], + }, + current_version: { + type: KqlQueryType.inline_query, + query: '*', + language: 'kuery', + filters: [], + }, + target_version: { + type: KqlQueryType.inline_query, + query: `*:'new query'`, + language: 'kuery', + filters: [], + }, + merged_version: { + type: KqlQueryType.inline_query, + query: '*', + language: 'kuery', + filters: [], + }, + diff_outcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Current, + conflict: ThreeWayDiffConflict.NON_SOLVABLE, + has_update: true, + has_base_version: true, + }); + + expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(2); // `version` is considered an updated field + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1); + expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(1); + + expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); + expect(reviewResponse.stats.num_rules_with_conflicts).toBe(1); + expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(1); + }); + }); + + describe('when rule type is threshold', () => { + it('should show a non-solvable conflict in the upgrade/_review API response', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects(es, [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 1, + type: 'threshold', + query: 'query string = true', + threshold: { + field: 'some.field', + value: 4, + }, + }), + ]); + await installPrebuiltRules(es, supertest); + + // Customize a kql_query field on the installed rule + await updateRule(supertest, { + ...getPrebuiltRuleMock(), + rule_id: 'rule-1', + type: 'threshold', + query: 'query string = false', + threshold: { + field: 'some.field', + value: 4, + }, + } as RuleUpdateProps); + + // Add a v2 rule asset to make the upgrade possible, update a kql_query field, and create the new rule assets + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 2, + type: 'threshold', + query: 'new query string = true', + threshold: { + field: 'some.field', + value: 4, + }, + }), + ]; + await createHistoricalPrebuiltRuleAssetSavedObjects(es, updatedRuleAssetSavedObjects); + + // Call the upgrade review prebuilt rules endpoint and check that one rule is eligible for update + // and kql_query field update has conflict + const reviewResponse = await reviewPrebuiltRulesToUpgrade(supertest); + const fieldDiffObject = reviewResponse.rules[0].diff.fields as AllFieldsDiff; + expect(fieldDiffObject.kql_query).toEqual({ + base_version: { + type: KqlQueryType.inline_query, + query: 'query string = true', + language: 'kuery', + filters: [], + }, + current_version: { + type: KqlQueryType.inline_query, + query: 'query string = false', + language: 'kuery', + filters: [], + }, + target_version: { + type: KqlQueryType.inline_query, + query: 'new query string = true', + language: 'kuery', + filters: [], + }, + merged_version: { + type: KqlQueryType.inline_query, + query: 'query string = false', + language: 'kuery', + filters: [], + }, + diff_outcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Current, + conflict: ThreeWayDiffConflict.NON_SOLVABLE, + has_update: true, + has_base_version: true, + }); + + expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(2); // `version` is considered an updated field + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1); + expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(1); + + expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); + expect(reviewResponse.stats.num_rules_with_conflicts).toBe(1); + expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(1); + }); + }); + + describe('when rule type is new_terms', () => { + it('should show a non-solvable conflict in the upgrade/_review API response', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects(es, [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 1, + type: 'new_terms', + query: 'query string = true', + new_terms_fields: ['user.name'], + history_window_start: 'now-7d', + }), + ]); + await installPrebuiltRules(es, supertest); + + // Customize a kql_query field on the installed rule + await updateRule(supertest, { + ...getPrebuiltRuleMock(), + rule_id: 'rule-1', + type: 'new_terms', + query: 'query string = false', + new_terms_fields: ['user.name'], + history_window_start: 'now-7d', + } as RuleUpdateProps); + + // Add a v2 rule asset to make the upgrade possible, update a kql_query field, and create the new rule assets + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 2, + type: 'new_terms', + query: 'new query string = true', + new_terms_fields: ['user.name'], + history_window_start: 'now-7d', + }), + ]; + await createHistoricalPrebuiltRuleAssetSavedObjects(es, updatedRuleAssetSavedObjects); + + // Call the upgrade review prebuilt rules endpoint and check that one rule is eligible for update + // and kql_query field update has conflict + const reviewResponse = await reviewPrebuiltRulesToUpgrade(supertest); + const fieldDiffObject = reviewResponse.rules[0].diff.fields as AllFieldsDiff; + expect(fieldDiffObject.kql_query).toEqual({ + base_version: { + type: KqlQueryType.inline_query, + query: 'query string = true', + language: 'kuery', + filters: [], + }, + current_version: { + type: KqlQueryType.inline_query, + query: 'query string = false', + language: 'kuery', + filters: [], + }, + target_version: { + type: KqlQueryType.inline_query, + query: 'new query string = true', + language: 'kuery', + filters: [], + }, + merged_version: { + type: KqlQueryType.inline_query, + query: 'query string = false', + language: 'kuery', + filters: [], + }, + diff_outcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Current, + conflict: ThreeWayDiffConflict.NON_SOLVABLE, + has_update: true, + has_base_version: true, + }); + + expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(2); // `version` is considered an updated field + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1); + expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(1); + + expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); + expect(reviewResponse.stats.num_rules_with_conflicts).toBe(1); + expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).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 - scenario -AA', () => { + it('should not show in the upgrade/_review API response', async () => { + // Install base prebuilt detection rule + await createPrebuiltRuleAssetSavedObjects(es, getQueryRuleAssetSavedObjects()); + await installPrebuiltRules(es, supertest); + + // Clear previous rule assets + await deleteAllPrebuiltRuleAssets(es, log); + + // Add a v2 rule asset to make the upgrade possible, but keep kql_query field unchanged + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 2, + type: 'query', + query: 'query string = true', + language: 'kuery', + filters: [], + }), + ]; + await createPrebuiltRuleAssetSavedObjects(es, updatedRuleAssetSavedObjects); + + // Call the upgrade review prebuilt rules endpoint and check that one rule is eligible for update + // but does NOT contain kql_query field + const reviewResponse = await reviewPrebuiltRulesToUpgrade(supertest); + const fieldDiffObject = reviewResponse.rules[0].diff.fields as AllFieldsDiff; + expect(fieldDiffObject.kql_query).toBeUndefined(); + + expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(1); // `version` is considered an updated field + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(1); // `version` is considered conflict + expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0); + + expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); + expect(reviewResponse.stats.num_rules_with_conflicts).toBe(1); + expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(0); + }); + }); + + describe('when rule field has an update and a custom value that are different - scenario -AB', () => { + it('should show in the upgrade/_review API response', async () => { + // Install base prebuilt detection rule + await createPrebuiltRuleAssetSavedObjects(es, getQueryRuleAssetSavedObjects()); + await installPrebuiltRules(es, supertest); + + // Clear previous rule assets + await deleteAllPrebuiltRuleAssets(es, log); + + // Customize a kql_query field on the installed rule + await updateRule(supertest, { + ...getPrebuiltRuleMock(), + rule_id: 'rule-1', + type: 'query', + query: 'query string = false', + language: 'kuery', + filters: [], + } as RuleUpdateProps); + + // Add a v2 rule asset to make the upgrade possible, update a kql_query field, and create the new rule assets + const updatedRuleAssetSavedObjects = [ + createRuleAssetSavedObject({ + rule_id: 'rule-1', + version: 2, + type: 'query', + query: 'new query string = true', + language: 'kuery', + filters: [], + }), + ]; + await createPrebuiltRuleAssetSavedObjects(es, updatedRuleAssetSavedObjects); + + // Call the upgrade review prebuilt rules endpoint and check that one rule is eligible for update + // and kql_query field update does not have a conflict + const reviewResponse = await reviewPrebuiltRulesToUpgrade(supertest); + const fieldDiffObject = reviewResponse.rules[0].diff.fields as AllFieldsDiff; + expect(fieldDiffObject.kql_query).toEqual({ + current_version: { + type: KqlQueryType.inline_query, + query: 'query string = false', + language: 'kuery', + filters: [], + }, + target_version: { + type: KqlQueryType.inline_query, + query: 'new query string = true', + language: 'kuery', + filters: [], + }, + merged_version: { + type: KqlQueryType.inline_query, + query: 'new query string = true', + language: 'kuery', + filters: [], + }, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.SOLVABLE, + has_update: true, + has_base_version: false, + }); + + expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(2); + expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(2); // version + query + expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0); + + expect(reviewResponse.stats.num_rules_to_upgrade_total).toBe(1); + expect(reviewResponse.stats.num_rules_with_conflicts).toBe(1); + expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(0); + }); + }); + }); + }); + }); +};