diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action.ts index d14d82edbba51..bb552d7f7a828 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action.ts @@ -25,6 +25,7 @@ import { getCreateExceptionListItemMinimalSchemaMock } from '@kbn/lists-plugin/c import { WebhookAuthType } from '@kbn/stack-connectors-plugin/common/webhook/constants'; import { binaryToString, + checkInvestigationFieldSoValue, createLegacyRuleAction, createRule, createSignalsIndex, @@ -45,6 +46,7 @@ import { getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray, deleteAllExceptions, } from '../../utils'; +import { createAlertsIndex } from '../../../security_solution_api_integration/test_suites/detections_response/utils/alerts/create_alerts_index'; import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export @@ -3008,20 +3010,23 @@ export default ({ getService }: FtrProviderContext): void => { describe('legacy investigation fields', () => { let ruleWithLegacyInvestigationField: Rule; let ruleWithLegacyInvestigationFieldEmptyArray: Rule; + let ruleWithIntendedInvestigationField: RuleResponse; beforeEach(async () => { await deleteAllAlerts(supertest, log, es); await deleteAllRules(supertest, log); - await createSignalsIndex(supertest, log); + await createAlertsIndex(supertest, log); ruleWithLegacyInvestigationField = await createRuleThroughAlertingEndpoint( supertest, getRuleSavedObjectWithLegacyInvestigationFields() ); + ruleWithLegacyInvestigationFieldEmptyArray = await createRuleThroughAlertingEndpoint( supertest, getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray() ); - await createRule(supertest, log, { + + ruleWithIntendedInvestigationField = await createRule(supertest, log, { ...getSimpleRule('rule-with-investigation-field'), name: 'Test investigation fields object', investigation_fields: { field_names: ['host.name'] }, @@ -3041,23 +3046,26 @@ export default ({ getService }: FtrProviderContext): void => { .parse(binaryToString); const [rule1, rule2, rule3, exportDetailsJson] = body.toString().split(/\n/); + const exportedRules = [rule1, rule2, rule3].map((rule) => JSON.parse(rule)); - const ruleToCompareWithLegacyInvestigationField = removeServerGeneratedProperties( - JSON.parse(rule1) + const exportedRuleWithLegacyInvestigationField = exportedRules.find( + (rule) => rule.id === ruleWithLegacyInvestigationField.id ); - expect(ruleToCompareWithLegacyInvestigationField.investigation_fields).to.eql({ + expect(exportedRuleWithLegacyInvestigationField.investigation_fields).to.eql({ field_names: ['client.address', 'agent.name'], }); - const ruleToCompareWithLegacyInvestigationFieldEmptyArray = removeServerGeneratedProperties( - JSON.parse(rule2) + const exportedRuleWithLegacyInvestigationFieldEmptyArray = exportedRules.find( + (rule) => rule.id === ruleWithLegacyInvestigationFieldEmptyArray.id ); - expect(ruleToCompareWithLegacyInvestigationFieldEmptyArray.investigation_fields).to.eql( + expect(exportedRuleWithLegacyInvestigationFieldEmptyArray.investigation_fields).to.eql( undefined ); - const ruleWithInvestigationField = removeServerGeneratedProperties(JSON.parse(rule3)); - expect(ruleWithInvestigationField.investigation_fields).to.eql({ + const exportedRuleWithInvestigationField = exportedRules.find( + (rule) => rule.id === ruleWithIntendedInvestigationField.id + ); + expect(exportedRuleWithInvestigationField.investigation_fields).to.eql({ field_names: ['host.name'], }); @@ -3066,12 +3074,14 @@ export default ({ getService }: FtrProviderContext): void => { * the SO itself is migrated to the inteded object type, or if the transformation is * happening just on the response. In this case, change should not include a migration on SO. */ - const { - hits: { - hits: [{ _source: ruleSO }], - }, - } = await getRuleSOById(es, JSON.parse(rule1).id); - expect(ruleSO?.alert?.params?.investigationFields).to.eql(['client.address', 'agent.name']); + const isInvestigationFieldMigratedInSo = await checkInvestigationFieldSoValue( + undefined, + { field_names: ['client.address', 'agent.name'] }, + es, + JSON.parse(rule1).id + ); + + expect(isInvestigationFieldMigratedInSo).to.eql(false); const exportDetails = JSON.parse(exportDetailsJson); expect(exportDetails).to.eql({ @@ -3156,7 +3166,6 @@ export default ({ getService }: FtrProviderContext): void => { (returnedRule: RuleResponse) => returnedRule.rule_id === 'rule-with-investigation-field' ); expect(ruleWithIntendedType.investigation_fields).to.eql({ field_names: ['host.name'] }); - /** * Confirm type on SO so that it's clear in the tests whether it's expected that * the SO itself is migrated to the inteded object type, or if the transformation is @@ -3167,7 +3176,12 @@ export default ({ getService }: FtrProviderContext): void => { hits: [{ _source: ruleSO }], }, } = await getRuleSOById(es, ruleWithLegacyField.id); - expect(ruleSO?.alert?.params?.investigationFields).to.eql(['client.address', 'agent.name']); + + const isInvestigationFieldMigratedInSo = await checkInvestigationFieldSoValue(ruleSO, { + field_names: ['client.address', 'agent.name'], + }); + + expect(isInvestigationFieldMigratedInSo).to.eql(false); expect(ruleSO?.alert?.enabled).to.eql(true); const { @@ -3226,26 +3240,36 @@ export default ({ getService }: FtrProviderContext): void => { * the SO itself is migrated to the inteded object type, or if the transformation is * happening just on the response. In this case, change should not include a migration on SO. */ - const { - hits: { - hits: [{ _source: ruleSO }], - }, - } = await getRuleSOById(es, ruleWithLegacyField.id); - expect(ruleSO?.alert?.params?.investigationFields).to.eql(['client.address', 'agent.name']); - - const { - hits: { - hits: [{ _source: ruleSO2 }], - }, - } = await getRuleSOById(es, ruleWithEmptyArray.id); - expect(ruleSO2?.alert?.params?.investigationFields).to.eql([]); + const isInvestigationFieldForRuleWithLegacyFieldMigratedInSo = + await checkInvestigationFieldSoValue( + undefined, + { + field_names: ['client.address', 'agent.name'], + }, + es, + ruleWithLegacyField.id + ); + expect(isInvestigationFieldForRuleWithLegacyFieldMigratedInSo).to.eql(false); - const { - hits: { - hits: [{ _source: ruleSO3 }], - }, - } = await getRuleSOById(es, ruleWithIntendedType.id); - expect(ruleSO3?.alert?.params?.investigationFields).to.eql({ field_names: ['host.name'] }); + const isInvestigationFieldForRuleWithEmptyArraydMigratedInSo = + await checkInvestigationFieldSoValue( + undefined, + { + field_names: [], + }, + es, + ruleWithEmptyArray.id + ); + expect(isInvestigationFieldForRuleWithEmptyArraydMigratedInSo).to.eql(false); + + const isInvestigationFieldForRuleWithIntendedTypeMigratedInSo = + await checkInvestigationFieldSoValue( + undefined, + { field_names: ['host.name'] }, + es, + ruleWithIntendedType.id + ); + expect(isInvestigationFieldForRuleWithIntendedTypeMigratedInSo).to.eql(true); }); it('should duplicate rules with legacy investigation fields and transform field in response', async () => { @@ -3289,66 +3313,82 @@ export default ({ getService }: FtrProviderContext): void => { returnedRule.name === 'Test investigation fields object [Duplicate]' ); + // DUPLICATED RULES /** * Confirm type on SO so that it's clear in the tests whether it's expected that * the SO itself is migrated to the inteded object type, or if the transformation is * happening just on the response. In this case, duplicated * rules should NOT have migrated value on write. */ - const { - hits: { - hits: [{ _source: ruleSO }], - }, - } = await getRuleSOById(es, ruleWithLegacyField.id); - - expect(ruleSO?.alert?.params?.investigationFields).to.eql(['client.address', 'agent.name']); - - const { - hits: { - hits: [{ _source: ruleSO2 }], - }, - } = await getRuleSOById(es, ruleWithEmptyArray.id); - expect(ruleSO2?.alert?.params?.investigationFields).to.eql([]); - - const { - hits: { - hits: [{ _source: ruleSO3 }], - }, - } = await getRuleSOById(es, ruleWithIntendedType.id); - expect(ruleSO3?.alert?.params?.investigationFields).to.eql({ field_names: ['host.name'] }); + const isInvestigationFieldForRuleWithLegacyFieldMigratedInSo = + await checkInvestigationFieldSoValue( + undefined, + { field_names: ['client.address', 'agent.name'] }, + es, + ruleWithLegacyField.id + ); + expect(isInvestigationFieldForRuleWithLegacyFieldMigratedInSo).to.eql(false); + + const isInvestigationFieldForRuleWithEmptyArrayMigratedInSo = + await checkInvestigationFieldSoValue( + undefined, + { field_names: [] }, + es, + ruleWithEmptyArray.id + ); + expect(isInvestigationFieldForRuleWithEmptyArrayMigratedInSo).to.eql(false); + + /* + It's duplicate of a rule with properly formatted "investigation fields". + So we just check that "investigation fields" are in intended format. + No migration needs to happen. + */ + const isInvestigationFieldForRuleWithIntendedTypeInSo = + await checkInvestigationFieldSoValue( + undefined, + { field_names: ['host.name'] }, + es, + ruleWithIntendedType.id + ); + expect(isInvestigationFieldForRuleWithIntendedTypeInSo).to.eql(true); + // ORIGINAL RULES - rules selected to be duplicated /** * Confirm type on SO so that it's clear in the tests whether it's expected that * the SO itself is migrated to the inteded object type, or if the transformation is * happening just on the response. In this case, the original * rules selected to be duplicated should not be migrated. */ - const { - hits: { - hits: [{ _source: ruleSOOriginalLegacy }], - }, - } = await getRuleSOById(es, ruleWithLegacyInvestigationField.id); - - expect(ruleSOOriginalLegacy?.alert?.params?.investigationFields).to.eql([ - 'client.address', - 'agent.name', - ]); - - const { - hits: { - hits: [{ _source: ruleSOOriginalLegacyEmptyArray }], - }, - } = await getRuleSOById(es, ruleWithLegacyInvestigationFieldEmptyArray.id); - expect(ruleSOOriginalLegacyEmptyArray?.alert?.params?.investigationFields).to.eql([]); - - const { - hits: { - hits: [{ _source: ruleSOOriginalNoLegacy }], - }, - } = await getRuleSOById(es, ruleWithIntendedType.id); - expect(ruleSOOriginalNoLegacy?.alert?.params?.investigationFields).to.eql({ - field_names: ['host.name'], - }); + const isInvestigationFieldForOriginalRuleWithLegacyFieldMigratedInSo = + await checkInvestigationFieldSoValue( + undefined, + { field_names: ['client.address', 'agent.name'] }, + es, + ruleWithLegacyInvestigationField.id + ); + expect(isInvestigationFieldForOriginalRuleWithLegacyFieldMigratedInSo).to.eql(false); + + const isInvestigationFieldForOriginalRuleWithEmptyArrayMigratedInSo = + await checkInvestigationFieldSoValue( + undefined, + { field_names: [] }, + es, + ruleWithLegacyInvestigationFieldEmptyArray.id + ); + expect(isInvestigationFieldForOriginalRuleWithEmptyArrayMigratedInSo).to.eql(false); + + /* + Since this rule was created with intended "investigation fields" format, + it shouldn't change - no need to migrate. + */ + const isInvestigationFieldForOriginalRuleWithIntendedTypeInSo = + await checkInvestigationFieldSoValue( + undefined, + { field_names: ['host.name'] }, + es, + ruleWithIntendedInvestigationField.id + ); + expect(isInvestigationFieldForOriginalRuleWithIntendedTypeInSo).to.eql(true); }); it('should edit rules with legacy investigation fields', async () => { @@ -3398,26 +3438,32 @@ export default ({ getService }: FtrProviderContext): void => { * the SO itself is migrated to the inteded object type, or if the transformation is * happening just on the response. In this case, change should not include a migration on SO. */ - const { - hits: { - hits: [{ _source: ruleSO }], - }, - } = await getRuleSOById(es, ruleWithLegacyInvestigationField.id); - expect(ruleSO?.alert?.params?.investigationFields).to.eql(['client.address', 'agent.name']); - - const { - hits: { - hits: [{ _source: ruleSO2 }], - }, - } = await getRuleSOById(es, ruleWithLegacyInvestigationFieldEmptyArray.id); - expect(ruleSO2?.alert?.params?.investigationFields).to.eql([]); - - const { - hits: { - hits: [{ _source: ruleSO3 }], - }, - } = await getRuleSOById(es, ruleWithIntendedType.id); - expect(ruleSO3?.alert?.params?.investigationFields).to.eql({ field_names: ['host.name'] }); + const isInvestigationFieldForRuleWithLegacyFieldMigratedInSo = + await checkInvestigationFieldSoValue( + undefined, + { field_names: ['client.address', 'agent.name'] }, + es, + ruleWithLegacyInvestigationField.id + ); + expect(isInvestigationFieldForRuleWithLegacyFieldMigratedInSo).to.eql(false); + + const isInvestigationFieldForRuleWithEmptyArrayFieldMigratedInSo = + await checkInvestigationFieldSoValue( + undefined, + { field_names: [] }, + es, + ruleWithLegacyInvestigationFieldEmptyArray.id + ); + expect(isInvestigationFieldForRuleWithEmptyArrayFieldMigratedInSo).to.eql(false); + + const isInvestigationFieldForRuleWithIntendedTypeMigratedInSo = + await checkInvestigationFieldSoValue( + undefined, + { field_names: ['host.name'] }, + es, + ruleWithIntendedType.id + ); + expect(isInvestigationFieldForRuleWithIntendedTypeMigratedInSo).to.eql(true); }); }); }); diff --git a/x-pack/test/detection_engine_api_integration/utils/check_investigation_field_in_so.ts b/x-pack/test/detection_engine_api_integration/utils/check_investigation_field_in_so.ts new file mode 100644 index 0000000000000..36804d6c0f50f --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/utils/check_investigation_field_in_so.ts @@ -0,0 +1,38 @@ +/* + * 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 { Client } from '@elastic/elasticsearch'; +import { SavedObjectReference } from '@kbn/core/server'; +import { InvestigationFields } from '@kbn/security-solution-plugin/common/api/detection_engine'; +import { Rule } from '@kbn/alerting-plugin/common'; +import { BaseRuleParams } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_schema'; +import { isEqual } from 'lodash/fp'; +import { getRuleSOById } from './get_rule_so_by_id'; + +interface RuleSO { + alert: Rule; + references: SavedObjectReference[]; +} + +export const checkInvestigationFieldSoValue = async ( + ruleSO: RuleSO | undefined, + expectedSoValue: undefined | InvestigationFields, + es?: Client, + ruleId?: string +): Promise => { + if (!ruleSO && es && ruleId) { + const { + hits: { + hits: [{ _source: rule }], + }, + } = await getRuleSOById(es, ruleId); + + return isEqual(rule?.alert.params.investigationFields, expectedSoValue); + } + + return isEqual(ruleSO?.alert.params.investigationFields, expectedSoValue); +}; diff --git a/x-pack/test/detection_engine_api_integration/utils/index.ts b/x-pack/test/detection_engine_api_integration/utils/index.ts index 1b18044d95449..e9db95076bc43 100644 --- a/x-pack/test/detection_engine_api_integration/utils/index.ts +++ b/x-pack/test/detection_engine_api_integration/utils/index.ts @@ -6,6 +6,7 @@ */ export * from './binary_to_string'; +export * from './check_investigation_field_in_so'; export * from './count_down_es'; export * from './count_down_test'; export * from './create_container_with_endpoint_entries';