diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml index f026499502e0d..7abd52a1ea153 100644 --- a/.buildkite/ftr_configs.yml +++ b/.buildkite/ftr_configs.yml @@ -457,7 +457,9 @@ enabled: - x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/exceptions/operators_data_types/ips_text_array/configs/ess.config.ts - x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_creation/configs/serverless.config.ts - x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_creation/configs/ess.config.ts - + - x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/actions/configs/serverless.config.ts + - x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/actions/configs/ess.config.ts + diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/index.ts index 2ce7684dedd48..1c9c874127660 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/index.ts @@ -15,8 +15,6 @@ export default ({ loadTestFile }: FtrProviderContext): void => { // existence being near 0. loadTestFile(require.resolve('./aliases')); - loadTestFile(require.resolve('./add_actions')); - loadTestFile(require.resolve('./update_actions')); loadTestFile(require.resolve('./check_privileges')); loadTestFile(require.resolve('./create_index')); loadTestFile(require.resolve('./preview_rules')); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/index.ts index 8844107744854..7822d11698c95 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/index.ts @@ -17,7 +17,6 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./get_rule_execution_results')); loadTestFile(require.resolve('./import_rules')); loadTestFile(require.resolve('./import_export_rules')); - loadTestFile(require.resolve('./legacy_actions_migrations')); loadTestFile(require.resolve('./read_rules')); loadTestFile(require.resolve('./resolve_read_rules')); loadTestFile(require.resolve('./update_rules')); @@ -36,7 +35,6 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./runtime')); loadTestFile(require.resolve('./throttle')); loadTestFile(require.resolve('./ignore_fields')); - loadTestFile(require.resolve('./migrations')); loadTestFile(require.resolve('./risk_engine/init_and_status_apis')); loadTestFile(require.resolve('./risk_engine/risk_score_preview')); loadTestFile(require.resolve('./risk_engine/risk_score_calculation')); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/legacy_actions_migrations.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/legacy_actions_migrations.ts deleted file mode 100644 index 236bf6991c9d6..0000000000000 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/legacy_actions_migrations.ts +++ /dev/null @@ -1,354 +0,0 @@ -/* - * 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 '@kbn/expect'; - -import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { - getLegacyActionSOById, - getLegacyActionNotificationSOById, - getRuleSOById, -} from '../../utils'; - -/** - * @deprecated Once the legacy notification system is removed, remove this test too. - */ -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext) => { - const supertest = getService('supertest'); - const es = getService('es'); - const esArchiver = getService('esArchiver'); - - // This test suite is not meant to test a specific route, but to test the legacy action migration - // code that lives in multiple routes. This code is also tested in each of the routes it lives in - // but not in as much detail and relying on mocks. This test loads an es_archive containing rules - // created in 7.15 with legacy actions. - // For new routes that do any updates on a rule, please ensure that you are including the legacy - // action migration code. We are monitoring legacy action telemetry to clean up once we see their - // existence being near 0. - describe('migrate_legacy_actions', () => { - before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/security_solution/legacy_actions'); - }); - - after(async () => { - await esArchiver.unload( - 'x-pack/test/functional/es_archives/security_solution/legacy_actions' - ); - }); - - it('migrates legacy actions for rule with no actions', async () => { - const soId = '9095ee90-b075-11ec-bb3f-1f063f8e06cf'; - const ruleId = '2297be91-894c-4831-830f-b424a0ec84f0'; - const legacySidecarId = '926668d0-b075-11ec-bb3f-1f063f8e06cf'; - - // check for legacy sidecar action - const sidecarActionSO = await getLegacyActionSOById(es, legacySidecarId); - expect(sidecarActionSO.hits.hits.length).to.eql(1); - - // check for legacy notification SO - // should not have been created for a rule with no actions - const legacyNotificationSO = await getLegacyActionNotificationSOById(es, soId); - expect(legacyNotificationSO.hits.hits.length).to.eql(0); - - // patch enable the rule - // any route that edits the rule should trigger the migration - await supertest - .patch(DETECTION_ENGINE_RULES_URL) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') - .send({ rule_id: ruleId, enabled: false }) - .expect(200); - - const { - hits: { - hits: [{ _source: ruleSO }], - }, - } = await getRuleSOById(es, soId); - - // Sidecar should be removed - const sidecarActionsSOAfterMigration = await getLegacyActionSOById(es, legacySidecarId); - expect(sidecarActionsSOAfterMigration.hits.hits.length).to.eql(0); - - expect(ruleSO?.alert.actions).to.eql([]); - expect(ruleSO?.alert.throttle).to.eql(null); - expect(ruleSO?.alert.notifyWhen).to.eql(null); - }); - - it('migrates legacy actions for rule with action run on every run', async () => { - const soId = 'dc6595f0-b075-11ec-bb3f-1f063f8e06cf'; - const ruleId = '72a0d429-363b-4f70-905e-c6019a224d40'; - const legacySidecarId = 'dde13970-b075-11ec-bb3f-1f063f8e06cf'; - - // check for legacy sidecar action - const sidecarActionSO = await getLegacyActionSOById(es, legacySidecarId); - expect(sidecarActionSO.hits.hits.length).to.eql(1); - - // check for legacy notification SO - // should not have been created for a rule that runs on every rule run - const legacyNotificationSO = await getLegacyActionNotificationSOById(es, soId); - expect(legacyNotificationSO.hits.hits.length).to.eql(0); - - // patch enable the rule - // any route that edits the rule should trigger the migration - await supertest - .patch(DETECTION_ENGINE_RULES_URL) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') - .send({ rule_id: ruleId, enabled: false }) - .expect(200); - - const { - hits: { - hits: [{ _source: ruleSO }], - }, - } = await getRuleSOById(es, soId); - - // Sidecar should be removed - const sidecarActionsSOAfterMigration = await getLegacyActionSOById(es, legacySidecarId); - expect(sidecarActionsSOAfterMigration.hits.hits.length).to.eql(0); - - expect(ruleSO?.alert.actions).to.eql([ - { - actionRef: 'action_0', - actionTypeId: '.email', - group: 'default', - params: { - message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', - subject: 'Test Actions', - to: ['test@test.com'], - }, - uuid: ruleSO?.alert.actions[0].uuid, - frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, - }, - { - actionRef: 'action_1', - actionTypeId: '.email', - group: 'default', - params: { - message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', - subject: 'Test Actions', - to: ['test@test.com'], - }, - uuid: ruleSO?.alert.actions[1].uuid, - frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, - }, - ]); - expect(ruleSO?.alert.throttle).to.eql(null); - expect(ruleSO?.alert.notifyWhen).to.eql(null); - expect(ruleSO?.references).to.eql([ - { - id: 'c95cb100-b075-11ec-bb3f-1f063f8e06cf', - name: 'action_0', - type: 'action', - }, - { - id: 'c95cb100-b075-11ec-bb3f-1f063f8e06cf', - name: 'action_1', - type: 'action', - }, - ]); - }); - - it('migrates legacy actions for rule with action run hourly', async () => { - const soId = '064e3160-b076-11ec-bb3f-1f063f8e06cf'; - const ruleId = '4c056b05-75ac-4209-be32-82100f771eb4'; - const legacySidecarId = '07aa8d10-b076-11ec-bb3f-1f063f8e06cf'; - - // check for legacy sidecar action - const sidecarActionSO = await getLegacyActionSOById(es, legacySidecarId); - expect(sidecarActionSO.hits.hits.length).to.eql(1); - - // check for legacy notification SO - const legacyNotificationSO = await getLegacyActionNotificationSOById(es, soId); - expect(legacyNotificationSO.hits.hits.length).to.eql(1); - - // patch enable the rule - // any route that edits the rule should trigger the migration - await supertest - .patch(DETECTION_ENGINE_RULES_URL) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') - .send({ rule_id: ruleId, enabled: false }) - .expect(200); - - const { - hits: { - hits: [{ _source: ruleSO }], - }, - } = await getRuleSOById(es, soId); - - // Sidecar should be removed - const sidecarActionsSOAfterMigration = await getLegacyActionSOById(es, legacySidecarId); - expect(sidecarActionsSOAfterMigration.hits.hits.length).to.eql(0); - - // Legacy notification should be removed - const legacyNotificationSOAfterMigration = await getLegacyActionNotificationSOById(es, soId); - expect(legacyNotificationSOAfterMigration.hits.hits.length).to.eql(0); - - expect(ruleSO?.alert.actions).to.eql([ - { - actionTypeId: '.email', - params: { - subject: 'Rule email', - to: ['test@test.com'], - message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', - }, - actionRef: 'action_0', - group: 'default', - uuid: ruleSO?.alert.actions[0].uuid, - frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, - }, - { - actionTypeId: '.slack', - params: { - message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', - }, - actionRef: 'action_1', - group: 'default', - uuid: ruleSO?.alert.actions[1].uuid, - frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, - }, - ]); - expect(ruleSO?.alert.throttle).to.eql(undefined); - expect(ruleSO?.alert.notifyWhen).to.eql(null); - expect(ruleSO?.references).to.eql([ - { - id: 'c95cb100-b075-11ec-bb3f-1f063f8e06cf', - name: 'action_0', - type: 'action', - }, - { - id: '207fa0e0-c04e-11ec-8a52-4fb92379525a', - name: 'action_1', - type: 'action', - }, - ]); - }); - - it('migrates legacy actions for rule with action run daily', async () => { - const soId = '27639570-b076-11ec-bb3f-1f063f8e06cf'; - const ruleId = '8e2c8550-f13f-4e21-be0c-92148d71a5f1'; - const legacySidecarId = '291ae260-b076-11ec-bb3f-1f063f8e06cf'; - - // check for legacy sidecar action - const sidecarActionSO = await getLegacyActionSOById(es, legacySidecarId); - expect(sidecarActionSO.hits.hits.length).to.eql(1); - - // check for legacy notification SO - const legacyNotificationSO = await getLegacyActionNotificationSOById(es, soId); - expect(legacyNotificationSO.hits.hits.length).to.eql(1); - - // patch enable the rule - await supertest - .patch(DETECTION_ENGINE_RULES_URL) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') - .send({ rule_id: ruleId, enabled: false }) - .expect(200); - - const { - hits: { - hits: [{ _source: ruleSO }], - }, - } = await getRuleSOById(es, soId); - - // Sidecar should be removed - const sidecarActionsSOAfterMigration = await getLegacyActionSOById(es, legacySidecarId); - expect(sidecarActionsSOAfterMigration.hits.hits.length).to.eql(0); - - // Legacy notification should be removed - const legacyNotificationSOAfterMigration = await getLegacyActionNotificationSOById(es, soId); - expect(legacyNotificationSOAfterMigration.hits.hits.length).to.eql(0); - - expect(ruleSO?.alert.actions).to.eql([ - { - actionRef: 'action_0', - actionTypeId: '.email', - group: 'default', - params: { - message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', - subject: 'Test Actions', - to: ['test@test.com'], - }, - uuid: ruleSO?.alert.actions[0].uuid, - frequency: { summary: true, throttle: '1d', notifyWhen: 'onThrottleInterval' }, - }, - ]); - expect(ruleSO?.alert.throttle).to.eql(undefined); - expect(ruleSO?.alert.notifyWhen).to.eql(null); - expect(ruleSO?.references).to.eql([ - { - id: 'c95cb100-b075-11ec-bb3f-1f063f8e06cf', - name: 'action_0', - type: 'action', - }, - ]); - }); - - it('migrates legacy actions for rule with action run weekly', async () => { - const soId = '61ec7a40-b076-11ec-bb3f-1f063f8e06cf'; - const ruleId = '05fbdd2a-e802-420b-bdc3-95ae0acca454'; - const legacySidecarId = '63aa2fd0-b076-11ec-bb3f-1f063f8e06cf'; - - // check for legacy sidecar action - const sidecarActionSO = await getLegacyActionSOById(es, legacySidecarId); - expect(sidecarActionSO.hits.hits.length).to.eql(1); - - // check for legacy notification SO - const legacyNotificationSO = await getLegacyActionNotificationSOById(es, soId); - expect(legacyNotificationSO.hits.hits.length).to.eql(1); - - // patch enable the rule - await supertest - .patch(DETECTION_ENGINE_RULES_URL) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') - .send({ rule_id: ruleId, enabled: false }) - .expect(200); - - const { - hits: { - hits: [{ _source: ruleSO }], - }, - } = await getRuleSOById(es, soId); - - // Sidecar should be removed - const sidecarActionsSOAfterMigration = await getLegacyActionSOById(es, legacySidecarId); - expect(sidecarActionsSOAfterMigration.hits.hits.length).to.eql(0); - - // Legacy notification should be removed - const legacyNotificationSOAfterMigration = await getLegacyActionNotificationSOById(es, soId); - expect(legacyNotificationSOAfterMigration.hits.hits.length).to.eql(0); - - expect(ruleSO?.alert.actions).to.eql([ - { - actionRef: 'action_0', - actionTypeId: '.email', - group: 'default', - params: { - message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', - subject: 'Test Actions', - to: ['test@test.com'], - }, - uuid: ruleSO?.alert.actions[0].uuid, - frequency: { summary: true, throttle: '7d', notifyWhen: 'onThrottleInterval' }, - }, - ]); - expect(ruleSO?.alert.throttle).to.eql(undefined); - expect(ruleSO?.alert.notifyWhen).to.eql(null); - expect(ruleSO?.references).to.eql([ - { - id: 'c95cb100-b075-11ec-bb3f-1f063f8e06cf', - name: 'action_0', - type: 'action', - }, - ]); - }); - }); -}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/migrations.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/migrations.ts deleted file mode 100644 index d43ecaff6823e..0000000000000 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/migrations.ts +++ /dev/null @@ -1,102 +0,0 @@ -/* - * 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 { SECURITY_SOLUTION_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server'; -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext): void => { - const esArchiver = getService('esArchiver'); - const es = getService('es'); - - describe('migrations', () => { - before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/security_solution/migrations'); - }); - - after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/security_solution/migrations'); - }); - - describe('7.16.0', () => { - it('migrates legacy siem-detection-engine-rule-actions to use saved object references', async () => { - const response = await es.get<{ - 'siem-detection-engine-rule-actions': { - ruleAlertId: string; - actions: [{ id: string; actionRef: string }]; - }; - references: [{}]; - }>( - { - index: SECURITY_SOLUTION_SAVED_OBJECT_INDEX, - id: 'siem-detection-engine-rule-actions:fce024a0-0452-11ec-9b15-d13d79d162f3', - }, - { - meta: true, - } - ); - expect(response.statusCode).to.eql(200); - - // references exist and are expected values - expect(response.body._source?.references).to.eql([ - { - name: 'alert_0', - id: 'fb1046a0-0452-11ec-9b15-d13d79d162f3', - type: 'alert', - }, - { - name: 'action_0', - id: 'f6e64c00-0452-11ec-9b15-d13d79d162f3', - type: 'action', - }, - ]); - - // actionRef exists and is the expected value - expect( - response.body._source?.['siem-detection-engine-rule-actions'].actions[0].actionRef - ).to.eql('action_0'); - - // ruleAlertId no longer exist - expect(response.body._source?.['siem-detection-engine-rule-actions'].ruleAlertId).to.eql( - undefined - ); - - // actions.id no longer exist - expect(response.body._source?.['siem-detection-engine-rule-actions'].actions[0].id).to.eql( - undefined - ); - }); - - it('migrates legacy siem-detection-engine-rule-actions and retains "ruleThrottle" and "alertThrottle" as the same attributes as before', async () => { - const response = await es.get<{ - 'siem-detection-engine-rule-actions': { - ruleThrottle: string; - alertThrottle: string; - }; - }>( - { - index: SECURITY_SOLUTION_SAVED_OBJECT_INDEX, - id: 'siem-detection-engine-rule-actions:fce024a0-0452-11ec-9b15-d13d79d162f3', - }, - { - meta: true, - } - ); - expect(response.statusCode).to.eql(200); - - // "alertThrottle" and "ruleThrottle" should still exist - expect(response.body._source?.['siem-detection-engine-rule-actions'].alertThrottle).to.eql( - '7d' - ); - expect(response.body._source?.['siem-detection-engine-rule-actions'].ruleThrottle).to.eql( - '7d' - ); - }); - }); - }); -}; diff --git a/x-pack/test/security_solution_api_integration/config/serverless/config.base.ts b/x-pack/test/security_solution_api_integration/config/serverless/config.base.ts index 105701ec61e08..6790c39c85115 100644 --- a/x-pack/test/security_solution_api_integration/config/serverless/config.base.ts +++ b/x-pack/test/security_solution_api_integration/config/serverless/config.base.ts @@ -5,8 +5,6 @@ * 2.0. */ import { FtrConfigProviderContext } from '@kbn/test'; -// import { ES_RESOURCES } from '@kbn/security-solution-plugin/scripts/endpoint/common/roles_users/serverless'; - export interface CreateTestConfigOptions { testFiles: string[]; junit: { reportName: string }; @@ -24,9 +22,6 @@ export function createTestConfig(options: CreateTestConfigOptions) { ...svlSharedConfig.get('kbnTestServer'), serverArgs: [...svlSharedConfig.get('kbnTestServer.serverArgs'), '--serverless=security'], }, - // esServerlessOptions: { - // resources: Object.values(ES_RESOURCES), - // }, testFiles: options.testFiles, junit: options.junit, diff --git a/x-pack/test/security_solution_api_integration/package.json b/x-pack/test/security_solution_api_integration/package.json index 4562cfc82cfc9..305db2251e0b1 100644 --- a/x-pack/test/security_solution_api_integration/package.json +++ b/x-pack/test/security_solution_api_integration/package.json @@ -31,6 +31,11 @@ "rule_creation:runner:serverless": "npm run run-tests rule_creation serverless serverlessEnv", "rule_creation:qa:serverless": "npm run run-tests rule_creation serverless qaEnv", "rule_creation:server:ess": "npm run initialize-server rule_creation ess", - "rule_creation:runner:ess": "npm run run-tests rule_creation ess essEnv" + "rule_creation:runner:ess": "npm run run-tests rule_creation ess essEnv", + "actions:server:serverless": "npm run initialize-server actions serverless", + "actions:runner:serverless": "npm run run-tests actions serverless serverlessEnv", + "actions:qa:serverless": "npm run run-tests actions serverless qaEnv", + "actions:server:ess": "npm run initialize-server actions ess", + "actions:runner:ess": "npm run run-tests actions ess essEnv" } } diff --git a/x-pack/test/security_solution_api_integration/scripts/index.js b/x-pack/test/security_solution_api_integration/scripts/index.js index 635c135e2c8b1..af12482f9f293 100644 --- a/x-pack/test/security_solution_api_integration/scripts/index.js +++ b/x-pack/test/security_solution_api_integration/scripts/index.js @@ -31,15 +31,15 @@ let grepArgs = []; if (type !== 'server') { switch (environment) { case 'serverlessEnv': - grepArgs = ['--grep', '@serverless', '--grep', '@brokenInServerless', '--invert']; + grepArgs = ['--grep', '/^(?!.*@brokenInServerless).*@serverless.*/']; break; case 'essEnv': - grepArgs = ['--grep', '@ess']; + grepArgs = ['--grep', '/^(?!.*@brokenInEss).*@ess.*/']; break; case 'qaEnv': - grepArgs = ['--grep', '@serverless', '--grep', '@brokenInServerless|@skipInQA', '--invert']; + grepArgs = ['--grep', '/^(?!.*@brokenInServerless|.*@skipInQA).*@serverless.*/']; break; default: diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/add_actions.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/actions/add_actions.ts similarity index 71% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/group1/add_actions.ts rename to x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/actions/add_actions.ts index 2bfd3c6102e61..257378f7442b9 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/add_actions.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/actions/add_actions.ts @@ -8,9 +8,8 @@ import expect from '@kbn/expect'; import { RuleCreateProps } from '@kbn/security-solution-plugin/common/api/detection_engine'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; import { - createSignalsIndex, + createAlertsIndex, deleteAllRules, removeServerGeneratedProperties, getWebHookAction, @@ -19,27 +18,35 @@ import { waitForRuleSuccess, createRule, deleteAllAlerts, + updateUsername, } from '../../utils'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { EsArchivePathBuilder } from '../../../../es_archive_path_builder'; -// eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); const log = getService('log'); const es = getService('es'); + // TODO: add a new service + const config = getService('config'); + const ELASTICSEARCH_USERNAME = config.get('servers.kibana.username'); + const isServerless = config.get('serverless'); + const dataPathBuilder = new EsArchivePathBuilder(isServerless); + const path = dataPathBuilder.getPath('auditbeat/hosts'); - describe('add_actions', () => { + describe('@serverless @ess add_actions', () => { describe('adding actions', () => { before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts'); + await esArchiver.load(path); }); after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/auditbeat/hosts'); + await esArchiver.unload(path); }); beforeEach(async () => { - await createSignalsIndex(supertest, log); + await createAlertsIndex(supertest, log); }); afterEach(async () => { @@ -52,17 +59,18 @@ export default ({ getService }: FtrProviderContext) => { const { body: hookAction } = await supertest .post('/api/actions/action') .set('kbn-xsrf', 'true') + .set('x-elastic-internal-origin', 'foo') .send(getWebHookAction()) .expect(200); const rule = await createRule(supertest, log, getRuleWithWebHookAction(hookAction.id)); const bodyToCompare = removeServerGeneratedProperties(rule); - expect(bodyToCompare).to.eql( - getSimpleRuleOutputWithWebHookAction( - `${bodyToCompare?.actions?.[0].id}`, - `${bodyToCompare?.actions?.[0].uuid}` - ) + const expected = getSimpleRuleOutputWithWebHookAction( + `${bodyToCompare?.actions?.[0].id}`, + `${bodyToCompare?.actions?.[0].uuid}` ); + const expectedRuleWithUserUpdated = updateUsername(expected, ELASTICSEARCH_USERNAME); + expect(bodyToCompare).to.eql(expectedRuleWithUserUpdated); }); it('should be able to create a new webhook action and attach it to a rule without a meta field and run it correctly', async () => { @@ -70,6 +78,7 @@ export default ({ getService }: FtrProviderContext) => { const { body: hookAction } = await supertest .post('/api/actions/action') .set('kbn-xsrf', 'true') + .set('x-elastic-internal-origin', 'foo') .send(getWebHookAction()) .expect(200); @@ -86,6 +95,7 @@ export default ({ getService }: FtrProviderContext) => { const { body: hookAction } = await supertest .post('/api/actions/action') .set('kbn-xsrf', 'true') + .set('x-elastic-internal-origin', 'foo') .send(getWebHookAction()) .expect(200); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/actions/configs/ess.config.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/actions/configs/ess.config.ts new file mode 100644 index 0000000000000..cec8d1cca41b5 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/actions/configs/ess.config.ts @@ -0,0 +1,22 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile( + require.resolve('../../../../../config/ess/config.base.trial') + ); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('..')], + junit: { + reportName: 'Detection Engine ESS/Actions API Integration Tests', + }, + }; +} diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/actions/configs/serverless.config.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/actions/configs/serverless.config.ts new file mode 100644 index 0000000000000..66edc0eef7f30 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/actions/configs/serverless.config.ts @@ -0,0 +1,15 @@ +/* + * 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 { createTestConfig } from '../../../../../config/serverless/config.base'; + +export default createTestConfig({ + testFiles: [require.resolve('..')], + junit: { + reportName: 'Detection Engine Serverless/Actions API Integration Tests', + }, +}); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/actions/index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/actions/index.ts new file mode 100644 index 0000000000000..5c26d445eb158 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/actions/index.ts @@ -0,0 +1,15 @@ +/* + * 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 { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Actions API', function () { + loadTestFile(require.resolve('./add_actions')); + loadTestFile(require.resolve('./update_actions')); + loadTestFile(require.resolve('./migrations')); + }); +} diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/actions/migrations.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/actions/migrations.ts new file mode 100644 index 0000000000000..ce5c87d2c3fb4 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/actions/migrations.ts @@ -0,0 +1,451 @@ +/* + * 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 '@kbn/expect'; +import { SECURITY_SOLUTION_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server'; + +import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; +import { + getLegacyActionSOById, + getLegacyActionNotificationSOById, + getRuleSOById, +} from '../../../../../detection_engine_api_integration/utils'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +/** + * @deprecated Once the legacy notification system is removed, remove this test too. + */ +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const es = getService('es'); + const esArchiver = getService('esArchiver'); + + describe('@ess @skipInQA actions migrations', () => { + // This test suite is not meant to test a specific route, but to test the legacy action migration + // code that lives in multiple routes. This code is also tested in each of the routes it lives in + // but not in as much detail and relying on mocks. This test loads an es_archive containing rules + // created in 7.15 with legacy actions. + // For new routes that do any updates on a rule, please ensure that you are including the legacy + // action migration code. We are monitoring legacy action telemetry to clean up once we see their + // existence being near 0. + describe('legacy actions', () => { + before(async () => { + await esArchiver.load( + 'x-pack/test/functional/es_archives/security_solution/legacy_actions' + ); + }); + + after(async () => { + await esArchiver.unload( + 'x-pack/test/functional/es_archives/security_solution/legacy_actions' + ); + }); + + it('migrates legacy actions for rule with no actions', async () => { + const soId = '9095ee90-b075-11ec-bb3f-1f063f8e06cf'; + const ruleId = '2297be91-894c-4831-830f-b424a0ec84f0'; + const legacySidecarId = '926668d0-b075-11ec-bb3f-1f063f8e06cf'; + + // check for legacy sidecar action + const sidecarActionSO = await getLegacyActionSOById(es, legacySidecarId); + expect(sidecarActionSO.hits.hits.length).to.eql(1); + + // check for legacy notification SO + // should not have been created for a rule with no actions + const legacyNotificationSO = await getLegacyActionNotificationSOById(es, soId); + expect(legacyNotificationSO.hits.hits.length).to.eql(0); + + // patch enable the rule + // any route that edits the rule should trigger the migration + await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '2023-10-31') + .send({ rule_id: ruleId, enabled: false }) + .expect(200); + + const { + hits: { + hits: [{ _source: ruleSO }], + }, + } = await getRuleSOById(es, soId); + + // Sidecar should be removed + const sidecarActionsSOAfterMigration = await getLegacyActionSOById(es, legacySidecarId); + expect(sidecarActionsSOAfterMigration.hits.hits.length).to.eql(0); + + expect(ruleSO?.alert.actions).to.eql([]); + expect(ruleSO?.alert.throttle).to.eql(null); + expect(ruleSO?.alert.notifyWhen).to.eql(null); + }); + + it('migrates legacy actions for rule with action run on every run', async () => { + const soId = 'dc6595f0-b075-11ec-bb3f-1f063f8e06cf'; + const ruleId = '72a0d429-363b-4f70-905e-c6019a224d40'; + const legacySidecarId = 'dde13970-b075-11ec-bb3f-1f063f8e06cf'; + + // check for legacy sidecar action + const sidecarActionSO = await getLegacyActionSOById(es, legacySidecarId); + expect(sidecarActionSO.hits.hits.length).to.eql(1); + + // check for legacy notification SO + // should not have been created for a rule that runs on every rule run + const legacyNotificationSO = await getLegacyActionNotificationSOById(es, soId); + expect(legacyNotificationSO.hits.hits.length).to.eql(0); + + // patch enable the rule + // any route that edits the rule should trigger the migration + await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '2023-10-31') + .send({ rule_id: ruleId, enabled: false }) + .expect(200); + + const { + hits: { + hits: [{ _source: ruleSO }], + }, + } = await getRuleSOById(es, soId); + + // Sidecar should be removed + const sidecarActionsSOAfterMigration = await getLegacyActionSOById(es, legacySidecarId); + expect(sidecarActionsSOAfterMigration.hits.hits.length).to.eql(0); + + expect(ruleSO?.alert.actions).to.eql([ + { + actionRef: 'action_0', + actionTypeId: '.email', + group: 'default', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + subject: 'Test Actions', + to: ['test@test.com'], + }, + uuid: ruleSO?.alert.actions[0].uuid, + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, + }, + { + actionRef: 'action_1', + actionTypeId: '.email', + group: 'default', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + subject: 'Test Actions', + to: ['test@test.com'], + }, + uuid: ruleSO?.alert.actions[1].uuid, + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, + }, + ]); + expect(ruleSO?.alert.throttle).to.eql(null); + expect(ruleSO?.alert.notifyWhen).to.eql(null); + expect(ruleSO?.references).to.eql([ + { + id: 'c95cb100-b075-11ec-bb3f-1f063f8e06cf', + name: 'action_0', + type: 'action', + }, + { + id: 'c95cb100-b075-11ec-bb3f-1f063f8e06cf', + name: 'action_1', + type: 'action', + }, + ]); + }); + + it('migrates legacy actions for rule with action run hourly', async () => { + const soId = '064e3160-b076-11ec-bb3f-1f063f8e06cf'; + const ruleId = '4c056b05-75ac-4209-be32-82100f771eb4'; + const legacySidecarId = '07aa8d10-b076-11ec-bb3f-1f063f8e06cf'; + + // check for legacy sidecar action + const sidecarActionSO = await getLegacyActionSOById(es, legacySidecarId); + expect(sidecarActionSO.hits.hits.length).to.eql(1); + + // check for legacy notification SO + const legacyNotificationSO = await getLegacyActionNotificationSOById(es, soId); + expect(legacyNotificationSO.hits.hits.length).to.eql(1); + + // patch enable the rule + // any route that edits the rule should trigger the migration + await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '2023-10-31') + .send({ rule_id: ruleId, enabled: false }) + .expect(200); + + const { + hits: { + hits: [{ _source: ruleSO }], + }, + } = await getRuleSOById(es, soId); + + // Sidecar should be removed + const sidecarActionsSOAfterMigration = await getLegacyActionSOById(es, legacySidecarId); + expect(sidecarActionsSOAfterMigration.hits.hits.length).to.eql(0); + + // Legacy notification should be removed + const legacyNotificationSOAfterMigration = await getLegacyActionNotificationSOById( + es, + soId + ); + expect(legacyNotificationSOAfterMigration.hits.hits.length).to.eql(0); + + expect(ruleSO?.alert.actions).to.eql([ + { + actionTypeId: '.email', + params: { + subject: 'Rule email', + to: ['test@test.com'], + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + }, + actionRef: 'action_0', + group: 'default', + uuid: ruleSO?.alert.actions[0].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, + }, + { + actionTypeId: '.slack', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + }, + actionRef: 'action_1', + group: 'default', + uuid: ruleSO?.alert.actions[1].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, + }, + ]); + expect(ruleSO?.alert.throttle).to.eql(undefined); + expect(ruleSO?.alert.notifyWhen).to.eql(null); + expect(ruleSO?.references).to.eql([ + { + id: 'c95cb100-b075-11ec-bb3f-1f063f8e06cf', + name: 'action_0', + type: 'action', + }, + { + id: '207fa0e0-c04e-11ec-8a52-4fb92379525a', + name: 'action_1', + type: 'action', + }, + ]); + }); + + it('migrates legacy actions for rule with action run daily', async () => { + const soId = '27639570-b076-11ec-bb3f-1f063f8e06cf'; + const ruleId = '8e2c8550-f13f-4e21-be0c-92148d71a5f1'; + const legacySidecarId = '291ae260-b076-11ec-bb3f-1f063f8e06cf'; + + // check for legacy sidecar action + const sidecarActionSO = await getLegacyActionSOById(es, legacySidecarId); + expect(sidecarActionSO.hits.hits.length).to.eql(1); + + // check for legacy notification SO + const legacyNotificationSO = await getLegacyActionNotificationSOById(es, soId); + expect(legacyNotificationSO.hits.hits.length).to.eql(1); + + // patch enable the rule + await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '2023-10-31') + .send({ rule_id: ruleId, enabled: false }) + .expect(200); + + const { + hits: { + hits: [{ _source: ruleSO }], + }, + } = await getRuleSOById(es, soId); + + // Sidecar should be removed + const sidecarActionsSOAfterMigration = await getLegacyActionSOById(es, legacySidecarId); + expect(sidecarActionsSOAfterMigration.hits.hits.length).to.eql(0); + + // Legacy notification should be removed + const legacyNotificationSOAfterMigration = await getLegacyActionNotificationSOById( + es, + soId + ); + expect(legacyNotificationSOAfterMigration.hits.hits.length).to.eql(0); + + expect(ruleSO?.alert.actions).to.eql([ + { + actionRef: 'action_0', + actionTypeId: '.email', + group: 'default', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + subject: 'Test Actions', + to: ['test@test.com'], + }, + uuid: ruleSO?.alert.actions[0].uuid, + frequency: { summary: true, throttle: '1d', notifyWhen: 'onThrottleInterval' }, + }, + ]); + expect(ruleSO?.alert.throttle).to.eql(undefined); + expect(ruleSO?.alert.notifyWhen).to.eql(null); + expect(ruleSO?.references).to.eql([ + { + id: 'c95cb100-b075-11ec-bb3f-1f063f8e06cf', + name: 'action_0', + type: 'action', + }, + ]); + }); + + it('migrates legacy actions for rule with action run weekly', async () => { + const soId = '61ec7a40-b076-11ec-bb3f-1f063f8e06cf'; + const ruleId = '05fbdd2a-e802-420b-bdc3-95ae0acca454'; + const legacySidecarId = '63aa2fd0-b076-11ec-bb3f-1f063f8e06cf'; + + // check for legacy sidecar action + const sidecarActionSO = await getLegacyActionSOById(es, legacySidecarId); + expect(sidecarActionSO.hits.hits.length).to.eql(1); + + // check for legacy notification SO + const legacyNotificationSO = await getLegacyActionNotificationSOById(es, soId); + expect(legacyNotificationSO.hits.hits.length).to.eql(1); + + // patch enable the rule + await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '2023-10-31') + .send({ rule_id: ruleId, enabled: false }) + .expect(200); + + const { + hits: { + hits: [{ _source: ruleSO }], + }, + } = await getRuleSOById(es, soId); + + // Sidecar should be removed + const sidecarActionsSOAfterMigration = await getLegacyActionSOById(es, legacySidecarId); + expect(sidecarActionsSOAfterMigration.hits.hits.length).to.eql(0); + + // Legacy notification should be removed + const legacyNotificationSOAfterMigration = await getLegacyActionNotificationSOById( + es, + soId + ); + expect(legacyNotificationSOAfterMigration.hits.hits.length).to.eql(0); + + expect(ruleSO?.alert.actions).to.eql([ + { + actionRef: 'action_0', + actionTypeId: '.email', + group: 'default', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + subject: 'Test Actions', + to: ['test@test.com'], + }, + uuid: ruleSO?.alert.actions[0].uuid, + frequency: { summary: true, throttle: '7d', notifyWhen: 'onThrottleInterval' }, + }, + ]); + expect(ruleSO?.alert.throttle).to.eql(undefined); + expect(ruleSO?.alert.notifyWhen).to.eql(null); + expect(ruleSO?.references).to.eql([ + { + id: 'c95cb100-b075-11ec-bb3f-1f063f8e06cf', + name: 'action_0', + type: 'action', + }, + ]); + }); + }); + + describe('7.16.0', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/security_solution/migrations'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/security_solution/migrations'); + }); + + it('migrates legacy siem-detection-engine-rule-actions to use saved object references', async () => { + const response = await es.get<{ + 'siem-detection-engine-rule-actions': { + ruleAlertId: string; + actions: [{ id: string; actionRef: string }]; + }; + references: [{}]; + }>( + { + index: SECURITY_SOLUTION_SAVED_OBJECT_INDEX, + id: 'siem-detection-engine-rule-actions:fce024a0-0452-11ec-9b15-d13d79d162f3', + }, + { + meta: true, + } + ); + expect(response.statusCode).to.eql(200); + + // references exist and are expected values + expect(response.body._source?.references).to.eql([ + { + name: 'alert_0', + id: 'fb1046a0-0452-11ec-9b15-d13d79d162f3', + type: 'alert', + }, + { + name: 'action_0', + id: 'f6e64c00-0452-11ec-9b15-d13d79d162f3', + type: 'action', + }, + ]); + + // actionRef exists and is the expected value + expect( + response.body._source?.['siem-detection-engine-rule-actions'].actions[0].actionRef + ).to.eql('action_0'); + + // ruleAlertId no longer exist + expect(response.body._source?.['siem-detection-engine-rule-actions'].ruleAlertId).to.eql( + undefined + ); + + // actions.id no longer exist + expect(response.body._source?.['siem-detection-engine-rule-actions'].actions[0].id).to.eql( + undefined + ); + }); + + it('migrates legacy siem-detection-engine-rule-actions and retains "ruleThrottle" and "alertThrottle" as the same attributes as before', async () => { + const response = await es.get<{ + 'siem-detection-engine-rule-actions': { + ruleThrottle: string; + alertThrottle: string; + }; + }>( + { + index: SECURITY_SOLUTION_SAVED_OBJECT_INDEX, + id: 'siem-detection-engine-rule-actions:fce024a0-0452-11ec-9b15-d13d79d162f3', + }, + { + meta: true, + } + ); + expect(response.statusCode).to.eql(200); + + // "alertThrottle" and "ruleThrottle" should still exist + expect(response.body._source?.['siem-detection-engine-rule-actions'].alertThrottle).to.eql( + '7d' + ); + expect(response.body._source?.['siem-detection-engine-rule-actions'].ruleThrottle).to.eql( + '7d' + ); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/update_actions.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/actions/update_actions.ts similarity index 76% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/group1/update_actions.ts rename to x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/actions/update_actions.ts index 8390fc10ad7fc..f56d949eadd38 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/update_actions.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/actions/update_actions.ts @@ -10,9 +10,8 @@ import { omit } from 'lodash'; import { RuleCreateProps } from '@kbn/security-solution-plugin/common/api/detection_engine'; import { ELASTIC_SECURITY_RULE_ID } from '@kbn/security-solution-plugin/common'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; import { - createSignalsIndex, + createAlertsIndex, deleteAllRules, deleteAllAlerts, removeServerGeneratedProperties, @@ -29,32 +28,40 @@ import { getPrebuiltRulesAndTimelinesStatus, getSimpleRuleOutput, ruleToUpdateSchema, + updateUsername, } from '../../utils'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { EsArchivePathBuilder } from '../../../../es_archive_path_builder'; -// eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { const es = getService('es'); const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); const log = getService('log'); + // TODO: add a new service + const config = getService('config'); + const ELASTICSEARCH_USERNAME = config.get('servers.kibana.username'); + const isServerless = config.get('serverless'); + const dataPathBuilder = new EsArchivePathBuilder(isServerless); + const path = dataPathBuilder.getPath('auditbeat/hosts'); const getImmutableRule = async () => { await installMockPrebuiltRules(supertest, es); return getRule(supertest, log, ELASTIC_SECURITY_RULE_ID); }; - describe('update_actions', () => { + describe('@serverless @ess update_actions', () => { describe('updating actions', () => { before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts'); + await esArchiver.load(path); }); after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/auditbeat/hosts'); + await esArchiver.unload(path); }); beforeEach(async () => { - await createSignalsIndex(supertest, log); + await createAlertsIndex(supertest, log); }); afterEach(async () => { @@ -70,14 +77,16 @@ export default ({ getService }: FtrProviderContext) => { const updatedRule = await updateRule(supertest, log, ruleToUpdate); const bodyToCompare = removeServerGeneratedProperties(updatedRule); - const expected = { + const expectedRule = { ...getSimpleRuleOutputWithWebHookAction( `${bodyToCompare.actions?.[0].id}`, `${bodyToCompare.actions?.[0].uuid}` ), revision: 1, // revision bump is required since this is an updated rule and this is part of the testing that we do bump the revision number on update }; - expect(bodyToCompare).to.eql(expected); + const expectedRuleWithUserUpdated = updateUsername(expectedRule, ELASTICSEARCH_USERNAME); + + expect(bodyToCompare).to.eql(expectedRuleWithUserUpdated); }); it('should be able to add a new webhook action and then remove the action from the rule again', async () => { @@ -92,7 +101,8 @@ export default ({ getService }: FtrProviderContext) => { ...getSimpleRuleOutput(), revision: 2, // revision bump is required since this is an updated rule and this is part of the testing that we do bump the revision number on update }; - expect(bodyToCompare).to.eql(expected); + const expectedRuleWithUserUpdated = updateUsername(expected, ELASTICSEARCH_USERNAME); + expect(bodyToCompare).to.eql(expectedRuleWithUserUpdated); }); it('should be able to create a new webhook action and attach it to a rule without a meta field and run it correctly', async () => { @@ -104,7 +114,7 @@ export default ({ getService }: FtrProviderContext) => { await waitForRuleSuccess({ supertest, log, id: updatedRule.id }); }); - it('should be able to create a new webhook action and attach it to a rule with a meta field and run it correctly', async () => { + it('@skipInQA should be able to create a new webhook action and attach it to a rule with a meta field and run it correctly', async () => { const hookAction = await createNewAction(supertest, log); const rule = getSimpleRule(); await createRule(supertest, log, rule); @@ -116,7 +126,7 @@ export default ({ getService }: FtrProviderContext) => { await waitForRuleSuccess({ supertest, log, id: updatedRule.id }); }); - it('should not change properties of immutable rule when applying actions to it', async () => { + it('@skipInQA should not change properties of immutable rule when applying actions to it', async () => { // actions and throttle to be removed from assertion (it asserted in a separate test case) const actionsProps = ['actions', 'throttle']; @@ -139,7 +149,7 @@ export default ({ getService }: FtrProviderContext) => { expect(expected.immutable).to.be(true); // It should stay immutable true when returning }); - it('should be able to create a new webhook action and attach it to an immutable rule', async () => { + it('@skipInQA should be able to create a new webhook action and attach it to an immutable rule', async () => { const immutableRule = await getImmutableRule(); const hookAction = await createNewAction(supertest, log); const ruleToUpdate = getRuleWithWebHookAction( @@ -155,11 +165,12 @@ export default ({ getService }: FtrProviderContext) => { `${bodyToCompare.actions?.[0].uuid}` ); - expect(bodyToCompare.actions).to.eql(expected.actions); - expect(bodyToCompare.throttle).to.eql(expected.throttle); + const expectedRuleWithUserUpdated = updateUsername(expected, ELASTICSEARCH_USERNAME); + expect(bodyToCompare.actions).to.eql(expectedRuleWithUserUpdated.actions); + expect(bodyToCompare.throttle).to.eql(expectedRuleWithUserUpdated.throttle); }); - it('should be able to create a new webhook action, attach it to an immutable rule and the count of prepackaged rules should not increase. If this fails, suspect the immutable tags are not staying on the rule correctly.', async () => { + it('@skipInQA should be able to create a new webhook action, attach it to an immutable rule and the count of prepackaged rules should not increase. If this fails, suspect the immutable tags are not staying on the rule correctly.', async () => { const immutableRule = await getImmutableRule(); const hookAction = await createNewAction(supertest, log); const ruleToUpdate = getRuleWithWebHookAction( @@ -173,7 +184,7 @@ export default ({ getService }: FtrProviderContext) => { expect(status.rules_not_installed).to.eql(0); }); - it('should be able to create a new webhook action, attach it to an immutable rule and the rule should stay immutable when searching against immutable tags', async () => { + it('@skipInQA should be able to create a new webhook action, attach it to an immutable rule and the rule should stay immutable when searching against immutable tags', async () => { const immutableRule = await getImmutableRule(); const hookAction = await createNewAction(supertest, log); const ruleToUpdate = getRuleWithWebHookAction( @@ -190,8 +201,8 @@ export default ({ getService }: FtrProviderContext) => { `${bodyToCompare.actions?.[0].id}`, `${bodyToCompare.actions?.[0].uuid}` ); - - expect(bodyToCompare.actions).to.eql(expected.actions); + const expectedRuleWithUserUpdated = updateUsername(expected, ELASTICSEARCH_USERNAME); + expect(bodyToCompare.actions).to.eql(expectedRuleWithUserUpdated.actions); expect(bodyToCompare.immutable).to.be(true); }); }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/actions/create_new_action.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/actions/create_new_action.ts new file mode 100644 index 0000000000000..7cbe8e858a043 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/actions/create_new_action.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ToolingLog } from '@kbn/tooling-log'; +import type SuperTest from 'supertest'; + +import { getWebHookAction } from './get_web_hook_action'; + +/** + * Helper to cut down on the noise in some of the tests. This + * creates a new action and expects a 200 and does not do any retries. + * @param supertest The supertest deps + */ +export const createNewAction = async ( + supertest: SuperTest.SuperTest, + log: ToolingLog +) => { + const response = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'true') + .set('x-elastic-internal-origin', 'foo') + .send(getWebHookAction()); + if (response.status !== 200) { + log.error( + `Did not get an expected 200 "ok" when creating a new action. CI issues could happen. Suspect this line if you are seeing CI issues. body: ${JSON.stringify( + response.body + )}, status: ${JSON.stringify(response.status)}` + ); + } + return response.body; +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/actions/index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/actions/index.ts index 438d983a69e05..d9b65ba596dd6 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/actions/index.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/actions/index.ts @@ -7,3 +7,4 @@ export * from './get_slack_action'; export * from './get_web_hook_action'; export * from './remove_uuid_from_actions'; +export * from './create_new_action'; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/get_rule_with_web_hook_action.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/get_rule_with_web_hook_action.ts new file mode 100644 index 0000000000000..6437df274098d --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/get_rule_with_web_hook_action.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + RuleCreateProps, + RuleUpdateProps, +} from '@kbn/security-solution-plugin/common/api/detection_engine'; +import { getSimpleRule } from './get_simple_rule'; + +export const getRuleWithWebHookAction = ( + id: string, + enabled = false, + rule?: RuleCreateProps +): RuleCreateProps | RuleUpdateProps => { + const finalRule = rule != null ? { ...rule, enabled } : getSimpleRule('rule-1', enabled); + return { + ...finalRule, + throttle: 'rule', + actions: [ + { + group: 'default', + id, + params: { + body: '{}', + }, + action_type_id: '.webhook', + }, + ], + }; +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/get_simple_rule_output_with_web_hook_action.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/get_simple_rule_output_with_web_hook_action.ts new file mode 100644 index 0000000000000..7ecee679e50b3 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/get_simple_rule_output_with_web_hook_action.ts @@ -0,0 +1,29 @@ +/* + * 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 { NOTIFICATION_DEFAULT_FREQUENCY } from '@kbn/security-solution-plugin/common/constants'; +import { getSimpleRuleOutput } from './get_simple_rule_output'; +import { RuleWithoutServerGeneratedProperties } from './remove_server_generated_properties'; + +export const getSimpleRuleOutputWithWebHookAction = ( + actionId: string, + uuid: string +): RuleWithoutServerGeneratedProperties => ({ + ...getSimpleRuleOutput(), + actions: [ + { + action_type_id: '.webhook', + group: 'default', + id: actionId, + params: { + body: '{}', + }, + uuid, + frequency: NOTIFICATION_DEFAULT_FREQUENCY, + }, + ], +}); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/index.ts index ba91dea27743e..0170faa8ceeda 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/index.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/index.ts @@ -26,5 +26,9 @@ export * from './find_immutable_rule_by_id'; export * from './create_rule_with_exception_entries'; export * from './downgrade_immutable_rule'; export * from './get_eql_rule_for_alert_testing'; +export * from './get_rule_with_web_hook_action'; +export * from './get_simple_rule_output_with_web_hook_action'; +export * from './rule_to_update_schema'; +export * from './update_rule'; export * from './prebuilt_rules'; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/rule_to_update_schema.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/rule_to_update_schema.ts new file mode 100644 index 0000000000000..f6669a1325eb1 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/rule_to_update_schema.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + RuleResponse, + RuleUpdateProps, +} from '@kbn/security-solution-plugin/common/api/detection_engine'; +import { omit, pickBy } from 'lodash'; + +const propertiesToRemove = [ + 'id', + 'immutable', + 'updated_at', + 'updated_by', + 'created_at', + 'created_by', + 'related_integrations', + 'required_fields', + 'revision', + 'setup', + 'execution_summary', +]; + +/** + * transforms RuleResponse rule to RuleUpdateProps + * returned result can be used in rule update API calls + */ +export const ruleToUpdateSchema = (rule: RuleResponse): RuleUpdateProps => { + const removedProperties = omit(rule, propertiesToRemove); + + // We're only removing undefined values, so this cast correctly narrows the type + return pickBy(removedProperties, (value) => value !== undefined) as RuleUpdateProps; +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/update_rule.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/update_rule.ts new file mode 100644 index 0000000000000..53c1beb272764 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/update_rule.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ToolingLog } from '@kbn/tooling-log'; +import type SuperTest from 'supertest'; + +import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; +import { + RuleUpdateProps, + RuleResponse, +} from '@kbn/security-solution-plugin/common/api/detection_engine'; + +/** + * Helper to cut down on the noise in some of the tests. This checks for + * an expected 200 still and does not do any retries. + * @param supertest The supertest deps + * @param rule The rule to create + */ +export const updateRule = async ( + supertest: SuperTest.SuperTest, + log: ToolingLog, + updatedRule: RuleUpdateProps +): Promise => { + const response = await supertest + .put(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '2023-10-31') + .send(updatedRule); + if (response.status !== 200) { + log.error( + `Did not get an expected 200 "ok" when updating a rule (updateRule). CI issues could happen. Suspect this line if you are seeing CI issues. body: ${JSON.stringify( + response.body + )}, status: ${JSON.stringify(response.status)}` + ); + } + return response.body; +};