From bf37a019d857e89d6dad3a6cf450ec323f0783e0 Mon Sep 17 00:00:00 2001 From: Maxim Palenov Date: Tue, 5 Nov 2024 13:24:37 +0300 Subject: [PATCH] [Security Solution] Unskip perform bulk action ESS FTR tests (#197660) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Closes: https://github.com/elastic/kibana/issues/196470** **Closes: https://github.com/elastic/kibana/issues/196462** ## Summary This PR unskips `perform_bulk_action_ess.ts` functional test. ## Details `perform_bulk_action_ess.ts` includes a number of functional tests where some of them were flaky. Investigation revealed that creating enabled rules and performing bulk actions may lead to race conditions. Under that conditions rule's SO isn't updated as expected. For example legacy rule actions aren't persisted in rule's SO when it's expected by the test. This PR includes `perform_bulk_action_ess.ts` refactoring to create disabled rules in the majority of tests. Enabled rules are created only in tests checking behavior upon rules disabling. These tests were checked multiple times and didn't demonstrate flakiness. Additionally `@kbn/expect` was replaced with `expect` to make asserting more transparent and avoid unclear error messages. ## Flaky test results [100 runs](https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/7274) 🟢 --- .../perform_bulk_action_ess.ts | 442 ++++++++++++------ ...t_rule_with_legacy_investigation_fields.ts | 12 +- 2 files changed, 298 insertions(+), 156 deletions(-) diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action_ess.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action_ess.ts index fcaf94728e854..7ee0ce440caad 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action_ess.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action_ess.ts @@ -5,15 +5,13 @@ * 2.0. */ -import { Rule } from '@kbn/alerting-plugin/common'; -import expect from '@kbn/expect'; +import expect from 'expect'; import type { RuleResponse } from '@kbn/security-solution-plugin/common/api/detection_engine'; import { getCreateEsqlRulesSchemaMock } from '@kbn/security-solution-plugin/common/api/detection_engine/model/rule_schema/mocks'; import { BulkActionEditTypeEnum, BulkActionTypeEnum, } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management'; -import { BaseRuleParams } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_schema'; import { createRule, deleteAllRules, @@ -30,10 +28,12 @@ import { getRuleSavedObjectWithLegacyInvestigationFields, getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray, getRuleSOById, - getSimpleRule, getWebHookAction, } from '../../../utils'; +// Rule's interval must be less or equal rule action's interval +const MINIMUM_RULE_INTERVAL_FOR_LEGACY_ACTION = '1h'; + export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const securitySolutionApi = getService('securitySolutionApi'); @@ -51,17 +51,14 @@ export default ({ getService }: FtrProviderContext): void => { const createWebHookConnector = () => createConnector(getWebHookAction()); - // Failing: See https://github.com/elastic/kibana/issues/173804 - // Failing: See https://github.com/elastic/kibana/issues/196470 - // Failing: See https://github.com/elastic/kibana/issues/196462 - describe.skip('@ess perform_bulk_action - ESS specific logic', () => { + describe('@ess perform_bulk_action - ESS specific logic', () => { beforeEach(async () => { await deleteAllRules(supertest, log); }); it('should delete rules and any associated legacy actions', async () => { const ruleId = 'ruleId'; - const [connector, rule1] = await Promise.all([ + const [connector, rule] = await Promise.all([ supertest .post(`/api/actions/connector`) .set('kbn-xsrf', 'foo') @@ -72,14 +69,22 @@ export default ({ getService }: FtrProviderContext): void => { webhookUrl: 'http://localhost:1234', }, }), - createRule(supertest, log, getSimpleRule(ruleId, false)), + + createRule( + supertest, + log, + getCustomQueryRuleParams({ + rule_id: ruleId, + interval: MINIMUM_RULE_INTERVAL_FOR_LEGACY_ACTION, + }) + ), ]); - await createLegacyRuleAction(supertest, rule1.id, connector.body.id); + await createLegacyRuleAction(supertest, rule.id, connector.body.id); // check for legacy sidecar action const sidecarActionsResults = await getLegacyActionSO(es); - expect(sidecarActionsResults.hits.hits.length).to.eql(1); - expect(sidecarActionsResults.hits.hits[0]?._source?.references[0].id).to.eql(rule1.id); + expect(sidecarActionsResults.hits.hits.length).toBe(1); + expect(sidecarActionsResults.hits.hits[0]?._source?.references[0].id).toBe(rule.id); const { body } = await securitySolutionApi .performRulesBulkAction({ @@ -88,14 +93,14 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); + expect(body.attributes.summary).toEqual({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); // Check that the deleted rule is returned with the response - expect(body.attributes.results.deleted[0].name).to.eql(rule1.name); + expect(body.attributes.results.deleted[0].name).toEqual(rule.name); // legacy sidecar action should be gone const sidecarActionsPostResults = await getLegacyActionSO(es); - expect(sidecarActionsPostResults.hits.hits.length).to.eql(0); + expect(sidecarActionsPostResults.hits.hits.length).toBe(0); // Check that the updates have been persisted await securitySolutionApi.readRule({ query: { rule_id: ruleId } }).expect(404); @@ -103,7 +108,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should enable rules and migrate actions', async () => { const ruleId = 'ruleId'; - const [connector, rule1] = await Promise.all([ + const [connector, rule] = await Promise.all([ supertest .post(`/api/actions/connector`) .set('kbn-xsrf', 'foo') @@ -114,14 +119,23 @@ export default ({ getService }: FtrProviderContext): void => { webhookUrl: 'http://localhost:1234', }, }), - createRule(supertest, log, { ...getSimpleRule(ruleId, false), index: ['*'] }), + createRule( + supertest, + log, + getCustomQueryRuleParams({ + rule_id: ruleId, + index: ['*'], + interval: MINIMUM_RULE_INTERVAL_FOR_LEGACY_ACTION, + enabled: false, + }) + ), ]); - await createLegacyRuleAction(supertest, rule1.id, connector.body.id); + await createLegacyRuleAction(supertest, rule.id, connector.body.id); // check for legacy sidecar action const sidecarActionsResults = await getLegacyActionSO(es); - expect(sidecarActionsResults.hits.hits.length).to.eql(1); - expect(sidecarActionsResults.hits.hits[0]?._source?.references[0].id).to.eql(rule1.id); + expect(sidecarActionsResults.hits.hits.length).toBe(1); + expect(sidecarActionsResults.hits.hits[0]?._source?.references[0].id).toBe(rule.id); const { body } = await securitySolutionApi .performRulesBulkAction({ @@ -130,10 +144,10 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); + expect(body.attributes.summary).toEqual({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); // Check that the updated rule is returned with the response - expect(body.attributes.results.updated[0].enabled).to.eql(true); + expect(body.attributes.results.updated[0].enabled).toBeTruthy(); // Check that the updates have been persisted const { body: ruleBody } = await securitySolutionApi @@ -142,10 +156,10 @@ export default ({ getService }: FtrProviderContext): void => { // legacy sidecar action should be gone const sidecarActionsPostResults = await getLegacyActionSO(es); - expect(sidecarActionsPostResults.hits.hits.length).to.eql(0); + expect(sidecarActionsPostResults.hits.hits.length).toBe(0); - expect(ruleBody.enabled).to.eql(true); - expect(ruleBody.actions).to.eql([ + expect(ruleBody.enabled).toBeTruthy(); + expect(ruleBody.actions).toEqual([ { action_type_id: '.slack', group: 'default', @@ -153,17 +167,17 @@ export default ({ getService }: FtrProviderContext): void => { params: { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, - uuid: ruleBody.actions[0].uuid, + uuid: expect.any(String), frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]); // we want to ensure rule is executing successfully, to prevent any AAD issues related to partial update of rule SO - await waitForRuleSuccess({ id: rule1.id, supertest, log }); + await waitForRuleSuccess({ id: rule.id, supertest, log }); }); it('should disable rules and migrate actions', async () => { const ruleId = 'ruleId'; - const [connector, rule1] = await Promise.all([ + const [connector, rule] = await Promise.all([ supertest .post(`/api/actions/connector`) .set('kbn-xsrf', 'foo') @@ -174,14 +188,22 @@ export default ({ getService }: FtrProviderContext): void => { webhookUrl: 'http://localhost:1234', }, }), - createRule(supertest, log, getSimpleRule(ruleId, true)), + createRule( + supertest, + log, + getCustomQueryRuleParams({ + rule_id: ruleId, + interval: MINIMUM_RULE_INTERVAL_FOR_LEGACY_ACTION, + enabled: true, + }) + ), ]); - await createLegacyRuleAction(supertest, rule1.id, connector.body.id); + await createLegacyRuleAction(supertest, rule.id, connector.body.id); // check for legacy sidecar action const sidecarActionsResults = await getLegacyActionSO(es); - expect(sidecarActionsResults.hits.hits.length).to.eql(1); - expect(sidecarActionsResults.hits.hits[0]?._source?.references[0].id).to.eql(rule1.id); + expect(sidecarActionsResults.hits.hits.length).toBe(1); + expect(sidecarActionsResults.hits.hits[0]?._source?.references[0].id).toBe(rule.id); const { body } = await securitySolutionApi .performRulesBulkAction({ @@ -190,10 +212,10 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); + expect(body.attributes.summary).toEqual({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); // Check that the updated rule is returned with the response - expect(body.attributes.results.updated[0].enabled).to.eql(false); + expect(body.attributes.results.updated[0].enabled).toBeFalsy(); // Check that the updates have been persisted const { body: ruleBody } = await securitySolutionApi @@ -202,10 +224,10 @@ export default ({ getService }: FtrProviderContext): void => { // legacy sidecar action should be gone const sidecarActionsPostResults = await getLegacyActionSO(es); - expect(sidecarActionsPostResults.hits.hits.length).to.eql(0); + expect(sidecarActionsPostResults.hits.hits.length).toBe(0); - expect(ruleBody.enabled).to.eql(false); - expect(ruleBody.actions).to.eql([ + expect(ruleBody.enabled).toBeFalsy(); + expect(ruleBody.actions).toEqual([ { action_type_id: '.slack', group: 'default', @@ -213,7 +235,7 @@ export default ({ getService }: FtrProviderContext): void => { params: { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, - uuid: ruleBody.actions[0].uuid, + uuid: expect.any(String), frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]); @@ -232,14 +254,21 @@ export default ({ getService }: FtrProviderContext): void => { webhookUrl: 'http://localhost:1234', }, }), - createRule(supertest, log, getSimpleRule(ruleId, true)), + createRule( + supertest, + log, + getCustomQueryRuleParams({ + rule_id: ruleId, + interval: MINIMUM_RULE_INTERVAL_FOR_LEGACY_ACTION, + }) + ), ]); await createLegacyRuleAction(supertest, ruleToDuplicate.id, connector.body.id); // check for legacy sidecar action const sidecarActionsResults = await getLegacyActionSO(es); - expect(sidecarActionsResults.hits.hits.length).to.eql(1); - expect(sidecarActionsResults.hits.hits[0]?._source?.references[0].id).to.eql( + expect(sidecarActionsResults.hits.hits.length).toBe(1); + expect(sidecarActionsResults.hits.hits[0]?._source?.references[0].id).toBe( ruleToDuplicate.id ); @@ -254,22 +283,21 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); + expect(body.attributes.summary).toEqual({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); // Check that the duplicated rule is returned with the response - expect(body.attributes.results.created[0].name).to.eql(`${ruleToDuplicate.name} [Duplicate]`); + expect(body.attributes.results.created[0].name).toBe(`${ruleToDuplicate.name} [Duplicate]`); // Check that the updates have been persisted const { body: rulesResponse } = await securitySolutionApi .findRules({ query: {} }) .expect(200); - expect(rulesResponse.total).to.eql(2); + expect(rulesResponse.total).toBe(2); rulesResponse.data.forEach((rule: RuleResponse) => { - const uuid = rule.actions[0].uuid; - expect(rule.actions).to.eql([ - { + expect(rule.actions).toEqual([ + expect.objectContaining({ action_type_id: '.slack', group: 'default', id: connector.body.id, @@ -277,9 +305,8 @@ export default ({ getService }: FtrProviderContext): void => { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, - ...(uuid ? { uuid } : {}), frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, - }, + }), ]); }); }); @@ -299,7 +326,7 @@ export default ({ getService }: FtrProviderContext): void => { .expect(200); // Check that the duplicated rule is returned with the correct rule_source - expect(body.attributes.results.created[0].rule_source).to.eql({ + expect(body.attributes.results.created[0].rule_source).toEqual({ type: 'internal', }); }); @@ -325,8 +352,13 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(500); - expect(body.attributes.summary).to.eql({ failed: 1, skipped: 0, succeeded: 0, total: 1 }); - expect(body.attributes.errors[0]).to.eql({ + expect(body.attributes.summary).toEqual({ + failed: 1, + skipped: 0, + succeeded: 0, + total: 1, + }); + expect(body.attributes.errors[0]).toEqual({ message: "Index patterns can't be added. ES|QL rule doesn't have index patterns property", status_code: 500, @@ -353,31 +385,41 @@ export default ({ getService }: FtrProviderContext): void => { webhookUrl: 'http://localhost:1234', }, }), - createRule(supertest, log, getSimpleRule(ruleId, true)), + createRule( + supertest, + log, + getCustomQueryRuleParams({ + rule_id: ruleId, + interval: MINIMUM_RULE_INTERVAL_FOR_LEGACY_ACTION, + }) + ), ]); await createLegacyRuleAction(supertest, ruleToDuplicate.id, connector.body.id); // check for legacy sidecar action const sidecarActionsResults = await getLegacyActionSO(es); - expect(sidecarActionsResults.hits.hits.length).to.eql(1); - expect(sidecarActionsResults.hits.hits[0]?._source?.references[0].id).to.eql( + expect(sidecarActionsResults.hits.hits.length).toBe(1); + expect(sidecarActionsResults.hits.hits[0]?._source?.references[0].id).toBe( ruleToDuplicate.id ); - const { body: setTagsBody } = await securitySolutionApi.performRulesBulkAction({ - body: { - query: '', - action: BulkActionTypeEnum.edit, - [BulkActionTypeEnum.edit]: [ - { - type: BulkActionEditTypeEnum.set_tags, - value: ['reset-tag'], - }, - ], - }, - query: {}, - }); - expect(setTagsBody.attributes.summary).to.eql({ + const { body: setTagsBody } = await securitySolutionApi + .performRulesBulkAction({ + body: { + query: '', + action: BulkActionTypeEnum.edit, + [BulkActionTypeEnum.edit]: [ + { + type: BulkActionEditTypeEnum.set_tags, + value: ['reset-tag'], + }, + ], + }, + query: {}, + }) + .expect(200); + + expect(setTagsBody.attributes.summary).toEqual({ failed: 0, skipped: 0, succeeded: 1, @@ -391,11 +433,10 @@ export default ({ getService }: FtrProviderContext): void => { // Sidecar should be removed const sidecarActionsPostResults = await getLegacyActionSO(es); - expect(sidecarActionsPostResults.hits.hits.length).to.eql(0); - - expect(setTagsRule.tags).to.eql(['reset-tag']); - expect(setTagsRule.actions).to.eql([ + expect(sidecarActionsPostResults.hits.hits.length).toBe(0); + expect(setTagsRule.tags).toEqual(['reset-tag']); + expect(setTagsRule.actions).toEqual([ { action_type_id: '.slack', group: 'default', @@ -404,7 +445,7 @@ export default ({ getService }: FtrProviderContext): void => { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, - uuid: setTagsRule.actions[0].uuid, + uuid: expect.any(String), frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]); @@ -432,7 +473,14 @@ export default ({ getService }: FtrProviderContext): void => { webhookUrl: 'http://localhost:1234', }, }), - createRule(supertest, log, getSimpleRule(ruleId, true)), + createRule( + supertest, + log, + getCustomQueryRuleParams({ + rule_id: ruleId, + interval: MINIMUM_RULE_INTERVAL_FOR_LEGACY_ACTION, + }) + ), ]); // create a new connector const webHookConnector = await createWebHookConnector(); @@ -441,8 +489,8 @@ export default ({ getService }: FtrProviderContext): void => { // check for legacy sidecar action const sidecarActionsResults = await getLegacyActionSO(es); - expect(sidecarActionsResults.hits.hits.length).to.eql(1); - expect(sidecarActionsResults.hits.hits[0]?._source?.references[0].id).to.eql( + expect(sidecarActionsResults.hits.hits.length).toBe(1); + expect(sidecarActionsResults.hits.hits[0]?._source?.references[0].id).toBe( createdRule.id ); @@ -475,53 +523,51 @@ export default ({ getService }: FtrProviderContext): void => { ...webHookActionMock, id: webHookConnector.id, action_type_id: '.webhook', - uuid: body.attributes.results.updated[0].actions[0].uuid, + uuid: expect.any(String), frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]; // Check that the updated rule is returned with the response - expect(body.attributes.results.updated[0].actions).to.eql(expectedRuleActions); + expect(body.attributes.results.updated[0].actions).toEqual(expectedRuleActions); // Check that the updates have been persisted const { body: readRule } = await securitySolutionApi .readRule({ query: { rule_id: ruleId } }) .expect(200); - expect(readRule.actions).to.eql(expectedRuleActions); + expect(readRule.actions).toEqual(expectedRuleActions); // Sidecar should be removed const sidecarActionsPostResults = await getLegacyActionSO(es); - expect(sidecarActionsPostResults.hits.hits.length).to.eql(0); + expect(sidecarActionsPostResults.hits.hits.length).toBe(0); }); }); }); }); describe('legacy investigation fields', () => { - let ruleWithLegacyInvestigationField: Rule; - let ruleWithLegacyInvestigationFieldEmptyArray: Rule; - let ruleWithIntendedInvestigationField: RuleResponse; - - beforeEach(async () => { - ruleWithLegacyInvestigationField = await createRuleThroughAlertingEndpoint( - supertest, - getRuleSavedObjectWithLegacyInvestigationFields() - ); - - ruleWithLegacyInvestigationFieldEmptyArray = await createRuleThroughAlertingEndpoint( - supertest, - getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray() - ); - - ruleWithIntendedInvestigationField = await createRule(supertest, log, { - ...getSimpleRule('rule-with-investigation-field'), - name: 'Test investigation fields object', - investigation_fields: { field_names: ['host.name'] }, - }); - }); - it('should export rules with legacy investigation_fields and transform legacy field in response', async () => { + const [ + ruleWithLegacyInvestigationField, + ruleWithLegacyInvestigationFieldEmptyArray, + ruleWithIntendedInvestigationField, + ] = await Promise.all([ + createRuleThroughAlertingEndpoint( + supertest, + getRuleSavedObjectWithLegacyInvestigationFields() + ), + createRuleThroughAlertingEndpoint( + supertest, + getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray() + ), + createRule(supertest, log, { + ...getCustomQueryRuleParams({ rule_id: 'rule-with-investigation-field' }), + name: 'Test investigation fields object', + investigation_fields: { field_names: ['host.name'] }, + }), + ]); + const { body } = await securitySolutionApi .performRulesBulkAction({ body: { query: '', action: BulkActionTypeEnum.export }, @@ -538,21 +584,21 @@ export default ({ getService }: FtrProviderContext): void => { const exportedRuleWithLegacyInvestigationField = exportedRules.find( (rule) => rule.id === ruleWithLegacyInvestigationField.id ); - expect(exportedRuleWithLegacyInvestigationField.investigation_fields).to.eql({ + expect(exportedRuleWithLegacyInvestigationField.investigation_fields).toEqual({ field_names: ['client.address', 'agent.name'], }); const exportedRuleWithLegacyInvestigationFieldEmptyArray = exportedRules.find( (rule) => rule.id === ruleWithLegacyInvestigationFieldEmptyArray.id ); - expect(exportedRuleWithLegacyInvestigationFieldEmptyArray.investigation_fields).to.eql( + expect(exportedRuleWithLegacyInvestigationFieldEmptyArray.investigation_fields).toEqual( undefined ); const exportedRuleWithInvestigationField = exportedRules.find( (rule) => rule.id === ruleWithIntendedInvestigationField.id ); - expect(exportedRuleWithInvestigationField.investigation_fields).to.eql({ + expect(exportedRuleWithInvestigationField.investigation_fields).toEqual({ field_names: ['host.name'], }); @@ -568,10 +614,10 @@ export default ({ getService }: FtrProviderContext): void => { JSON.parse(rule1).id ); - expect(isInvestigationFieldMigratedInSo).to.eql(false); + expect(isInvestigationFieldMigratedInSo).toBeFalsy(); const exportDetails = JSON.parse(exportDetailsJson); - expect(exportDetails).to.eql({ + expect(exportDetails).toEqual({ exported_exception_list_count: 0, exported_exception_list_item_count: 0, exported_count: 3, @@ -591,6 +637,23 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should delete rules with investigation fields and transform legacy field in response', async () => { + const [ruleWithLegacyInvestigationField, ruleWithLegacyInvestigationFieldEmptyArray] = + await Promise.all([ + createRuleThroughAlertingEndpoint( + supertest, + getRuleSavedObjectWithLegacyInvestigationFields() + ), + createRuleThroughAlertingEndpoint( + supertest, + getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray() + ), + createRule(supertest, log, { + ...getCustomQueryRuleParams({ rule_id: 'rule-with-investigation-field' }), + name: 'Test investigation fields object', + investigation_fields: { field_names: ['host.name'] }, + }), + ]); + const { body } = await securitySolutionApi .performRulesBulkAction({ body: { query: '', action: BulkActionTypeEnum.delete }, @@ -598,22 +661,22 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 3, total: 3 }); + expect(body.attributes.summary).toEqual({ failed: 0, skipped: 0, succeeded: 3, total: 3 }); // Check that the deleted rule is returned with the response const names = body.attributes.results.deleted.map( (returnedRule: RuleResponse) => returnedRule.name ); - expect(names.includes('Test investigation fields')).to.eql(true); - expect(names.includes('Test investigation fields empty array')).to.eql(true); - expect(names.includes('Test investigation fields object')).to.eql(true); + expect(names.includes('Test investigation fields')).toBeTruthy(); + expect(names.includes('Test investigation fields empty array')).toBeTruthy(); + expect(names.includes('Test investigation fields object')).toBeTruthy(); const ruleWithLegacyField = body.attributes.results.deleted.find( (returnedRule: RuleResponse) => returnedRule.rule_id === ruleWithLegacyInvestigationField.params.ruleId ); - expect(ruleWithLegacyField.investigation_fields).to.eql({ + expect(ruleWithLegacyField.investigation_fields).toEqual({ field_names: ['client.address', 'agent.name'], }); @@ -632,6 +695,23 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should enable rules with legacy investigation fields and transform legacy field in response', async () => { + const [ruleWithLegacyInvestigationField, ruleWithLegacyInvestigationFieldEmptyArray] = + await Promise.all([ + createRuleThroughAlertingEndpoint( + supertest, + getRuleSavedObjectWithLegacyInvestigationFields({ enabled: false }) + ), + createRuleThroughAlertingEndpoint( + supertest, + getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray({ enabled: false }) + ), + createRule(supertest, log, { + ...getCustomQueryRuleParams({ rule_id: 'rule-with-investigation-field' }), + name: 'Test investigation fields object', + investigation_fields: { field_names: ['host.name'] }, + }), + ]); + const { body } = await securitySolutionApi .performRulesBulkAction({ body: { query: '', action: BulkActionTypeEnum.enable }, @@ -639,7 +719,7 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 3, total: 3 }); + expect(body.attributes.summary).toEqual({ failed: 0, skipped: 0, succeeded: 3, total: 3 }); // Check that the updated rule is returned with the response // and field transformed on response @@ -647,13 +727,13 @@ export default ({ getService }: FtrProviderContext): void => { body.attributes.results.updated.every( (returnedRule: RuleResponse) => returnedRule.enabled ) - ).to.eql(true); + ).toBeTruthy(); const ruleWithLegacyField = body.attributes.results.updated.find( (returnedRule: RuleResponse) => returnedRule.rule_id === ruleWithLegacyInvestigationField.params.ruleId ); - expect(ruleWithLegacyField.investigation_fields).to.eql({ + expect(ruleWithLegacyField.investigation_fields).toEqual({ field_names: ['client.address', 'agent.name'], }); @@ -661,12 +741,12 @@ export default ({ getService }: FtrProviderContext): void => { (returnedRule: RuleResponse) => returnedRule.rule_id === ruleWithLegacyInvestigationFieldEmptyArray.params.ruleId ); - expect(ruleWithEmptyArray.investigation_fields).to.eql(undefined); + expect(ruleWithEmptyArray.investigation_fields).toBeUndefined(); const ruleWithIntendedType = body.attributes.results.updated.find( (returnedRule: RuleResponse) => returnedRule.rule_id === 'rule-with-investigation-field' ); - expect(ruleWithIntendedType.investigation_fields).to.eql({ field_names: ['host.name'] }); + expect(ruleWithIntendedType.investigation_fields).toEqual({ 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 @@ -682,27 +762,44 @@ export default ({ getService }: FtrProviderContext): void => { field_names: ['client.address', 'agent.name'], }); - expect(isInvestigationFieldMigratedInSo).to.eql(false); - expect(ruleSO?.alert?.enabled).to.eql(true); + expect(isInvestigationFieldMigratedInSo).toBeFalsy(); + expect(ruleSO?.alert?.enabled).toBeTruthy(); const { hits: { hits: [{ _source: ruleSO2 }], }, } = await getRuleSOById(es, ruleWithEmptyArray.id); - expect(ruleSO2?.alert?.params?.investigationFields).to.eql([]); - expect(ruleSO?.alert?.enabled).to.eql(true); + expect(ruleSO2?.alert?.params?.investigationFields).toEqual([]); + expect(ruleSO?.alert?.enabled).toBeTruthy(); const { hits: { hits: [{ _source: ruleSO3 }], }, } = await getRuleSOById(es, ruleWithIntendedType.id); - expect(ruleSO3?.alert?.params?.investigationFields).to.eql({ field_names: ['host.name'] }); - expect(ruleSO?.alert?.enabled).to.eql(true); + expect(ruleSO3?.alert?.params?.investigationFields).toEqual({ field_names: ['host.name'] }); + expect(ruleSO?.alert?.enabled).toBeTruthy(); }); it('should disable rules with legacy investigation fields and transform legacy field in response', async () => { + const [ruleWithLegacyInvestigationField, ruleWithLegacyInvestigationFieldEmptyArray] = + await Promise.all([ + createRuleThroughAlertingEndpoint( + supertest, + getRuleSavedObjectWithLegacyInvestigationFields({ enabled: true }) + ), + createRuleThroughAlertingEndpoint( + supertest, + getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray({ enabled: false }) + ), + createRule(supertest, log, { + ...getCustomQueryRuleParams({ rule_id: 'rule-with-investigation-field' }), + name: 'Test investigation fields object', + investigation_fields: { field_names: ['host.name'] }, + }), + ]); + const { body } = await securitySolutionApi .performRulesBulkAction({ body: { query: '', action: BulkActionTypeEnum.disable }, @@ -710,7 +807,7 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 3, total: 3 }); + expect(body.attributes.summary).toEqual({ failed: 0, skipped: 0, succeeded: 3, total: 3 }); // Check that the updated rule is returned with the response // and field transformed on response @@ -718,13 +815,13 @@ export default ({ getService }: FtrProviderContext): void => { body.attributes.results.updated.every( (returnedRule: RuleResponse) => !returnedRule.enabled ) - ).to.eql(true); + ).toBeTruthy(); const ruleWithLegacyField = body.attributes.results.updated.find( (returnedRule: RuleResponse) => returnedRule.rule_id === ruleWithLegacyInvestigationField.params.ruleId ); - expect(ruleWithLegacyField.investigation_fields).to.eql({ + expect(ruleWithLegacyField.investigation_fields).toEqual({ field_names: ['client.address', 'agent.name'], }); @@ -732,12 +829,12 @@ export default ({ getService }: FtrProviderContext): void => { (returnedRule: RuleResponse) => returnedRule.rule_id === ruleWithLegacyInvestigationFieldEmptyArray.params.ruleId ); - expect(ruleWithEmptyArray.investigation_fields).to.eql(undefined); + expect(ruleWithEmptyArray.investigation_fields).toBeUndefined(); const ruleWithIntendedType = body.attributes.results.updated.find( (returnedRule: RuleResponse) => returnedRule.rule_id === 'rule-with-investigation-field' ); - expect(ruleWithIntendedType.investigation_fields).to.eql({ field_names: ['host.name'] }); + expect(ruleWithIntendedType.investigation_fields).toEqual({ field_names: ['host.name'] }); /** * Confirm type on SO so that it's clear in the tests whether it's expected that @@ -753,7 +850,7 @@ export default ({ getService }: FtrProviderContext): void => { es, ruleWithLegacyField.id ); - expect(isInvestigationFieldForRuleWithLegacyFieldMigratedInSo).to.eql(false); + expect(isInvestigationFieldForRuleWithLegacyFieldMigratedInSo).toBeFalsy(); const isInvestigationFieldForRuleWithEmptyArraydMigratedInSo = await checkInvestigationFieldSoValue( @@ -764,7 +861,7 @@ export default ({ getService }: FtrProviderContext): void => { es, ruleWithEmptyArray.id ); - expect(isInvestigationFieldForRuleWithEmptyArraydMigratedInSo).to.eql(false); + expect(isInvestigationFieldForRuleWithEmptyArraydMigratedInSo).toBeFalsy(); const isInvestigationFieldForRuleWithIntendedTypeMigratedInSo = await checkInvestigationFieldSoValue( @@ -773,10 +870,30 @@ export default ({ getService }: FtrProviderContext): void => { es, ruleWithIntendedType.id ); - expect(isInvestigationFieldForRuleWithIntendedTypeMigratedInSo).to.eql(true); + expect(isInvestigationFieldForRuleWithIntendedTypeMigratedInSo).toBeTruthy(); }); it('should duplicate rules with legacy investigation fields and transform field in response', async () => { + const [ + ruleWithLegacyInvestigationField, + ruleWithLegacyInvestigationFieldEmptyArray, + ruleWithIntendedInvestigationField, + ] = await Promise.all([ + createRuleThroughAlertingEndpoint( + supertest, + getRuleSavedObjectWithLegacyInvestigationFields() + ), + createRuleThroughAlertingEndpoint( + supertest, + getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray() + ), + createRule(supertest, log, { + ...getCustomQueryRuleParams({ rule_id: 'rule-with-investigation-field' }), + name: 'Test investigation fields object', + investigation_fields: { field_names: ['host.name'] }, + }), + ]); + const { body } = await securitySolutionApi .performRulesBulkAction({ body: { @@ -788,22 +905,22 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 3, total: 3 }); + expect(body.attributes.summary).toEqual({ failed: 0, skipped: 0, succeeded: 3, total: 3 }); // Check that the duplicated rule is returned with the response const names = body.attributes.results.created.map( (returnedRule: RuleResponse) => returnedRule.name ); - expect(names.includes('Test investigation fields [Duplicate]')).to.eql(true); - expect(names.includes('Test investigation fields empty array [Duplicate]')).to.eql(true); - expect(names.includes('Test investigation fields object [Duplicate]')).to.eql(true); + expect(names.includes('Test investigation fields [Duplicate]')).toBeTruthy(); + expect(names.includes('Test investigation fields empty array [Duplicate]')).toBeTruthy(); + expect(names.includes('Test investigation fields object [Duplicate]')).toBeTruthy(); // Check that the updates have been persisted const { body: rulesResponse } = await await securitySolutionApi .findRules({ query: {} }) .expect(200); - expect(rulesResponse.total).to.eql(6); + expect(rulesResponse.total).toBe(6); const ruleWithLegacyField = body.attributes.results.created.find( (returnedRule: RuleResponse) => @@ -832,7 +949,7 @@ export default ({ getService }: FtrProviderContext): void => { es, ruleWithLegacyField.id ); - expect(isInvestigationFieldForRuleWithLegacyFieldMigratedInSo).to.eql(false); + expect(isInvestigationFieldForRuleWithLegacyFieldMigratedInSo).toBeFalsy(); const isInvestigationFieldForRuleWithEmptyArrayMigratedInSo = await checkInvestigationFieldSoValue( @@ -841,7 +958,7 @@ export default ({ getService }: FtrProviderContext): void => { es, ruleWithEmptyArray.id ); - expect(isInvestigationFieldForRuleWithEmptyArrayMigratedInSo).to.eql(false); + expect(isInvestigationFieldForRuleWithEmptyArrayMigratedInSo).toBeFalsy(); /* It's duplicate of a rule with properly formatted "investigation fields". @@ -855,7 +972,7 @@ export default ({ getService }: FtrProviderContext): void => { es, ruleWithIntendedType.id ); - expect(isInvestigationFieldForRuleWithIntendedTypeInSo).to.eql(true); + expect(isInvestigationFieldForRuleWithIntendedTypeInSo).toBeTruthy(); // ORIGINAL RULES - rules selected to be duplicated /** @@ -871,7 +988,7 @@ export default ({ getService }: FtrProviderContext): void => { es, ruleWithLegacyInvestigationField.id ); - expect(isInvestigationFieldForOriginalRuleWithLegacyFieldMigratedInSo).to.eql(false); + expect(isInvestigationFieldForOriginalRuleWithLegacyFieldMigratedInSo).toBeFalsy(); const isInvestigationFieldForOriginalRuleWithEmptyArrayMigratedInSo = await checkInvestigationFieldSoValue( @@ -880,7 +997,7 @@ export default ({ getService }: FtrProviderContext): void => { es, ruleWithLegacyInvestigationFieldEmptyArray.id ); - expect(isInvestigationFieldForOriginalRuleWithEmptyArrayMigratedInSo).to.eql(false); + expect(isInvestigationFieldForOriginalRuleWithEmptyArrayMigratedInSo).toBeFalsy(); /* Since this rule was created with intended "investigation fields" format, @@ -893,10 +1010,27 @@ export default ({ getService }: FtrProviderContext): void => { es, ruleWithIntendedInvestigationField.id ); - expect(isInvestigationFieldForOriginalRuleWithIntendedTypeInSo).to.eql(true); + expect(isInvestigationFieldForOriginalRuleWithIntendedTypeInSo).toBeTruthy(); }); it('should edit rules with legacy investigation fields', async () => { + const [ruleWithLegacyInvestigationField, ruleWithLegacyInvestigationFieldEmptyArray] = + await Promise.all([ + createRuleThroughAlertingEndpoint( + supertest, + getRuleSavedObjectWithLegacyInvestigationFields() + ), + createRuleThroughAlertingEndpoint( + supertest, + getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray() + ), + createRule(supertest, log, { + ...getCustomQueryRuleParams({ rule_id: 'rule-with-investigation-field' }), + name: 'Test investigation fields object', + investigation_fields: { field_names: ['host.name'] }, + }), + ]); + const { body } = await securitySolutionApi.performRulesBulkAction({ body: { query: '', @@ -910,7 +1044,7 @@ export default ({ getService }: FtrProviderContext): void => { }, query: {}, }); - expect(body.attributes.summary).to.eql({ + expect(body.attributes.summary).toEqual({ failed: 0, skipped: 0, succeeded: 3, @@ -923,23 +1057,23 @@ export default ({ getService }: FtrProviderContext): void => { (returnedRule: RuleResponse) => returnedRule.rule_id === ruleWithLegacyInvestigationField.params.ruleId ); - expect(ruleWithLegacyField.investigation_fields).to.eql({ + expect(ruleWithLegacyField.investigation_fields).toEqual({ field_names: ['client.address', 'agent.name'], }); - expect(ruleWithLegacyField.tags).to.eql(['reset-tag']); + expect(ruleWithLegacyField.tags).toEqual(['reset-tag']); const ruleWithEmptyArray = body.attributes.results.updated.find( (returnedRule: RuleResponse) => returnedRule.rule_id === ruleWithLegacyInvestigationFieldEmptyArray.params.ruleId ); - expect(ruleWithEmptyArray.investigation_fields).to.eql(undefined); - expect(ruleWithEmptyArray.tags).to.eql(['reset-tag']); + expect(ruleWithEmptyArray.investigation_fields).toBeUndefined(); + expect(ruleWithEmptyArray.tags).toEqual(['reset-tag']); const ruleWithIntendedType = body.attributes.results.updated.find( (returnedRule: RuleResponse) => returnedRule.rule_id === 'rule-with-investigation-field' ); - expect(ruleWithIntendedType.investigation_fields).to.eql({ field_names: ['host.name'] }); - expect(ruleWithIntendedType.tags).to.eql(['reset-tag']); + expect(ruleWithIntendedType.investigation_fields).toEqual({ field_names: ['host.name'] }); + expect(ruleWithIntendedType.tags).toEqual(['reset-tag']); /** * Confirm type on SO so that it's clear in the tests whether it's expected that @@ -953,7 +1087,7 @@ export default ({ getService }: FtrProviderContext): void => { es, ruleWithLegacyInvestigationField.id ); - expect(isInvestigationFieldForRuleWithLegacyFieldMigratedInSo).to.eql(false); + expect(isInvestigationFieldForRuleWithLegacyFieldMigratedInSo).toBeFalsy(); const isInvestigationFieldForRuleWithEmptyArrayFieldMigratedInSo = await checkInvestigationFieldSoValue( @@ -962,7 +1096,7 @@ export default ({ getService }: FtrProviderContext): void => { es, ruleWithLegacyInvestigationFieldEmptyArray.id ); - expect(isInvestigationFieldForRuleWithEmptyArrayFieldMigratedInSo).to.eql(false); + expect(isInvestigationFieldForRuleWithEmptyArrayFieldMigratedInSo).toBeFalsy(); const isInvestigationFieldForRuleWithIntendedTypeMigratedInSo = await checkInvestigationFieldSoValue( @@ -971,7 +1105,7 @@ export default ({ getService }: FtrProviderContext): void => { es, ruleWithIntendedType.id ); - expect(isInvestigationFieldForRuleWithIntendedTypeMigratedInSo).to.eql(true); + expect(isInvestigationFieldForRuleWithIntendedTypeMigratedInSo).toBeTruthy(); }); }); }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/get_rule_with_legacy_investigation_fields.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/get_rule_with_legacy_investigation_fields.ts index 55056748a0945..9b315dbd9dbb5 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/get_rule_with_legacy_investigation_fields.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/get_rule_with_legacy_investigation_fields.ts @@ -7,7 +7,9 @@ import { InternalRuleCreate } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_schema'; -export const getRuleSavedObjectWithLegacyInvestigationFields = (): InternalRuleCreate => +export const getRuleSavedObjectWithLegacyInvestigationFields = ( + rewrites?: Partial +): InternalRuleCreate => ({ name: 'Test investigation fields', tags: ['migration'], @@ -54,6 +56,7 @@ export const getRuleSavedObjectWithLegacyInvestigationFields = (): InternalRuleC responseActions: undefined, alertSuppression: undefined, dataViewId: undefined, + ...rewrites?.params, }, schedule: { interval: '5m', @@ -61,11 +64,14 @@ export const getRuleSavedObjectWithLegacyInvestigationFields = (): InternalRuleC enabled: false, actions: [], throttle: null, + ...rewrites, // cast is due to alerting API expecting rule_type_id // and our internal schema expecting alertTypeId } as unknown as InternalRuleCreate); -export const getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray = (): InternalRuleCreate => +export const getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray = ( + rewrites?: Partial +): InternalRuleCreate => ({ name: 'Test investigation fields empty array', tags: ['migration'], @@ -112,6 +118,7 @@ export const getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray = (): Int responseActions: undefined, alertSuppression: undefined, dataViewId: undefined, + ...rewrites?.params, }, schedule: { interval: '5m', @@ -119,6 +126,7 @@ export const getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray = (): Int enabled: false, actions: [], throttle: null, + ...rewrites, // cast is due to alerting API expecting rule_type_id // and our internal schema expecting alertTypeId } as unknown as InternalRuleCreate);