From 29705c01e4cca9ebd2b83ef1afd52546ddc198c6 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Wed, 4 May 2022 08:45:24 -0700 Subject: [PATCH] [Security Solution][Legacy Actions] - Update legacy action migration to account for more edge cases (#130511) ## Summary Updates the legacy actions migration code to account for edge cases we had not initially caught. Thanks to testing from some teammates, they reported seeing the following behavior: - Rules created pre 7.16 with no actions still create the legacy action sidecar (but not a `siem.notifications` legacy actions alert) which upon migration to 7.16+ was not being deleted - Rules created pre 7.16 with actions that run on every rule run create the legacy action sidecar(but not a `siem.notifications` legacy actions alert) which upon migration to 7.16+ was not being deleted - Rules created pre 7.16 with actions that were never enabled until 8.x did not have a `siem.notifications` legacy actions alert type created Because the legacy migration code relied on checking if a corresponding `siem.notifications` SO existed to kick off the necessary cleanup/migration, the above edge cases were not being caught. --- .../routes/__mocks__/request_responses.ts | 373 +++++- .../rules/add_prepackaged_rules_route.test.ts | 11 + .../routes/rules/delete_rules_route.test.ts | 13 + .../rules/patch_rules_bulk_route.test.ts | 14 + .../routes/rules/patch_rules_route.test.ts | 13 + .../rules/perform_bulk_action_route.test.ts | 23 +- .../rules/update_rules_bulk_route.test.ts | 15 + .../routes/rules/update_rules_route.test.ts | 13 + .../rules/update_prepacked_rules.test.ts | 14 +- .../lib/detection_engine/rules/utils.test.ts | 474 ++++++++ .../lib/detection_engine/rules/utils.ts | 126 +- .../security_and_spaces/tests/delete_rules.ts | 3 +- .../tests/delete_rules_bulk.ts | 7 +- .../security_and_spaces/tests/index.ts | 4 + .../tests/legacy_actions_migrations.ts | 322 +++++ .../get_legacy_action_notification_so.ts | 27 + ...et_legacy_action_notifications_so_by_id.ts | 29 + .../utils/get_legacy_actions_so_by_id.ts | 29 + .../utils/get_rule_so_by_id.ts | 27 + .../utils/get_slack_action.ts | 14 + .../utils/index.ts | 5 + .../legacy_actions/data.json | 1064 +++++++++++++++++ 22 files changed, 2584 insertions(+), 36 deletions(-) create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/tests/legacy_actions_migrations.ts create mode 100644 x-pack/test/detection_engine_api_integration/utils/get_legacy_action_notification_so.ts create mode 100644 x-pack/test/detection_engine_api_integration/utils/get_legacy_action_notifications_so_by_id.ts create mode 100644 x-pack/test/detection_engine_api_integration/utils/get_legacy_actions_so_by_id.ts create mode 100644 x-pack/test/detection_engine_api_integration/utils/get_rule_so_by_id.ts create mode 100644 x-pack/test/detection_engine_api_integration/utils/get_slack_action.ts create mode 100644 x-pack/test/functional/es_archives/security_solution/legacy_actions/data.json diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts index 8e2f0da4e65c9..e7f539d31ec0c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -9,7 +9,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { ALERT_WORKFLOW_STATUS } from '@kbn/rule-data-utils'; import { ruleTypeMappings } from '@kbn/securitysolution-rules'; -import { SavedObjectsFindResponse } from '@kbn/core/server'; +import { SavedObjectsFindResponse, SavedObjectsFindResult } from '@kbn/core/server'; import { ActionResult } from '@kbn/actions-plugin/server'; import { @@ -49,6 +49,8 @@ import { } from '../../../../../common/detection_engine/schemas/common'; // eslint-disable-next-line no-restricted-imports import type { LegacyRuleNotificationAlertType } from '../../notifications/legacy_types'; +// eslint-disable-next-line no-restricted-imports +import { LegacyIRuleActionsAttributes } from '../../rule_actions/legacy_types'; import { RuleExecutionSummariesByRuleId } from '../../rule_execution_log'; export const typicalSetStatusSignalByIdsPayload = (): SetSignalsStatusSchemaDecoded => ({ @@ -688,14 +690,20 @@ export const getSignalsMigrationStatusRequest = () => /** * @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function */ -export const legacyGetNotificationResult = (): LegacyRuleNotificationAlertType => ({ - id: '200dbf2f-b269-4bf9-aa85-11ba32ba73ba', +export const legacyGetNotificationResult = ({ + id = '456', + ruleId = '123', +}: { + id?: string; + ruleId?: string; +} = {}): LegacyRuleNotificationAlertType => ({ + id, name: 'Notification for Rule Test', tags: [], alertTypeId: 'siem.notifications', consumer: 'siem', params: { - ruleAlertId: '85b64e8a-2e40-4096-86af-5ac172c10825', + ruleAlertId: `${ruleId}`, }, schedule: { interval: '5m', @@ -732,10 +740,357 @@ export const legacyGetNotificationResult = (): LegacyRuleNotificationAlertType = /** * @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function */ -export const legacyGetFindNotificationsResultWithSingleHit = - (): FindHit => ({ +export const legacyGetHourlyNotificationResult = ( + id = '456', + ruleId = '123' +): LegacyRuleNotificationAlertType => ({ + id, + name: 'Notification for Rule Test', + tags: [], + alertTypeId: 'siem.notifications', + consumer: 'siem', + params: { + ruleAlertId: `${ruleId}`, + }, + schedule: { + interval: '1h', + }, + enabled: true, + actions: [ + { + group: 'default', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + to: ['test@test.com'], + subject: 'Test Actions', + }, + actionTypeId: '.email', + id: '99403909-ca9b-49ba-9d7a-7e5320e68d05', + }, + ], + throttle: null, + notifyWhen: 'onActiveAlert', + apiKey: null, + apiKeyOwner: 'elastic', + createdBy: 'elastic', + updatedBy: 'elastic', + createdAt: new Date('2020-03-21T11:15:13.530Z'), + muteAll: false, + mutedInstanceIds: [], + scheduledTaskId: '62b3a130-6b70-11ea-9ce9-6b9818c4cbd7', + updatedAt: new Date('2020-03-21T12:37:08.730Z'), + executionStatus: { + status: 'unknown', + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + }, +}); + +/** + * @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function + */ +export const legacyGetDailyNotificationResult = ( + id = '456', + ruleId = '123' +): LegacyRuleNotificationAlertType => ({ + id, + name: 'Notification for Rule Test', + tags: [], + alertTypeId: 'siem.notifications', + consumer: 'siem', + params: { + ruleAlertId: `${ruleId}`, + }, + schedule: { + interval: '1d', + }, + enabled: true, + actions: [ + { + group: 'default', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + to: ['test@test.com'], + subject: 'Test Actions', + }, + actionTypeId: '.email', + id: '99403909-ca9b-49ba-9d7a-7e5320e68d05', + }, + ], + throttle: null, + notifyWhen: 'onActiveAlert', + apiKey: null, + apiKeyOwner: 'elastic', + createdBy: 'elastic', + updatedBy: 'elastic', + createdAt: new Date('2020-03-21T11:15:13.530Z'), + muteAll: false, + mutedInstanceIds: [], + scheduledTaskId: '62b3a130-6b70-11ea-9ce9-6b9818c4cbd7', + updatedAt: new Date('2020-03-21T12:37:08.730Z'), + executionStatus: { + status: 'unknown', + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + }, +}); + +/** + * @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function + */ +export const legacyGetWeeklyNotificationResult = ( + id = '456', + ruleId = '123' +): LegacyRuleNotificationAlertType => ({ + id, + name: 'Notification for Rule Test', + tags: [], + alertTypeId: 'siem.notifications', + consumer: 'siem', + params: { + ruleAlertId: `${ruleId}`, + }, + schedule: { + interval: '7d', + }, + enabled: true, + actions: [ + { + group: 'default', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + to: ['test@test.com'], + subject: 'Test Actions', + }, + actionTypeId: '.email', + id: '99403909-ca9b-49ba-9d7a-7e5320e68d05', + }, + ], + throttle: null, + notifyWhen: 'onActiveAlert', + apiKey: null, + apiKeyOwner: 'elastic', + createdBy: 'elastic', + updatedBy: 'elastic', + createdAt: new Date('2020-03-21T11:15:13.530Z'), + muteAll: false, + mutedInstanceIds: [], + scheduledTaskId: '62b3a130-6b70-11ea-9ce9-6b9818c4cbd7', + updatedAt: new Date('2020-03-21T12:37:08.730Z'), + executionStatus: { + status: 'unknown', + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + }, +}); + +/** + * @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function + */ +export const legacyGetFindNotificationsResultWithSingleHit = ( + ruleId = '123' +): FindHit => ({ + page: 1, + perPage: 1, + total: 1, + data: [legacyGetNotificationResult({ ruleId })], +}); + +/** + * @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function + */ +export const legacyGetSiemNotificationRuleNoActionsSOResult = ( + ruleId = '123' +): SavedObjectsFindResult => ({ + type: 'siem-detection-engine-rule-actions', + id: 'ID_OF_LEGACY_SIDECAR_NO_ACTIONS', + namespaces: ['default'], + attributes: { + actions: [], + ruleThrottle: 'no_actions', + alertThrottle: null, + }, + references: [{ id: ruleId, type: 'alert', name: 'alert_0' }], + migrationVersion: { + 'siem-detection-engine-rule-actions': '7.11.2', + }, + coreMigrationVersion: '7.15.2', + updated_at: '2022-03-31T19:06:40.473Z', + version: 'WzIzNywxXQ==', + score: 0, +}); + +/** + * @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function + */ +export const legacyGetSiemNotificationRuleEveryRunSOResult = ( + ruleId = '123' +): SavedObjectsFindResult => ({ + type: 'siem-detection-engine-rule-actions', + id: 'ID_OF_LEGACY_SIDECAR_RULE_RUN_ACTIONS', + namespaces: ['default'], + attributes: { + actions: [ + { + group: 'default', + actionRef: 'action_0', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + to: ['test@test.com'], + subject: 'Test Actions', + }, + action_type_id: '.email', + }, + ], + ruleThrottle: 'rule', + alertThrottle: null, + }, + references: [{ id: ruleId, type: 'alert', name: 'alert_0' }], + migrationVersion: { + 'siem-detection-engine-rule-actions': '7.11.2', + }, + coreMigrationVersion: '7.15.2', + updated_at: '2022-03-31T19:06:40.473Z', + version: 'WzIzNywxXQ==', + score: 0, +}); + +/** + * @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function + */ +export const legacyGetSiemNotificationRuleHourlyActionsSOResult = ( + ruleId = '123', + connectorId = '456' +): SavedObjectsFindResult => ({ + type: 'siem-detection-engine-rule-actions', + id: 'ID_OF_LEGACY_SIDECAR_HOURLY_ACTIONS', + namespaces: ['default'], + attributes: { + actions: [ + { + group: 'default', + actionRef: 'action_0', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + to: ['test@test.com'], + subject: 'Test Actions', + }, + action_type_id: '.email', + }, + ], + ruleThrottle: '1h', + alertThrottle: '1h', + }, + references: [ + { id: ruleId, type: 'alert', name: 'alert_0' }, + { id: connectorId, type: 'action', name: 'action_0' }, + ], + migrationVersion: { + 'siem-detection-engine-rule-actions': '7.11.2', + }, + coreMigrationVersion: '7.15.2', + updated_at: '2022-03-31T19:06:40.473Z', + version: 'WzIzNywxXQ==', + score: 0, +}); + +/** + * @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function + */ +export const legacyGetSiemNotificationRuleDailyActionsSOResult = ( + ruleId = '123', + connectorId = '456' +): SavedObjectsFindResult => ({ + type: 'siem-detection-engine-rule-actions', + id: 'ID_OF_LEGACY_SIDECAR_DAILY_ACTIONS', + namespaces: ['default'], + attributes: { + actions: [ + { + group: 'default', + actionRef: 'action_0', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + to: ['test@test.com'], + subject: 'Test Actions', + }, + action_type_id: '.email', + }, + ], + ruleThrottle: '1d', + alertThrottle: '1d', + }, + references: [ + { id: ruleId, type: 'alert', name: 'alert_0' }, + { id: connectorId, type: 'action', name: 'action_0' }, + ], + migrationVersion: { + 'siem-detection-engine-rule-actions': '7.11.2', + }, + coreMigrationVersion: '7.15.2', + updated_at: '2022-03-31T19:06:40.473Z', + version: 'WzIzNywxXQ==', + score: 0, +}); + +/** + * @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function + */ +export const legacyGetSiemNotificationRuleWeeklyActionsSOResult = ( + ruleId = '123', + connectorId = '456' +): SavedObjectsFindResult => ({ + type: 'siem-detection-engine-rule-actions', + id: 'ID_OF_LEGACY_SIDECAR_WEEKLY_ACTIONS', + namespaces: ['default'], + attributes: { + actions: [ + { + group: 'default', + actionRef: 'action_0', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + to: ['test@test.com'], + subject: 'Test Actions', + }, + action_type_id: '.email', + }, + ], + ruleThrottle: '7d', + alertThrottle: '7d', + }, + references: [ + { id: ruleId, type: 'alert', name: 'alert_0' }, + { id: connectorId, type: 'action', name: 'action_0' }, + ], + migrationVersion: { + 'siem-detection-engine-rule-actions': '7.11.2', + }, + coreMigrationVersion: '7.15.2', + updated_at: '2022-03-31T19:06:40.473Z', + version: 'WzIzNywxXQ==', + score: 0, +}); + +const getLegacyActionSOs = (ruleId = '123', connectorId = '456') => ({ + none: () => legacyGetSiemNotificationRuleNoActionsSOResult(ruleId), + rule: () => legacyGetSiemNotificationRuleEveryRunSOResult(ruleId), + hourly: () => legacyGetSiemNotificationRuleHourlyActionsSOResult(ruleId, connectorId), + daily: () => legacyGetSiemNotificationRuleDailyActionsSOResult(ruleId, connectorId), + weekly: () => legacyGetSiemNotificationRuleWeeklyActionsSOResult(ruleId, connectorId), +}); + +/** + * @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function + */ +export const legacyGetSiemNotificationRuleActionsSOResultWithSingleHit = ( + actionTypes: Array<'none' | 'rule' | 'daily' | 'hourly' | 'weekly'>, + ruleId = '123', + connectorId = '456' +): SavedObjectsFindResponse => { + const actions = getLegacyActionSOs(ruleId, connectorId); + + return { page: 1, - perPage: 1, + per_page: 1, total: 1, - data: [legacyGetNotificationResult()], - }); + saved_objects: actionTypes.map((type) => actions[type]()), + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts index 327b8afb46b5d..8db750ba220af 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts @@ -21,6 +21,15 @@ import { installPrepackagedTimelines } from '../../../timeline/routes/prepackage // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { elasticsearchClientMock } from '@kbn/core/server/elasticsearch/client/mocks'; import { getQueryRuleParams } from '../../schemas/rule_schemas.mock'; +import { legacyMigrate } from '../../rules/utils'; + +jest.mock('../../rules/utils', () => { + const actual = jest.requireActual('../../rules/utils'); + return { + ...actual, + legacyMigrate: jest.fn(), + }; +}); jest.mock('../../rules/get_prepackaged_rules', () => { return { @@ -92,6 +101,8 @@ describe('add_prepackaged_rules_route', () => { errors: [], }); + (legacyMigrate as jest.Mock).mockResolvedValue(getRuleMock(getQueryRuleParams())); + context.core.elasticsearch.client.asCurrentUser.search.mockResolvedValue( elasticsearchClientMock.createSuccessTransportRequestPromise(getBasicEmptySearchResponse()) ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts index 83a40005d0148..85c324008856c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts @@ -13,10 +13,20 @@ import { getFindResultWithSingleHit, getDeleteRequestById, getEmptySavedObjectsResponse, + getRuleMock, } from '../__mocks__/request_responses'; import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { deleteRulesRoute } from './delete_rules_route'; import { getQueryRuleParams } from '../../schemas/rule_schemas.mock'; +import { legacyMigrate } from '../../rules/utils'; + +jest.mock('../../rules/utils', () => { + const actual = jest.requireActual('../../rules/utils'); + return { + ...actual, + legacyMigrate: jest.fn(), + }; +}); describe('delete_rules', () => { let server: ReturnType; @@ -29,6 +39,8 @@ describe('delete_rules', () => { clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); clients.savedObjectsClient.find.mockResolvedValue(getEmptySavedObjectsResponse()); + (legacyMigrate as jest.Mock).mockResolvedValue(getRuleMock(getQueryRuleParams())); + deleteRulesRoute(server.router); }); @@ -54,6 +66,7 @@ describe('delete_rules', () => { test('returns 404 when deleting a single rule that does not exist with a valid actionClient and alertClient', async () => { clients.rulesClient.find.mockResolvedValue(getEmptyFindResult()); + (legacyMigrate as jest.Mock).mockResolvedValue(null); const response = await server.inject( getDeleteRequest(), requestContextMock.convertContext(context) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts index 6abac6e946380..fa75be49a61c5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts @@ -23,9 +23,18 @@ import { patchRulesBulkRoute } from './patch_rules_bulk_route'; import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; import { getQueryRuleParams } from '../../schemas/rule_schemas.mock'; import { loggingSystemMock } from '@kbn/core/server/mocks'; +import { legacyMigrate } from '../../rules/utils'; jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); +jest.mock('../../rules/utils', () => { + const actual = jest.requireActual('../../rules/utils'); + return { + ...actual, + legacyMigrate: jest.fn(), + }; +}); + describe('patch_rules_bulk', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); @@ -40,6 +49,8 @@ describe('patch_rules_bulk', () => { clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); // rule exists clients.rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); // update succeeds + (legacyMigrate as jest.Mock).mockResolvedValue(getRuleMock(getQueryRuleParams())); + patchRulesBulkRoute(server.router, ml, logger); }); @@ -54,6 +65,7 @@ describe('patch_rules_bulk', () => { test('returns an error in the response when updating a single rule that does not exist', async () => { clients.rulesClient.find.mockResolvedValue(getEmptyFindResult()); + (legacyMigrate as jest.Mock).mockResolvedValue(null); const response = await server.inject( getPatchBulkRequest(), requestContextMock.convertContext(context) @@ -148,6 +160,8 @@ describe('patch_rules_bulk', () => { describe('request validation', () => { test('rejects payloads with no ID', async () => { + (legacyMigrate as jest.Mock).mockResolvedValue(null); + const request = requestMock.create({ method: 'patch', path: DETECTION_ENGINE_RULES_BULK_UPDATE, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts index cbcf5540b4f15..87c2e79922457 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts @@ -21,9 +21,18 @@ import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { patchRulesRoute } from './patch_rules_route'; import { getPatchRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/patch_rules_schema.mock'; import { getQueryRuleParams } from '../../schemas/rule_schemas.mock'; +import { legacyMigrate } from '../../rules/utils'; jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); +jest.mock('../../rules/utils', () => { + const actual = jest.requireActual('../../rules/utils'); + return { + ...actual, + legacyMigrate: jest.fn(), + }; +}); + describe('patch_rules', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); @@ -41,6 +50,8 @@ describe('patch_rules', () => { getRuleExecutionSummarySucceeded() ); + (legacyMigrate as jest.Mock).mockResolvedValue(getRuleMock(getQueryRuleParams())); + patchRulesRoute(server.router, ml); }); @@ -55,6 +66,7 @@ describe('patch_rules', () => { test('returns 404 when updating a single rule that does not exist', async () => { clients.rulesClient.find.mockResolvedValue(getEmptyFindResult()); + (legacyMigrate as jest.Mock).mockResolvedValue(null); const response = await server.inject( getPatchRequest(), requestContextMock.convertContext(context) @@ -67,6 +79,7 @@ describe('patch_rules', () => { }); test('returns error if requesting a non-rule', async () => { + (legacyMigrate as jest.Mock).mockResolvedValue(null); clients.rulesClient.find.mockResolvedValue(nonRuleFindResult()); const response = await server.inject( getPatchRequest(), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.test.ts index 3dc8ab5199c7f..4c50ee58e2d7f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.test.ts @@ -14,6 +14,7 @@ import { getBulkActionEditRequest, getFindResultWithSingleHit, getFindResultWithMultiHits, + getRuleMock, } from '../__mocks__/request_responses'; import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { performBulkActionRoute } from './perform_bulk_action_route'; @@ -23,10 +24,20 @@ import { } from '../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema.mock'; import { loggingSystemMock } from '@kbn/core/server/mocks'; import { readRules } from '../../rules/read_rules'; +import { legacyMigrate } from '../../rules/utils'; +import { getQueryRuleParams } from '../../schemas/rule_schemas.mock'; jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); jest.mock('../../rules/read_rules', () => ({ readRules: jest.fn() })); +jest.mock('../../rules/utils', () => { + const actual = jest.requireActual('../../rules/utils'); + return { + ...actual, + legacyMigrate: jest.fn(), + }; +}); + describe('perform_bulk_action', () => { const readRulesMock = readRules as jest.Mock; let server: ReturnType; @@ -40,6 +51,8 @@ describe('perform_bulk_action', () => { logger = loggingSystemMock.createLogger(); ({ clients, context } = requestContextMock.createTools()); ml = mlServicesMock.createSetupContract(); + (legacyMigrate as jest.Mock).mockResolvedValue(getRuleMock(getQueryRuleParams())); + clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); performBulkActionRoute(server.router, ml, logger); }); @@ -220,7 +233,10 @@ describe('perform_bulk_action', () => { readRulesMock.mockImplementationOnce(() => Promise.resolve({ ...mockRule, params: { ...mockRule.params, type: 'machine_learning' } }) ); - + (legacyMigrate as jest.Mock).mockResolvedValue({ + ...mockRule, + params: { ...mockRule.params, type: 'machine_learning' }, + }); const request = requestMock.create({ method: 'patch', path: DETECTION_ENGINE_RULES_BULK_ACTION, @@ -271,7 +287,10 @@ describe('perform_bulk_action', () => { readRulesMock.mockImplementationOnce(() => Promise.resolve({ ...mockRule, params: { ...mockRule.params, index: ['index-*'] } }) ); - + (legacyMigrate as jest.Mock).mockResolvedValue({ + ...mockRule, + params: { ...mockRule.params, index: ['index-*'] }, + }); const request = requestMock.create({ method: 'patch', path: DETECTION_ENGINE_RULES_BULK_ACTION, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts index 88720646fa6cd..e0c9289a562e7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts @@ -21,9 +21,18 @@ import { BulkError } from '../utils'; import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; import { getQueryRuleParams } from '../../schemas/rule_schemas.mock'; import { loggingSystemMock } from '@kbn/core/server/mocks'; +import { legacyMigrate } from '../../rules/utils'; jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); +jest.mock('../../rules/utils', () => { + const actual = jest.requireActual('../../rules/utils'); + return { + ...actual, + legacyMigrate: jest.fn(), + }; +}); + describe('update_rules_bulk', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); @@ -40,6 +49,8 @@ describe('update_rules_bulk', () => { clients.appClient.getSignalsIndex.mockReturnValue('.siem-signals-test-index'); + (legacyMigrate as jest.Mock).mockResolvedValue(getRuleMock(getQueryRuleParams())); + updateRulesBulkRoute(server.router, ml, logger); }); @@ -54,6 +65,8 @@ describe('update_rules_bulk', () => { test('returns 200 as a response when updating a single rule that does not exist', async () => { clients.rulesClient.find.mockResolvedValue(getEmptyFindResult()); + (legacyMigrate as jest.Mock).mockResolvedValue(null); + const expected: BulkError[] = [ { error: { message: 'rule_id: "rule-1" not found', status_code: 404 }, @@ -116,6 +129,8 @@ describe('update_rules_bulk', () => { describe('request validation', () => { test('rejects payloads with no ID', async () => { + (legacyMigrate as jest.Mock).mockResolvedValue(null); + const noIdRequest = requestMock.create({ method: 'put', path: DETECTION_ENGINE_RULES_BULK_UPDATE, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts index 39040f4c9c4fc..7f2d5c3bde7e0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts @@ -21,9 +21,18 @@ import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { updateRulesRoute } from './update_rules_route'; import { getUpdateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; import { getQueryRuleParams } from '../../schemas/rule_schemas.mock'; +import { legacyMigrate } from '../../rules/utils'; jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); +jest.mock('../../rules/utils', () => { + const actual = jest.requireActual('../../rules/utils'); + return { + ...actual, + legacyMigrate: jest.fn(), + }; +}); + describe('update_rules', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); @@ -42,6 +51,8 @@ describe('update_rules', () => { ); clients.appClient.getSignalsIndex.mockReturnValue('.siem-signals-test-index'); + (legacyMigrate as jest.Mock).mockResolvedValue(getRuleMock(getQueryRuleParams())); + updateRulesRoute(server.router, ml); }); @@ -56,6 +67,7 @@ describe('update_rules', () => { test('returns 404 when updating a single rule that does not exist', async () => { clients.rulesClient.find.mockResolvedValue(getEmptyFindResult()); + (legacyMigrate as jest.Mock).mockResolvedValue(null); const response = await server.inject( getUpdateRequest(), requestContextMock.convertContext(context) @@ -69,6 +81,7 @@ describe('update_rules', () => { }); test('returns error when updating non-rule', async () => { + (legacyMigrate as jest.Mock).mockResolvedValue(null); clients.rulesClient.find.mockResolvedValue(nonRuleFindResult()); const response = await server.inject( getUpdateRequest(), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.test.ts index 96fba6703a537..2a54ce1e976d4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.test.ts @@ -7,14 +7,24 @@ import { rulesClientMock } from '@kbn/alerting-plugin/server/mocks'; import { savedObjectsClientMock } from '@kbn/core/server/mocks'; -import { getFindResultWithSingleHit } from '../routes/__mocks__/request_responses'; +import { getRuleMock, getFindResultWithSingleHit } from '../routes/__mocks__/request_responses'; import { updatePrepackagedRules } from './update_prepacked_rules'; import { patchRules } from './patch_rules'; import { getAddPrepackagedRulesSchemaDecodedMock } from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema.mock'; import { ruleExecutionLogMock } from '../rule_execution_log/__mocks__'; +import { legacyMigrate } from './utils'; +import { getQueryRuleParams } from '../schemas/rule_schemas.mock'; jest.mock('./patch_rules'); +jest.mock('./utils', () => { + const actual = jest.requireActual('./utils'); + return { + ...actual, + legacyMigrate: jest.fn(), + }; +}); + describe('updatePrepackagedRules', () => { let rulesClient: ReturnType; let savedObjectsClient: ReturnType; @@ -24,6 +34,8 @@ describe('updatePrepackagedRules', () => { rulesClient = rulesClientMock.create(); savedObjectsClient = savedObjectsClientMock.create(); ruleExecutionLog = ruleExecutionLogMock.forRoutes.create(); + + (legacyMigrate as jest.Mock).mockResolvedValue(getRuleMock(getQueryRuleParams())); }); it('should omit actions and enabled when calling patchRules', async () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.test.ts index 5151a58d0d885..0952da3182e01 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.test.ts @@ -13,6 +13,8 @@ import { transformToAlertThrottle, transformFromAlertThrottle, transformActions, + legacyMigrate, + getUpdatedActionsParams, } from './utils'; import { RuleAction, SanitizedRule } from '@kbn/alerting-plugin/common'; import { RuleParams } from '../schemas/rule_schemas'; @@ -23,6 +25,67 @@ import { import { FullResponseSchema } from '../../../../common/detection_engine/schemas/request'; // eslint-disable-next-line no-restricted-imports import { LegacyRuleActions } from '../rule_actions/legacy_types'; +import { + getEmptyFindResult, + legacyGetSiemNotificationRuleActionsSOResultWithSingleHit, + legacyGetDailyNotificationResult, + legacyGetHourlyNotificationResult, + legacyGetWeeklyNotificationResult, +} from '../routes/__mocks__/request_responses'; +import { requestContextMock } from '../routes/__mocks__'; + +const getRuleLegacyActions = (): SanitizedRule => + ({ + id: '123', + notifyWhen: 'onThrottleInterval', + name: 'Simple Rule Query', + tags: ['__internal_rule_id:ruleId', '__internal_immutable:false'], + alertTypeId: 'siem.queryRule', + consumer: 'siem', + enabled: true, + throttle: '1h', + apiKeyOwner: 'elastic', + createdBy: 'elastic', + updatedBy: 'elastic', + muteAll: false, + mutedInstanceIds: [], + monitoring: { execution: { history: [], calculated_metrics: { success_ratio: 0 } } }, + mapped_params: { risk_score: 1, severity: '60-high' }, + schedule: { interval: '5m' }, + actions: [], + params: { + author: [], + description: 'Simple Rule Query', + ruleId: 'ruleId', + falsePositives: [], + from: 'now-6m', + immutable: false, + outputIndex: '.siem-signals-default', + maxSignals: 100, + riskScore: 1, + riskScoreMapping: [], + severity: 'high', + severityMapping: [], + threat: [], + to: 'now', + references: [], + version: 1, + exceptionsList: [], + type: 'query', + language: 'kuery', + index: ['auditbeat-*'], + query: 'user.name: root or user.name: admin', + }, + snoozeEndTime: null, + updatedAt: '2022-03-31T21:47:25.695Z', + createdAt: '2022-03-31T21:47:16.379Z', + scheduledTaskId: '21bb9b60-b13c-11ec-99d0-asdfasdfasf', + executionStatus: { + status: 'pending', + lastExecutionDate: '2022-03-31T21:47:25.695Z', + lastDuration: 0, + }, + } as unknown as SanitizedRule); describe('utils', () => { describe('#calculateInterval', () => { @@ -588,4 +651,415 @@ describe('utils', () => { ]); }); }); + + describe('#legacyMigrate', () => { + const ruleId = '123'; + const connectorId = '456'; + const { clients } = requestContextMock.createTools(); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + test('it does no cleanup or migration if no legacy reminants found', async () => { + clients.rulesClient.find.mockResolvedValueOnce(getEmptyFindResult()); + clients.savedObjectsClient.find.mockResolvedValueOnce({ + page: 0, + per_page: 0, + total: 0, + saved_objects: [], + }); + + const rule = { + ...getRuleLegacyActions(), + id: ruleId, + actions: [], + throttle: null, + notifyWhen: 'onActiveAlert', + muteAll: true, + } as SanitizedRule; + + const migratedRule = await legacyMigrate({ + rulesClient: clients.rulesClient, + savedObjectsClient: clients.savedObjectsClient, + rule, + }); + + expect(clients.rulesClient.delete).not.toHaveBeenCalled(); + expect(clients.savedObjectsClient.delete).not.toHaveBeenCalled(); + expect(migratedRule).toEqual(rule); + }); + + // Even if a rule is created with no actions pre 7.16, a + // siem-detection-engine-rule-actions SO is still created + test('it migrates a rule with no actions', async () => { + // siem.notifications is not created for a rule with no actions + clients.rulesClient.find.mockResolvedValueOnce(getEmptyFindResult()); + // siem-detection-engine-rule-actions SO is still created + clients.savedObjectsClient.find.mockResolvedValueOnce( + legacyGetSiemNotificationRuleActionsSOResultWithSingleHit(['none'], ruleId, connectorId) + ); + + const migratedRule = await legacyMigrate({ + rulesClient: clients.rulesClient, + savedObjectsClient: clients.savedObjectsClient, + rule: { + ...getRuleLegacyActions(), + id: ruleId, + actions: [], + throttle: null, + notifyWhen: 'onActiveAlert', + muteAll: true, + }, + }); + + expect(clients.rulesClient.delete).not.toHaveBeenCalled(); + expect(clients.savedObjectsClient.delete).toHaveBeenCalledWith( + 'siem-detection-engine-rule-actions', + 'ID_OF_LEGACY_SIDECAR_NO_ACTIONS' + ); + expect(migratedRule?.actions).toEqual([]); + expect(migratedRule?.throttle).toBeNull(); + expect(migratedRule?.muteAll).toBeTruthy(); + expect(migratedRule?.notifyWhen).toEqual('onActiveAlert'); + }); + + test('it migrates a rule with every rule run action', async () => { + // siem.notifications is not created for a rule with actions run every rule run + clients.rulesClient.find.mockResolvedValueOnce(getEmptyFindResult()); + // siem-detection-engine-rule-actions SO is still created + clients.savedObjectsClient.find.mockResolvedValueOnce( + legacyGetSiemNotificationRuleActionsSOResultWithSingleHit(['rule'], ruleId, connectorId) + ); + + const migratedRule = await legacyMigrate({ + rulesClient: clients.rulesClient, + savedObjectsClient: clients.savedObjectsClient, + rule: { + ...getRuleLegacyActions(), + id: ruleId, + actions: [ + { + actionTypeId: '.email', + params: { + subject: 'Test Actions', + to: ['test@test.com'], + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + }, + id: connectorId, + group: 'default', + }, + ], + throttle: null, + notifyWhen: 'onActiveAlert', + muteAll: false, + }, + }); + + expect(clients.rulesClient.delete).not.toHaveBeenCalled(); + expect(clients.savedObjectsClient.delete).toHaveBeenCalledWith( + 'siem-detection-engine-rule-actions', + 'ID_OF_LEGACY_SIDECAR_RULE_RUN_ACTIONS' + ); + expect(migratedRule?.actions).toEqual([ + { + id: connectorId, + actionTypeId: '.email', + group: 'default', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + subject: 'Test Actions', + to: ['test@test.com'], + }, + }, + ]); + expect(migratedRule?.notifyWhen).toEqual('onActiveAlert'); + expect(migratedRule?.throttle).toBeNull(); + expect(migratedRule?.muteAll).toBeFalsy(); + }); + + test('it migrates a rule with daily legacy actions', async () => { + // siem.notifications is not created for a rule with no actions + clients.rulesClient.find.mockResolvedValueOnce({ + page: 1, + perPage: 1, + total: 1, + data: [legacyGetDailyNotificationResult(connectorId, ruleId)], + }); + // siem-detection-engine-rule-actions SO is still created + clients.savedObjectsClient.find.mockResolvedValueOnce( + legacyGetSiemNotificationRuleActionsSOResultWithSingleHit(['daily'], ruleId, connectorId) + ); + + const migratedRule = await legacyMigrate({ + rulesClient: clients.rulesClient, + savedObjectsClient: clients.savedObjectsClient, + rule: { + ...getRuleLegacyActions(), + id: ruleId, + actions: [], + throttle: null, + notifyWhen: 'onActiveAlert', + }, + }); + + expect(clients.rulesClient.delete).toHaveBeenCalledWith({ id: '456' }); + expect(clients.savedObjectsClient.delete).toHaveBeenCalledWith( + 'siem-detection-engine-rule-actions', + 'ID_OF_LEGACY_SIDECAR_DAILY_ACTIONS' + ); + expect(migratedRule?.actions).toEqual([ + { + actionTypeId: '.email', + group: 'default', + id: connectorId, + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + to: ['test@test.com'], + subject: 'Test Actions', + }, + }, + ]); + expect(migratedRule?.throttle).toEqual('1d'); + expect(migratedRule?.notifyWhen).toEqual('onThrottleInterval'); + expect(migratedRule?.muteAll).toBeFalsy(); + }); + + test('it migrates a rule with hourly legacy actions', async () => { + // siem.notifications is not created for a rule with no actions + clients.rulesClient.find.mockResolvedValueOnce({ + page: 1, + perPage: 1, + total: 1, + data: [legacyGetHourlyNotificationResult(connectorId, ruleId)], + }); + // siem-detection-engine-rule-actions SO is still created + clients.savedObjectsClient.find.mockResolvedValueOnce( + legacyGetSiemNotificationRuleActionsSOResultWithSingleHit(['hourly'], ruleId, connectorId) + ); + + const migratedRule = await legacyMigrate({ + rulesClient: clients.rulesClient, + savedObjectsClient: clients.savedObjectsClient, + rule: { + ...getRuleLegacyActions(), + id: ruleId, + actions: [], + throttle: null, + notifyWhen: 'onActiveAlert', + }, + }); + + expect(clients.rulesClient.delete).toHaveBeenCalledWith({ id: '456' }); + expect(clients.savedObjectsClient.delete).toHaveBeenCalledWith( + 'siem-detection-engine-rule-actions', + 'ID_OF_LEGACY_SIDECAR_HOURLY_ACTIONS' + ); + expect(migratedRule?.actions).toEqual([ + { + actionTypeId: '.email', + group: 'default', + id: connectorId, + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + to: ['test@test.com'], + subject: 'Test Actions', + }, + }, + ]); + expect(migratedRule?.throttle).toEqual('1h'); + expect(migratedRule?.notifyWhen).toEqual('onThrottleInterval'); + expect(migratedRule?.muteAll).toBeFalsy(); + }); + + test('it migrates a rule with weekly legacy actions', async () => { + // siem.notifications is not created for a rule with no actions + clients.rulesClient.find.mockResolvedValueOnce({ + page: 1, + perPage: 1, + total: 1, + data: [legacyGetWeeklyNotificationResult(connectorId, ruleId)], + }); + // siem-detection-engine-rule-actions SO is still created + clients.savedObjectsClient.find.mockResolvedValueOnce( + legacyGetSiemNotificationRuleActionsSOResultWithSingleHit(['weekly'], ruleId, connectorId) + ); + + const migratedRule = await legacyMigrate({ + rulesClient: clients.rulesClient, + savedObjectsClient: clients.savedObjectsClient, + rule: { + ...getRuleLegacyActions(), + id: ruleId, + actions: [], + throttle: null, + notifyWhen: 'onActiveAlert', + }, + }); + + expect(clients.rulesClient.delete).toHaveBeenCalledWith({ id: '456' }); + expect(clients.savedObjectsClient.delete).toHaveBeenCalledWith( + 'siem-detection-engine-rule-actions', + 'ID_OF_LEGACY_SIDECAR_WEEKLY_ACTIONS' + ); + expect(migratedRule?.actions).toEqual([ + { + actionTypeId: '.email', + group: 'default', + id: connectorId, + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + to: ['test@test.com'], + subject: 'Test Actions', + }, + }, + ]); + expect(migratedRule?.throttle).toEqual('7d'); + expect(migratedRule?.notifyWhen).toEqual('onThrottleInterval'); + expect(migratedRule?.muteAll).toBeFalsy(); + }); + }); + + describe('#getUpdatedActionsParams', () => { + it('updates one action', () => { + const { id, ...rule } = { + ...getRuleLegacyActions(), + id: '123', + actions: [], + throttle: null, + notifyWhen: 'onActiveAlert', + } as SanitizedRule; + + expect( + getUpdatedActionsParams({ + rule: { + ...rule, + id, + }, + ruleThrottle: '1h', + actions: [ + { + actionRef: 'action_0', + group: 'default', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + to: ['a@a.com'], + subject: 'Test Actions', + }, + action_type_id: '.email', + }, + ], + references: [ + { + id: '61ec7a40-b076-11ec-bb3f-1f063f8e06cf', + type: 'alert', + name: 'alert_0', + }, + { + id: '1234', + type: 'action', + name: 'action_0', + }, + ], + }) + ).toEqual({ + ...rule, + actions: [ + { + actionTypeId: '.email', + group: 'default', + id: '1234', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + subject: 'Test Actions', + to: ['a@a.com'], + }, + }, + ], + throttle: '1h', + notifyWhen: 'onThrottleInterval', + }); + }); + + it('updates multiple actions', () => { + const { id, ...rule } = { + ...getRuleLegacyActions(), + id: '123', + actions: [], + throttle: null, + notifyWhen: 'onActiveAlert', + } as SanitizedRule; + + expect( + getUpdatedActionsParams({ + rule: { + ...rule, + id, + }, + ruleThrottle: '1h', + actions: [ + { + actionRef: 'action_0', + group: 'default', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + to: ['test@test.com'], + subject: 'Rule email', + }, + action_type_id: '.email', + }, + { + actionRef: 'action_1', + group: 'default', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + }, + action_type_id: '.slack', + }, + ], + references: [ + { + id: '064e3160-b076-11ec-bb3f-1f063f8e06cf', + type: 'alert', + name: 'alert_0', + }, + { + id: 'c95cb100-b075-11ec-bb3f-1f063f8e06cf', + type: 'action', + name: 'action_0', + }, + { + id: '207fa0e0-c04e-11ec-8a52-4fb92379525a', + type: 'action', + name: 'action_1', + }, + ], + }) + ).toEqual({ + ...rule, + actions: [ + { + actionTypeId: '.email', + group: 'default', + id: 'c95cb100-b075-11ec-bb3f-1f063f8e06cf', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + subject: 'Rule email', + to: ['test@test.com'], + }, + }, + { + actionTypeId: '.slack', + group: 'default', + id: '207fa0e0-c04e-11ec-8a52-4fb92379525a', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + }, + }, + ], + throttle: '1h', + notifyWhen: 'onThrottleInterval', + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts index c7858a131cb1f..dd25676a758e4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts @@ -28,6 +28,7 @@ import type { } from '@kbn/securitysolution-io-ts-alerting-types'; import type { ListArrayOrUndefined } from '@kbn/securitysolution-io-ts-list-types'; import type { VersionOrUndefined } from '@kbn/securitysolution-io-ts-types'; +import { SavedObjectReference } from '@kbn/core/server'; import { RuleAction, RuleNotifyWhenType, SanitizedRule } from '@kbn/alerting-plugin/common'; import { RulesClient } from '@kbn/alerting-plugin/server'; import { @@ -63,7 +64,11 @@ import { NOTIFICATION_THROTTLE_RULE, } from '../../../../common/constants'; // eslint-disable-next-line no-restricted-imports -import { LegacyRuleActions } from '../rule_actions/legacy_types'; +import { + LegacyIRuleActionsAttributes, + LegacyRuleActions, + LegacyRuleAlertSavedObjectAction, +} from '../rule_actions/legacy_types'; import { FullResponseSchema } from '../../../../common/detection_engine/schemas/request'; import { transformAlertToRuleAction } from '../../../../common/detection_engine/transform_actions'; // eslint-disable-next-line no-restricted-imports @@ -302,6 +307,59 @@ export const maybeMute = async ({ } }; +/** + * Translate legacy action sidecar action to rule action + */ +export const getUpdatedActionsParams = ({ + rule, + ruleThrottle, + actions, + references, +}: { + rule: SanitizedRule; + ruleThrottle: string | null; + actions: LegacyRuleAlertSavedObjectAction[]; + references: SavedObjectReference[]; +}): Omit, 'id'> => { + const { id, ...restOfRule } = rule; + + const actionReference = references.reduce>( + (acc, reference) => { + acc[reference.name] = reference; + return acc; + }, + {} + ); + + if (isEmpty(actionReference)) { + throw new Error( + `An error occurred migrating legacy action for rule with id:${id}. Connector reference id not found.` + ); + } + // If rule has an action on any other interval (other than on every + // rule run), need to move the action info from the sidecar/legacy action + // into the rule itself + return { + ...restOfRule, + actions: actions.reduce((acc, action) => { + const { actionRef, action_type_id: actionTypeId, ...resOfAction } = action; + if (!actionReference[actionRef]) { + return acc; + } + return [ + ...acc, + { + ...resOfAction, + id: actionReference[actionRef].id, + actionTypeId, + }, + ]; + }, []), + throttle: transformToAlertThrottle(ruleThrottle), + notifyWhen: transformToNotifyWhen(ruleThrottle), + }; +}; + /** * Determines if rule needs to be migrated from legacy actions * and returns necessary pieces for the updated rule @@ -332,7 +390,7 @@ export const legacyMigrate = async ({ }, }, }), - savedObjectsClient.find({ + savedObjectsClient.find({ type: legacyRuleActionsSavedObjectType, hasReference: { type: 'alert', @@ -341,29 +399,57 @@ export const legacyMigrate = async ({ }), ]); - if (siemNotification != null && siemNotification.data.length > 0) { - await Promise.all([ - rulesClient.delete({ id: siemNotification.data[0].id }), - legacyRuleActionsSO != null && legacyRuleActionsSO.saved_objects.length > 0 - ? savedObjectsClient.delete( - legacyRuleActionsSavedObjectType, - legacyRuleActionsSO.saved_objects[0].id - ) - : null, - ]); + const siemNotificationsExist = siemNotification != null && siemNotification.data.length > 0; + const legacyRuleNotificationSOsExist = + legacyRuleActionsSO != null && legacyRuleActionsSO.saved_objects.length > 0; + + // Assumption: if no legacy sidecar SO or notification rule types exist + // that reference the rule in question, assume rule actions are not legacy + if (!siemNotificationsExist && !legacyRuleNotificationSOsExist) { + return rule; + } + // If the legacy notification rule type ("siem.notification") exist, + // migration and cleanup are needed + if (siemNotificationsExist) { + await rulesClient.delete({ id: siemNotification.data[0].id }); + } + // If legacy notification sidecar ("siem-detection-engine-rule-actions") + // exist, migration and cleanup are needed + if (legacyRuleNotificationSOsExist) { + // Delete the legacy sidecar SO + await savedObjectsClient.delete( + legacyRuleActionsSavedObjectType, + legacyRuleActionsSO.saved_objects[0].id + ); + + // If "siem-detection-engine-rule-actions" notes that `ruleThrottle` is + // "no_actions" or "rule", rule has no actions or rule is set to run + // action on every rule run. In these cases, sidecar deletion is the only + // cleanup needed and updates to the "throttle" and "notifyWhen". "siem.notification" are + // not created for these action types + if ( + legacyRuleActionsSO.saved_objects[0].attributes.ruleThrottle === 'no_actions' || + legacyRuleActionsSO.saved_objects[0].attributes.ruleThrottle === 'rule' + ) { + return rule; + } + + // Use "legacyRuleActionsSO" instead of "siemNotification" as "siemNotification" is not created + // until a rule is run and added to task manager. That means that if by chance a user has a rule + // with actions which they have yet to enable, the actions would be lost. Instead, + // "legacyRuleActionsSO" is created on rule creation (pre 7.15) and we can rely on it to be there + const migratedRule = getUpdatedActionsParams({ + rule, + ruleThrottle: legacyRuleActionsSO.saved_objects[0].attributes.ruleThrottle, + actions: legacyRuleActionsSO.saved_objects[0].attributes.actions, + references: legacyRuleActionsSO.saved_objects[0].references, + }); - const { id, ...restOfRule } = rule; - const migratedRule = { - ...restOfRule, - actions: siemNotification.data[0].actions, - throttle: siemNotification.data[0].schedule.interval, - notifyWhen: transformToNotifyWhen(siemNotification.data[0].throttle), - }; await rulesClient.update({ id: rule.id, data: migratedRule, }); + return { id: rule.id, ...migratedRule }; } - return rule; }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules.ts index 69cbd9fd4d4c7..f138bad4b9d2c 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules.ts @@ -20,6 +20,7 @@ import { getSimpleRuleOutput, getSimpleRuleOutputWithoutRuleId, getSimpleRuleWithoutRuleId, + getSlackAction, getWebHookAction, removeServerGeneratedProperties, removeServerGeneratedPropertiesIncludingRuleId, @@ -148,7 +149,7 @@ export default ({ getService }: FtrProviderContext): void => { const { body: hookAction } = await supertest .post('/api/actions/action') .set('kbn-xsrf', 'true') - .send(getWebHookAction()) + .send(getSlackAction()) .expect(200); // create a rule without actions diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules_bulk.ts index 29f5d5edf49d9..d05dcb690994c 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules_bulk.ts @@ -20,6 +20,7 @@ import { getSimpleRuleOutput, getSimpleRuleOutputWithoutRuleId, getSimpleRuleWithoutRuleId, + getSlackAction, getWebHookAction, removeServerGeneratedProperties, removeServerGeneratedPropertiesIncludingRuleId, @@ -277,7 +278,7 @@ export default ({ getService }: FtrProviderContext): void => { const { body: hookAction } = await supertest .post('/api/actions/action') .set('kbn-xsrf', 'true') - .send(getWebHookAction()) + .send(getSlackAction()) .expect(200); // create a rule without actions @@ -318,12 +319,12 @@ export default ({ getService }: FtrProviderContext): void => { const { body: hookAction1 } = await supertest .post('/api/actions/action') .set('kbn-xsrf', 'true') - .send(getWebHookAction()) + .send(getSlackAction()) .expect(200); const { body: hookAction2 } = await supertest .post('/api/actions/action') .set('kbn-xsrf', 'true') - .send(getWebHookAction()) + .send(getSlackAction()) .expect(200); // create 2 rules without actions diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts index a3c4dd8ed3be1..9a61569ada3b0 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts @@ -13,6 +13,9 @@ export default ({ loadTestFile }: FtrProviderContext): void => { describe('', function () { this.tags('ciGroup11'); + // !!NOTE: 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. loadTestFile(require.resolve('./aliases')); loadTestFile(require.resolve('./add_actions')); loadTestFile(require.resolve('./update_actions')); @@ -33,6 +36,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./get_rule_execution_events')); 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')); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/legacy_actions_migrations.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/legacy_actions_migrations.ts new file mode 100644 index 0000000000000..ad6e45954e06a --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/legacy_actions_migrations.ts @@ -0,0 +1,322 @@ +/* + * 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') + .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('onActiveAlert'); + }); + + 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') + .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'], + }, + }, + ]); + expect(ruleSO?.alert.throttle).to.eql(null); + expect(ruleSO?.alert.notifyWhen).to.eql('onActiveAlert'); + 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 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') + .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', + }, + { + actionTypeId: '.slack', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + }, + actionRef: 'action_1', + group: 'default', + }, + ]); + expect(ruleSO?.alert.throttle).to.eql('1h'); + expect(ruleSO?.alert.notifyWhen).to.eql('onThrottleInterval'); + 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') + .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'], + }, + }, + ]); + expect(ruleSO?.alert.throttle).to.eql('1d'); + expect(ruleSO?.alert.notifyWhen).to.eql('onThrottleInterval'); + 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') + .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'], + }, + }, + ]); + expect(ruleSO?.alert.throttle).to.eql('7d'); + expect(ruleSO?.alert.notifyWhen).to.eql('onThrottleInterval'); + 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/utils/get_legacy_action_notification_so.ts b/x-pack/test/detection_engine_api_integration/utils/get_legacy_action_notification_so.ts new file mode 100644 index 0000000000000..9a084d800a2d8 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/utils/get_legacy_action_notification_so.ts @@ -0,0 +1,27 @@ +/* + * 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 { Client } from '@elastic/elasticsearch'; +import { SavedObjectReference } from '@kbn/core/server'; +import type { SearchResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { LegacyRuleNotificationAlertTypeParams } from '@kbn/security-solution-plugin/server/lib/detection_engine/notifications/legacy_types'; + +interface LegacyActionNotificationSO extends LegacyRuleNotificationAlertTypeParams { + references: SavedObjectReference[]; +} + +/** + * Fetch all legacy action sidecar notification SOs from the .kibana index + * @param es The ElasticSearch service + */ +export const getLegacyActionNotificationSO = async ( + es: Client +): Promise> => + es.search({ + index: '.kibana', + q: 'alert.alertTypeId:siem.notifications', + }); diff --git a/x-pack/test/detection_engine_api_integration/utils/get_legacy_action_notifications_so_by_id.ts b/x-pack/test/detection_engine_api_integration/utils/get_legacy_action_notifications_so_by_id.ts new file mode 100644 index 0000000000000..3120e85e899bf --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/utils/get_legacy_action_notifications_so_by_id.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 type { Client } from '@elastic/elasticsearch'; +import { SavedObjectReference } from '@kbn/core/server'; +import type { SearchResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { LegacyRuleNotificationAlertTypeParams } from '@kbn/security-solution-plugin/server/lib/detection_engine/notifications/legacy_types'; + +interface LegacyActionNotificationSO extends LegacyRuleNotificationAlertTypeParams { + references: SavedObjectReference[]; +} + +/** + * Fetch legacy action sidecar notification SOs from the .kibana index + * @param es The ElasticSearch service + * @param id SO id + */ +export const getLegacyActionNotificationSOById = async ( + es: Client, + id: string +): Promise> => + es.search({ + index: '.kibana', + q: `alert.alertTypeId:siem.notifications AND alert.params.ruleAlertId:"${id}"`, + }); diff --git a/x-pack/test/detection_engine_api_integration/utils/get_legacy_actions_so_by_id.ts b/x-pack/test/detection_engine_api_integration/utils/get_legacy_actions_so_by_id.ts new file mode 100644 index 0000000000000..48ea142aa28ca --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/utils/get_legacy_actions_so_by_id.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 type { Client } from '@elastic/elasticsearch'; +import { SavedObjectReference } from '@kbn/core/server'; +import type { SearchResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { LegacyRuleActions } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_actions/legacy_types'; + +interface LegacyActionSO extends LegacyRuleActions { + references: SavedObjectReference[]; +} + +/** + * Fetch legacy action sidecar SOs from the .kibana index + * @param es The ElasticSearch service + * @param id SO id + */ +export const getLegacyActionSOById = async ( + es: Client, + id: string +): Promise> => + es.search({ + index: '.kibana', + q: `type:siem-detection-engine-rule-actions AND _id:"siem-detection-engine-rule-actions:${id}"`, + }); diff --git a/x-pack/test/detection_engine_api_integration/utils/get_rule_so_by_id.ts b/x-pack/test/detection_engine_api_integration/utils/get_rule_so_by_id.ts new file mode 100644 index 0000000000000..5a46f96cc400a --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/utils/get_rule_so_by_id.ts @@ -0,0 +1,27 @@ +/* + * 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 { Client } from '@elastic/elasticsearch'; +import { SavedObjectReference } from '@kbn/core/server'; +import type { SearchResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { Rule } from '@kbn/alerting-plugin/common'; + +interface RuleSO { + alert: Rule; + references: SavedObjectReference[]; +} + +/** + * Fetch legacy action sidecar SOs from the .kibana index + * @param es The ElasticSearch service + * @param id SO id + */ +export const getRuleSOById = async (es: Client, id: string): Promise> => + es.search({ + index: '.kibana', + q: `type:alert AND _id:"alert:${id}"`, + }); diff --git a/x-pack/test/detection_engine_api_integration/utils/get_slack_action.ts b/x-pack/test/detection_engine_api_integration/utils/get_slack_action.ts new file mode 100644 index 0000000000000..1d88f2cdbce73 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/utils/get_slack_action.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const getSlackAction = () => ({ + actionTypeId: '.slack', + secrets: { + webhookUrl: 'http://localhost:123', + }, + name: 'Slack connector', +}); diff --git a/x-pack/test/detection_engine_api_integration/utils/index.ts b/x-pack/test/detection_engine_api_integration/utils/index.ts index 3fb9c8ebafc02..81bad1b1b583b 100644 --- a/x-pack/test/detection_engine_api_integration/utils/index.ts +++ b/x-pack/test/detection_engine_api_integration/utils/index.ts @@ -35,7 +35,10 @@ export * from './get_detection_metrics_from_body'; export * from './get_eql_rule_for_signal_testing'; export * from './get_event_log_execute_complete_by_id'; export * from './get_index_name_from_load'; +export * from './get_legacy_action_notification_so'; +export * from './get_legacy_action_notifications_so_by_id'; export * from './get_legacy_action_so'; +export * from './get_legacy_actions_so_by_id'; export * from './get_open_signals'; export * from './get_prepackaged_rule_status'; export * from './get_query_all_signals'; @@ -44,6 +47,7 @@ export * from './get_query_signals_ids'; export * from './get_query_signals_rule_id'; export * from './get_rule'; export * from './get_rule_for_signal_testing'; +export * from './get_rule_so_by_id'; export * from './get_rule_for_signal_testing_with_timestamp_override'; export * from './get_rule_with_web_hook_action'; export * from './get_saved_query_rule_for_signal_testing'; @@ -70,6 +74,7 @@ export * from './get_stats'; export * from './get_stats_url'; export * from './get_threat_match_rule_for_signal_testing'; export * from './get_threshold_rule_for_signal_testing'; +export * from './get_slack_action'; export * from './get_web_hook_action'; export * from './index_event_log_execution_events'; export * from './install_prepackaged_rules'; diff --git a/x-pack/test/functional/es_archives/security_solution/legacy_actions/data.json b/x-pack/test/functional/es_archives/security_solution/legacy_actions/data.json new file mode 100644 index 0000000000000..25aa371e02144 --- /dev/null +++ b/x-pack/test/functional/es_archives/security_solution/legacy_actions/data.json @@ -0,0 +1,1064 @@ +{ + "type": "doc", + "value": { + "index" : ".kibana", + "id" : "action:c95cb100-b075-11ec-bb3f-1f063f8e06cf", + "source" : { + "action" : { + "actionTypeId" : ".email", + "name" : "test", + "isMissingSecrets" : false, + "config" : { + "hasAuth" : true, + "from" : "a@a.com", + "host" : "gmail", + "port" : 56, + "service" : "other", + "secure" : null + }, + "secrets" : "jCv0fZczCM+rlWXIkI6Qo/ZqasgxPhplokCaNeXJrJlJYjKeCgbZgiLAEue4bpq4GUDohfye8o1BlJCQi+3HJPb2Qp6lhbcvXn+C6wUzxj9ZitwY8ZTDo34e22TX7qHFs/1mVfO4stjZ6X86ufRZttjTNu4qjchfi3Jao7nWbw==" + }, + "type" : "action", + "references" : [ ], + "namespaces" : [ + "default" + ], + "migrationVersion" : { + "action" : "8.0.0" + }, + "coreMigrationVersion" : "8.1.2", + "updated_at" : "2022-03-30T22:07:27.777Z" + } + } +} + +{ + "type": "doc", + "value": { + "index" : ".kibana", + "id" : "action:207fa0e0-c04e-11ec-8a52-4fb92379525a", + "source" : { + "action" : { + "actionTypeId" : ".slack", + "name" : "Slack connector", + "isMissingSecrets" : false, + "config" : { }, + "secrets" : "WTpvAWxAnR1yHdFqQrmGGeOmqGzeIDQnjGexz7F3JdFVEH0CJkPbiyEk9SiiBr1OPZ3s0LWqphjNREjyxSTCTuYTKaEIQ1+mJFQzjIulekMZklQdon7kNh4hOVHW832IX1Lpx9f5cgAPQst2bqJCehz+3BeZOh17Hgrvb/0zzKRCR4cvlhYbOUNUCNR/d6YoEniwUK8pO9xcel9hle7XY6MNfBGiMX8T41O87lvgp9xDjch3LpwqzDjUIFs=" + }, + "type" : "action", + "references" : [ ], + "namespaces" : [ + "default" + ], + "migrationVersion" : { + "action" : "8.0.0" + }, + "coreMigrationVersion" : "8.1.2", + "updated_at" : "2022-03-30T22:07:27.777Z" + } + } +} + +{ + "type": "doc", + "value": { + "index" : ".kibana", + "id" : "alert:9095ee90-b075-11ec-bb3f-1f063f8e06cf", + "source" : { + "alert" : { + "name" : "test w/ no actions", + "tags" : [ + "__internal_rule_id:2297be91-894c-4831-830f-b424a0ec84f0", + "__internal_immutable:false", + "auto_disabled_8.0" + ], + "alertTypeId" : "siem.queryRule", + "consumer" : "siem", + "params" : { + "author" : [ ], + "description" : "a", + "ruleId" : "2297be91-894c-4831-830f-b424a0ec84f0", + "falsePositives" : [ ], + "from" : "now-360s", + "immutable" : false, + "license" : "", + "outputIndex" : "", + "meta" : { + "from" : "1m", + "kibana_siem_app_url" : "https://actions.kb.us-central1.gcp.cloud.es.io:9243/app/security" + }, + "maxSignals" : 100, + "riskScore" : 21, + "riskScoreMapping" : [ ], + "severity" : "low", + "severityMapping" : [ ], + "threat" : [ ], + "to" : "now", + "references" : [ ], + "version" : 1, + "exceptionsList" : [ ], + "type" : "query", + "language" : "kuery", + "index" : [ + "apm-*-transaction*", + "traces-apm*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*" + ], + "query" : "*:*", + "filters" : [ ] + }, + "schedule" : { + "interval" : "5m" + }, + "enabled" : true, + "actions" : [ ], + "throttle" : null, + "notifyWhen" : "onActiveAlert", + "apiKeyOwner" : null, + "apiKey" : null, + "createdBy" : "1527796724", + "updatedBy" : "1527796724", + "createdAt" : "2022-03-30T22:05:53.511Z", + "updatedAt" : "2022-03-30T22:05:53.511Z", + "muteAll" : false, + "mutedInstanceIds" : [ ], + "executionStatus" : { + "status" : "ok", + "lastExecutionDate" : "2022-03-31T19:53:37.507Z", + "error" : null, + "lastDuration" : 2377 + }, + "meta" : { + "versionApiKeyLastmodified" : "7.15.2" + }, + "scheduledTaskId" : null, + "legacyId" : "9095ee90-b075-11ec-bb3f-1f063f8e06cf" + }, + "type" : "alert", + "references" : [ ], + "namespaces" : [ + "default" + ], + "migrationVersion" : { + "alert" : "8.0.1" + }, + "coreMigrationVersion" : "8.1.2", + "updated_at" : "2022-03-31T19:53:39.885Z" + } + } +} + +{ + "type": "doc", + "value": { + "index" : ".kibana", + "id" : "alert:dc6595f0-b075-11ec-bb3f-1f063f8e06cf", + "source" : { + "alert" : { + "name" : "test w/ action - every rule run", + "tags" : [ + "__internal_rule_id:72a0d429-363b-4f70-905e-c6019a224d40", + "__internal_immutable:false", + "auto_disabled_8.0" + ], + "alertTypeId" : "siem.queryRule", + "consumer" : "siem", + "params" : { + "author" : [ ], + "description" : "a", + "ruleId" : "72a0d429-363b-4f70-905e-c6019a224d40", + "falsePositives" : [ ], + "from" : "now-360s", + "immutable" : false, + "license" : "", + "outputIndex" : "", + "meta" : { + "from" : "1m", + "kibana_siem_app_url" : "https://actions.kb.us-central1.gcp.cloud.es.io:9243/app/security" + }, + "maxSignals" : 100, + "riskScore" : 21, + "riskScoreMapping" : [ ], + "severity" : "low", + "severityMapping" : [ ], + "threat" : [ ], + "to" : "now", + "references" : [ ], + "version" : 1, + "exceptionsList" : [ ], + "type" : "query", + "language" : "kuery", + "index" : [ + "apm-*-transaction*", + "traces-apm*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*" + ], + "query" : "*:* ", + "filters" : [ ] + }, + "schedule" : { + "interval" : "5m" + }, + "enabled" : true, + "actions" : [ + { + "group" : "default", + "params" : { + "message" : "Rule {{context.rule.name}} generated {{state.signals_count}} alerts", + "to" : [ + "test@test.com" + ], + "subject" : "Test Actions" + }, + "actionTypeId" : ".email", + "actionRef" : "action_0" + } + ], + "throttle" : null, + "notifyWhen" : "onActiveAlert", + "apiKeyOwner" : null, + "apiKey" : null, + "createdBy" : "1527796724", + "updatedBy" : "1527796724", + "createdAt" : "2022-03-30T22:08:00.172Z", + "updatedAt" : "2022-03-30T22:08:00.172Z", + "muteAll" : false, + "mutedInstanceIds" : [ ], + "executionStatus" : { + "status" : "ok", + "lastExecutionDate" : "2022-03-31T19:55:43.502Z", + "error" : null, + "lastDuration" : 2700 + }, + "meta" : { + "versionApiKeyLastmodified" : "7.15.2" + }, + "scheduledTaskId" : null, + "legacyId" : "dc6595f0-b075-11ec-bb3f-1f063f8e06cf" + }, + "type" : "alert", + "references" : [ + { + "id" : "c95cb100-b075-11ec-bb3f-1f063f8e06cf", + "name" : "action_0", + "type" : "action" + } + ], + "namespaces" : [ + "default" + ], + "migrationVersion" : { + "alert" : "8.0.1" + }, + "coreMigrationVersion" : "8.1.2", + "updated_at" : "2022-03-31T19:55:46.202Z" + } + } +} + +{ + "type": "doc", + "value": { + "index" : ".kibana", + "id" : "alert:064e3160-b076-11ec-bb3f-1f063f8e06cf", + "source" : { + "alert" : { + "name" : "test w/ action - every hour", + "tags" : [ + "__internal_rule_id:4c056b05-75ac-4209-be32-82100f771eb4", + "__internal_immutable:false", + "auto_disabled_8.0" + ], + "alertTypeId" : "siem.queryRule", + "consumer" : "siem", + "params" : { + "author" : [ ], + "description" : "a", + "ruleId" : "4c056b05-75ac-4209-be32-82100f771eb4", + "falsePositives" : [ ], + "from" : "now-360s", + "immutable" : false, + "license" : "", + "outputIndex" : "", + "meta" : { + "from" : "1m", + "kibana_siem_app_url" : "https://actions.kb.us-central1.gcp.cloud.es.io:9243/app/security" + }, + "maxSignals" : 100, + "riskScore" : 21, + "riskScoreMapping" : [ ], + "severity" : "low", + "severityMapping" : [ ], + "threat" : [ ], + "to" : "now", + "references" : [ ], + "version" : 1, + "exceptionsList" : [ ], + "type" : "query", + "language" : "kuery", + "index" : [ + "apm-*-transaction*", + "traces-apm*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*" + ], + "query" : "*:*", + "filters" : [ ] + }, + "schedule" : { + "interval" : "5m" + }, + "enabled" : true, + "actions" : [ ], + "throttle" : null, + "notifyWhen" : "onActiveAlert", + "apiKeyOwner" : null, + "apiKey" : null, + "createdBy" : "1527796724", + "updatedBy" : "1527796724", + "createdAt" : "2022-03-30T22:09:10.261Z", + "updatedAt" : "2022-03-30T22:09:10.261Z", + "muteAll" : false, + "mutedInstanceIds" : [ ], + "executionStatus" : { + "status" : "ok", + "lastExecutionDate" : "2022-03-31T19:56:43.499Z", + "error" : null, + "lastDuration" : 2815 + }, + "meta" : { + "versionApiKeyLastmodified" : "7.15.2" + }, + "scheduledTaskId" : null, + "legacyId" : "064e3160-b076-11ec-bb3f-1f063f8e06cf" + }, + "type" : "alert", + "references" : [ ], + "namespaces" : [ + "default" + ], + "migrationVersion" : { + "alert" : "8.0.1" + }, + "coreMigrationVersion" : "8.1.2", + "updated_at" : "2022-03-31T19:56:46.314Z" + } + } +} + +{ + "type": "doc", + "value": { + "index" : ".kibana", + "id" : "alert:27639570-b076-11ec-bb3f-1f063f8e06cf", + "source" : { + "alert" : { + "name" : "test w/ action - daily", + "tags" : [ + "__internal_rule_id:8e2c8550-f13f-4e21-be0c-92148d71a5f1", + "__internal_immutable:false", + "auto_disabled_8.0" + ], + "alertTypeId" : "siem.queryRule", + "consumer" : "siem", + "params" : { + "author" : [ ], + "description" : "a", + "ruleId" : "8e2c8550-f13f-4e21-be0c-92148d71a5f1", + "falsePositives" : [ ], + "from" : "now-360s", + "immutable" : false, + "license" : "", + "outputIndex" : "", + "meta" : { + "from" : "1m", + "kibana_siem_app_url" : "https://actions.kb.us-central1.gcp.cloud.es.io:9243/app/security" + }, + "maxSignals" : 100, + "riskScore" : 21, + "riskScoreMapping" : [ ], + "severity" : "low", + "severityMapping" : [ ], + "threat" : [ ], + "to" : "now", + "references" : [ ], + "version" : 1, + "exceptionsList" : [ ], + "type" : "query", + "language" : "kuery", + "index" : [ + "apm-*-transaction*", + "traces-apm*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*" + ], + "query" : "*:*", + "filters" : [ ] + }, + "schedule" : { + "interval" : "5m" + }, + "enabled" : true, + "actions" : [ ], + "throttle" : null, + "notifyWhen" : "onActiveAlert", + "apiKeyOwner" : null, + "apiKey" : null, + "createdBy" : "1527796724", + "updatedBy" : "1527796724", + "createdAt" : "2022-03-30T22:10:06.358Z", + "updatedAt" : "2022-03-30T22:10:06.358Z", + "muteAll" : false, + "mutedInstanceIds" : [ ], + "executionStatus" : { + "status" : "ok", + "lastExecutionDate" : "2022-03-31T19:58:13.480Z", + "error" : null, + "lastDuration" : 3023 + }, + "meta" : { + "versionApiKeyLastmodified" : "7.15.2" + }, + "scheduledTaskId" : null, + "legacyId" : "27639570-b076-11ec-bb3f-1f063f8e06cf" + }, + "type" : "alert", + "references" : [ ], + "namespaces" : [ + "default" + ], + "migrationVersion" : { + "alert" : "8.0.1" + }, + "coreMigrationVersion" : "8.1.2", + "updated_at" : "2022-03-31T19:58:16.503Z" + } + } +} + +{ + "type": "doc", + "value": { + "index" : ".kibana", + "id" : "alert:61ec7a40-b076-11ec-bb3f-1f063f8e06cf", + "source" : { + "alert" : { + "name" : "test w/ actions - weekly", + "tags" : [ + "__internal_rule_id:05fbdd2a-e802-420b-bdc3-95ae0acca454", + "__internal_immutable:false", + "auto_disabled_8.0" + ], + "alertTypeId" : "siem.queryRule", + "consumer" : "siem", + "params" : { + "author" : [ ], + "description" : "a", + "ruleId" : "05fbdd2a-e802-420b-bdc3-95ae0acca454", + "falsePositives" : [ ], + "from" : "now-360s", + "immutable" : false, + "license" : "", + "outputIndex" : "", + "meta" : { + "from" : "1m", + "kibana_siem_app_url" : "https://actions.kb.us-central1.gcp.cloud.es.io:9243/app/security" + }, + "maxSignals" : 100, + "riskScore" : 21, + "riskScoreMapping" : [ ], + "severity" : "low", + "severityMapping" : [ ], + "threat" : [ ], + "to" : "now", + "references" : [ ], + "version" : 1, + "exceptionsList" : [ ], + "type" : "query", + "language" : "kuery", + "index" : [ + "apm-*-transaction*", + "traces-apm*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*" + ], + "query" : "*:*", + "filters" : [ ] + }, + "schedule" : { + "interval" : "5m" + }, + "enabled" : true, + "actions" : [ ], + "throttle" : null, + "notifyWhen" : "onActiveAlert", + "apiKeyOwner" : null, + "apiKey" : null, + "createdBy" : "1527796724", + "updatedBy" : "1527796724", + "createdAt" : "2022-03-30T22:11:44.516Z", + "updatedAt" : "2022-03-30T22:11:44.516Z", + "muteAll" : false, + "mutedInstanceIds" : [ ], + "executionStatus" : { + "status" : "ok", + "lastExecutionDate" : "2022-03-31T19:54:22.442Z", + "error" : null, + "lastDuration" : 2612 + }, + "meta" : { + "versionApiKeyLastmodified" : "7.15.2" + }, + "scheduledTaskId" : null, + "legacyId" : "61ec7a40-b076-11ec-bb3f-1f063f8e06cf" + }, + "type" : "alert", + "references" : [ ], + "namespaces" : [ + "default" + ], + "migrationVersion" : { + "alert" : "8.0.1" + }, + "coreMigrationVersion" : "8.1.2", + "updated_at" : "2022-03-31T19:54:25.054Z" + } + } +} + +{ + "type": "doc", + "value": { + "index" : ".kibana", + "id" : "alert:64492ef0-b076-11ec-bb3f-1f063f8e06cf", + "source" : { + "alert" : { + "name" : "test w/ actions - weekly", + "tags" : [ + "__internal_rule_alert_id:61ec7a40-b076-11ec-bb3f-1f063f8e06cf" + ], + "alertTypeId" : "siem.notifications", + "consumer" : "siem", + "params" : { + "ruleAlertId" : "61ec7a40-b076-11ec-bb3f-1f063f8e06cf" + }, + "schedule" : { + "interval" : "7d" + }, + "enabled" : true, + "actions" : [ + { + "group" : "default", + "params" : { + "message" : "Rule {{context.rule.name}} generated {{state.signals_count}} alerts", + "to" : [ + "test@test.com" + ], + "subject" : "Test Actions" + }, + "actionTypeId" : ".email", + "actionRef" : "action_0" + } + ], + "throttle" : null, + "notifyWhen" : "onActiveAlert", + "apiKeyOwner" : null, + "apiKey" : null, + "createdBy" : "1527796724", + "updatedBy" : "1527796724", + "createdAt" : "2022-03-30T22:11:48.661Z", + "updatedAt" : "2022-03-30T22:11:48.661Z", + "muteAll" : false, + "mutedInstanceIds" : [ ], + "executionStatus" : { + "status" : "ok", + "lastExecutionDate" : "2022-03-30T22:11:51.640Z", + "error" : null + }, + "meta" : { + "versionApiKeyLastmodified" : "7.15.2" + }, + "scheduledTaskId" : null, + "legacyId" : "64492ef0-b076-11ec-bb3f-1f063f8e06cf" + }, + "type" : "alert", + "references" : [ + { + "id" : "c95cb100-b075-11ec-bb3f-1f063f8e06cf", + "name" : "action_0", + "type" : "action" + }, + { + "id" : "61ec7a40-b076-11ec-bb3f-1f063f8e06cf", + "name" : "param:alert_0", + "type" : "alert" + } + ], + "namespaces" : [ + "default" + ], + "migrationVersion" : { + "alert" : "8.0.1" + }, + "coreMigrationVersion" : "8.1.2", + "updated_at" : "2022-03-30T22:11:51.707Z" + } + } +} + +{ + "type": "doc", + "value": { + "index" : ".kibana", + "id" : "alert:d42e8210-b076-11ec-bb3f-1f063f8e06cf", + "source" : { + "alert" : { + "name" : "test w/ action - every hour", + "tags" : [ + "__internal_rule_alert_id:064e3160-b076-11ec-bb3f-1f063f8e06cf" + ], + "alertTypeId" : "siem.notifications", + "consumer" : "siem", + "params" : { + "ruleAlertId" : "064e3160-b076-11ec-bb3f-1f063f8e06cf" + }, + "schedule" : { + "interval" : "1h" + }, + "enabled" : true, + "actions" : [ + { + "group" : "default", + "params" : { + "message" : "Rule {{context.rule.name}} generated {{state.signals_count}} alerts", + "to" : [ + "test@test.com" + ], + "subject" : "Rule email" + }, + "actionTypeId" : ".email", + "actionRef" : "action_0" + }, + { + "group" : "default", + "params" : { + "message" : "Rule {{context.rule.name}} generated {{state.signals_count}} alerts" + }, + "actionTypeId" : ".slack", + "actionRef" : "action_1" + } + ], + "throttle" : null, + "notifyWhen" : "onActiveAlert", + "apiKeyOwner" : null, + "apiKey" : null, + "createdBy" : "1527796724", + "updatedBy" : "1527796724", + "createdAt" : "2022-03-30T22:14:56.318Z", + "updatedAt" : "2022-03-30T22:15:12.135Z", + "muteAll" : false, + "mutedInstanceIds" : [ ], + "executionStatus" : { + "status" : "pending", + "lastExecutionDate" : "2022-03-30T22:14:56.318Z", + "error" : null + }, + "meta" : { + "versionApiKeyLastmodified" : "7.15.2" + }, + "legacyId" : "d42e8210-b076-11ec-bb3f-1f063f8e06cf" + }, + "type" : "alert", + "references" : [ + { + "id" : "c95cb100-b075-11ec-bb3f-1f063f8e06cf", + "name" : "action_0", + "type" : "action" + }, + { + "id" : "207fa0e0-c04e-11ec-8a52-4fb92379525a", + "name" : "action_1", + "type" : "action" + }, + { + "id" : "064e3160-b076-11ec-bb3f-1f063f8e06cf", + "name" : "param:alert_0", + "type" : "alert" + } + ], + "namespaces" : [ + "default" + ], + "migrationVersion" : { + "alert" : "8.0.1" + }, + "coreMigrationVersion" : "8.1.2", + "updated_at" : "2022-03-30T22:15:12.135Z" + } + } +} + +{ + "type": "doc", + "value": { + "index" : ".kibana", + "id" : "alert:29ba2fa0-b076-11ec-bb3f-1f063f8e06cf", + "source" : { + "alert" : { + "name" : "test w/ action - daily", + "tags" : [ + "__internal_rule_alert_id:27639570-b076-11ec-bb3f-1f063f8e06cf" + ], + "alertTypeId" : "siem.notifications", + "consumer" : "siem", + "params" : { + "ruleAlertId" : "27639570-b076-11ec-bb3f-1f063f8e06cf" + }, + "schedule" : { + "interval" : "1d" + }, + "enabled" : true, + "actions" : [ + { + "group" : "default", + "params" : { + "message" : "Rule {{context.rule.name}} generated {{state.signals_count}} alerts", + "to" : [ + "test@test.com" + ], + "subject" : "Test Actions" + }, + "actionTypeId" : ".email", + "actionRef" : "action_0" + } + ], + "throttle" : null, + "notifyWhen" : "onActiveAlert", + "apiKeyOwner" : null, + "apiKey" : null, + "createdBy" : "1527796724", + "updatedBy" : "1527796724", + "createdAt" : "2022-03-30T22:10:10.435Z", + "updatedAt" : "2022-03-30T22:10:10.435Z", + "muteAll" : false, + "mutedInstanceIds" : [ ], + "executionStatus" : { + "status" : "ok", + "lastExecutionDate" : "2022-04-01T22:10:15.478Z", + "error" : null, + "lastDuration" : 984 + }, + "meta" : { + "versionApiKeyLastmodified" : "7.15.2" + }, + "scheduledTaskId" : null, + "legacyId" : "29ba2fa0-b076-11ec-bb3f-1f063f8e06cf", + "monitoring" : { + "execution" : { + "history" : [ + { + "duration" : 111, + "success" : true, + "timestamp" : 1648764614215 + }, + { + "duration" : 984, + "success" : true, + "timestamp" : 1648851016462 + } + ], + "calculated_metrics" : { + "p99" : 984, + "success_ratio" : 1, + "p50" : 547.5, + "p95" : 984 + } + } + } + }, + "type" : "alert", + "references" : [ + { + "id" : "c95cb100-b075-11ec-bb3f-1f063f8e06cf", + "name" : "action_0", + "type" : "action" + }, + { + "id" : "27639570-b076-11ec-bb3f-1f063f8e06cf", + "name" : "param:alert_0", + "type" : "alert" + } + ], + "namespaces" : [ + "default" + ], + "migrationVersion" : { + "alert" : "8.0.1" + }, + "coreMigrationVersion" : "8.1.2", + "updated_at" : "2022-04-01T22:10:16.467Z" + } + } +} + +{ + "type": "doc", + "value": { + "index" : ".kibana", + "id" : "siem-detection-engine-rule-actions:926668d0-b075-11ec-bb3f-1f063f8e06cf", + "source" : { + "siem-detection-engine-rule-actions" : { + "actions" : [ ], + "ruleThrottle" : "no_actions", + "alertThrottle" : null + }, + "type" : "siem-detection-engine-rule-actions", + "references" : [ + { + "id" : "9095ee90-b075-11ec-bb3f-1f063f8e06cf", + "type" : "alert", + "name" : "alert_0" + } + ], + "namespaces" : [ + "default" + ], + "migrationVersion" : { + "siem-detection-engine-rule-actions" : "8.0.0" + }, + "coreMigrationVersion" : "8.1.2", + "updated_at" : "2022-03-30T22:05:55.563Z" + } + } +} + +{ + "type": "doc", + "value": { + "index" : ".kibana", + "id" : "siem-detection-engine-rule-actions:dde13970-b075-11ec-bb3f-1f063f8e06cf", + "source" : { + "siem-detection-engine-rule-actions" : { + "actions" : [ + { + "actionRef" : "action_0", + "group" : "default", + "params" : { + "message" : "Rule {{context.rule.name}} generated {{state.signals_count}} alerts", + "to" : [ + "test@test.com" + ], + "subject" : "Test Actions" + }, + "action_type_id" : ".email" + } + ], + "ruleThrottle" : "rule", + "alertThrottle" : null + }, + "type" : "siem-detection-engine-rule-actions", + "references" : [ + { + "id" : "dc6595f0-b075-11ec-bb3f-1f063f8e06cf", + "type" : "alert", + "name" : "alert_0" + }, + { + "id" : "c95cb100-b075-11ec-bb3f-1f063f8e06cf", + "type" : "action", + "name" : "action_0" + } + ], + "namespaces" : [ + "default" + ], + "migrationVersion" : { + "siem-detection-engine-rule-actions" : "8.0.0" + }, + "coreMigrationVersion" : "8.1.2", + "updated_at" : "2022-03-30T22:08:02.207Z" + } + } +} + +{ + "type": "doc", + "value": { + "index" : ".kibana", + "id" : "siem-detection-engine-rule-actions:07aa8d10-b076-11ec-bb3f-1f063f8e06cf", + "source" : { + "siem-detection-engine-rule-actions" : { + "actions" : [ + { + "actionRef" : "action_0", + "group" : "default", + "params" : { + "message" : "Rule {{context.rule.name}} generated {{state.signals_count}} alerts", + "to" : [ + "test@test.com" + ], + "subject" : "Rule email" + }, + "action_type_id" : ".email" + }, + { + "actionRef" : "action_1", + "group" : "default", + "params" : { + "message" : "Rule {{context.rule.name}} generated {{state.signals_count}} alerts" + }, + "action_type_id" : ".slack" + } + ], + "ruleThrottle" : "1h", + "alertThrottle" : "1h" + }, + "type" : "siem-detection-engine-rule-actions", + "references" : [ + { + "id" : "064e3160-b076-11ec-bb3f-1f063f8e06cf", + "type" : "alert", + "name" : "alert_0" + }, + { + "id" : "c95cb100-b075-11ec-bb3f-1f063f8e06cf", + "type" : "action", + "name" : "action_0" + }, + { + "id" : "207fa0e0-c04e-11ec-8a52-4fb92379525a", + "type" : "action", + "name" : "action_1" + } + ], + "namespaces" : [ + "default" + ], + "migrationVersion" : { + "siem-detection-engine-rule-actions" : "8.0.0" + }, + "coreMigrationVersion" : "8.1.2", + "updated_at" : "2022-03-30T22:09:12.300Z" + } + } +} + +{ + "type": "doc", + "value": { + "index" : ".kibana", + "id" : "siem-detection-engine-rule-actions:291ae260-b076-11ec-bb3f-1f063f8e06cf", + "source" : { + "siem-detection-engine-rule-actions" : { + "actions" : [ + { + "actionRef" : "action_0", + "group" : "default", + "params" : { + "message" : "Rule {{context.rule.name}} generated {{state.signals_count}} alerts", + "to" : [ + "test@test.com" + ], + "subject" : "Test Actions" + }, + "action_type_id" : ".email" + } + ], + "ruleThrottle" : "1d", + "alertThrottle" : "1d" + }, + "type" : "siem-detection-engine-rule-actions", + "references" : [ + { + "id" : "27639570-b076-11ec-bb3f-1f063f8e06cf", + "type" : "alert", + "name" : "alert_0" + }, + { + "id" : "c95cb100-b075-11ec-bb3f-1f063f8e06cf", + "type" : "action", + "name" : "action_0" + } + ], + "namespaces" : [ + "default" + ], + "migrationVersion" : { + "siem-detection-engine-rule-actions" : "8.0.0" + }, + "coreMigrationVersion" : "8.1.2", + "updated_at" : "2022-03-30T22:10:08.399Z" + } + } +} + +{ + "type": "doc", + "value": { + "index" : ".kibana", + "id" : "siem-detection-engine-rule-actions:63aa2fd0-b076-11ec-bb3f-1f063f8e06cf", + "source" : { + "siem-detection-engine-rule-actions" : { + "actions" : [ + { + "actionRef" : "action_0", + "group" : "default", + "params" : { + "message" : "Rule {{context.rule.name}} generated {{state.signals_count}} alerts", + "to" : [ + "test@test.com" + ], + "subject" : "Test Actions" + }, + "action_type_id" : ".email" + } + ], + "ruleThrottle" : "7d", + "alertThrottle" : "7d" + }, + "type" : "siem-detection-engine-rule-actions", + "references" : [ + { + "id" : "61ec7a40-b076-11ec-bb3f-1f063f8e06cf", + "type" : "alert", + "name" : "alert_0" + }, + { + "id" : "c95cb100-b075-11ec-bb3f-1f063f8e06cf", + "type" : "action", + "name" : "action_0" + } + ], + "namespaces" : [ + "default" + ], + "migrationVersion" : { + "siem-detection-engine-rule-actions" : "8.0.0" + }, + "coreMigrationVersion" : "8.1.2", + "updated_at" : "2022-03-30T22:11:46.643Z" + } + } +}