From de43a3b83d90d66744d9f71fbe4c742bed9df6af Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Mon, 27 Sep 2021 17:18:03 -0600 Subject: [PATCH] [Security Solutions] Adds back the legacy actions and notification system in a limited fashion (#112869) ## Summary Fixes https://github.com/elastic/security-team/issues/1759 Related earlier PR, https://github.com/elastic/kibana/pull/109722, where these were removed to where they could no longer function. This PR adds them back to where they will function for existing users. The end goal is to have users naturally migrate as they update, enable/disable, or create new rules. What this PR does: * Adds back the legacy side car actions `siem-detection-engine-rule-actions` * Adds back the legacy hidden alert of `siem.notifications` * Adds back unit tests where they existed. Both of these systems did not have existing e2e tests. * Re-adds the find feature and functionality which should show the rules with legacy and non-legacy notifications/side car actions during a REST find operation. * Updates the logic for when to show a legacy vs. non-legacy notification/side car action. * Adds a new route called `/internal/api/detection/legacy/notifications` which is only for developer and tests for us to maintain this system for the foreseeable future. * Adds script to exercise creating old notifications `detection_engine/scripts/post_legacy_notification.sh` * Adds a data file for the script to use as an example for ad-hoc testing, `scripts/legacy_notifications/one_action.json` * Adds within `security_solution/server/types.ts` `ActionsApiRequestHandlerContext` so that if we need to directly access actions within plugins we can. I do not use it here, but it should have been existing there and is good to have it in case we need it at this point within REST routes. * When adding back the files and changes, I use the kibana-core approach of prefixing files, functions, types, etc... with the words `legacyFoo`. The files are named `legacy_foo.ts`. Everything has `@deprecation` above them as well. The intent here is all of this should hopefully make it unambiguously clear which parts of the notification system are for the new system/existing API and which ones are only for the deprecated legacy system. There exists some parts of the system that are used within _both_ and the hope is that we can keep the legacy pieces separate from the non-legacy pieces for strangling the legacy pieces. * This adds a new linter rule to prevent users from easily importing files named `legacy_foo.ts` or `foo_legacy.ts` we are using here and can also use for other similar legacy parts of the system we have. This seems to be the established pattern that kibana-core does as well looking through the linters and code base. * Removes some dead import/export code and types instead of maintaining them since they are no longer used. What this PR does not do (but are planned on follow ups): * This PR does not add migration logic in most conditions such as a user enabling/disabling a rule, editing a rule unless the user is explicitly changing the actions by turning off the notification and then re-adding the notification. * This PR does not log any information indicating to the user that they are running legacy rules or indicates they have that. * This PR does not allow the executors or any UI/UX, backend to re-add a legacy notification. Instead only the hidden REST route of `/internal/api/detection/legacy/notifications` allows us to do this for testing purposes. * This PR does not migrate the data structure of actions legacy notification system `siem-detection-engine-rule-actions` to use saved object references. * If you delete an alert this will not delete the side car if it detects one is present on it. * If you update an alert notification with a new notification this will not remove the side car on the update. **Ad-hoc testing instructions** How to do ad-hoc testing for various situations such as having a legacy notification system such as a user's or if you want to mimic a malfunction and result of a "split-brain" to where you have both notification systems running at the same time due to a bug or regression: Create a rule and activate it normally within security_solution: Screen Shot 2021-09-22 at 2 09 14 PM Do not add actions to the rule at this point as we will first exercise the older legacy system. However, you want at least one action configured such as a slack notification: Screen Shot 2021-09-22 at 2 28 16 PM Within dev tools do a query for all your actions and grab one of the `_id` of them without their prefix: ```json # See all your actions GET .kibana/_search { "query": { "term": { "type": "action" } } } ``` Mine was `"_id" : "action:879e8ff0-1be1-11ec-a722-83da1c22a481",` so I will be copying the ID of `879e8ff0-1be1-11ec-a722-83da1c22a481` Go to the file `detection_engine/scripts/legacy_notifications/one_action.json` and add this id to the file. Something like this: ```json { "name": "Legacy notification with one action", "interval": "1m", <--- You can use whatever you want. Real values are "1h", "1d", "1w". I use "1m" for testing purposes. "actions": [ { "id": "879e8ff0-1be1-11ec-a722-83da1c22a481", <--- My action id "group": "default", "params": { "message": "Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts" }, "actionTypeId": ".slack" <--- I am a slack action id type. } ] } ``` Query for an alert you want to add manually add back a legacy notification to it. Such as: ```json # See all your siem.signals alert types and choose one GET .kibana/_search { "query": { "term": { "alert.alertTypeId": "siem.signals" } } } ``` Grab the `_id` without the `alert` prefix. For mine this was `933ca720-1be1-11ec-a722-83da1c22a481` Within the directory of `detection_engine/scripts` execute the script ```bash ./post_legacy_notification.sh 933ca720-1be1-11ec-a722-83da1c22a481 { "ok": "acknowledged" } ``` which is going to do a few things. See the file `detection_engine/routes/rules/legacy_create_legacy_notification.ts` for the definition of the route and what it does in full, but we should notice that we have now: Created a legacy side car action object of type `siem-detection-engine-rule-actions` you can see in dev tools: ```json # See the actions "side car" which are part of the legacy notification system. GET .kibana/_search { "query": { "term": { "type": { "value": "siem-detection-engine-rule-actions" } } } } ``` Note in the response: ```json "siem-detection-engine-rule-actions" : { "ruleAlertId" : "933ca720-1be1-11ec-a722-83da1c22a481", <--- NOTE, not migrated to references yet "actions" : [ { "action_type_id" : ".slack", "id" : "879e8ff0-1be1-11ec-a722-83da1c22a481", <--- NOTE, not migrated to references yet "params" : { "message" : "Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts" }, "group" : "default" } ], "ruleThrottle" : "1m", <--- Should be the same as the interval in "one_action.json" config "alertThrottle" : "1m" <--- Should be the same as the interval in "one_action.json" config }, "type" : "siem-detection-engine-rule-actions", "references" : [ ], ``` Created a `siem.notification` rule instance which you can see in dev tools as well: ```json # Get the alert type of "siem-notifications" which is part of the legacy system. GET .kibana/_search { "query": { "term": { "alert.alertTypeId": "siem.notifications" } } } ``` Take note from the `siem.notifications` these values which determine how/when it fires and if your actions are set up correctly: ```json "name" : "Legacy notification with one action" <--- Our name from one_action.json "schedule" : { "interval" : "1m" <--- Interval should match interval in one_action.json }, "enabled" : true, <--- We should be enabled "actions" : [ { "group" : "default", "params" : { "message" : "Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts" }, "actionTypeId" : ".slack", <--- Our actionID "actionRef" : "action_0" } ], ``` And that now there exists a task within task manager that will be executing this: ```json # Get the tasks of siem notifications to ensure and see it is running GET .task-manager/_search { "query": { "term": { "task.taskType": "alerting:siem.notifications" } } } ``` You can double check the interval from the result of the query to ensure it runs as the configuration test file shows it should be: ```json "schedule" : { "interval" : "1m" }, ``` Within time you should see your action execute like the legacy notification system: Screen Shot 2021-09-22 at 2 55 28 PM If you go to edit the rule you should notice that the rule now has the side car attached to it within the UI: Screen Shot 2021-09-22 at 8 08 54 PM You can also look at your log messages in debug mode to verify the behaviors of the legacy system and the normal rules running. Compare these data structures to a 7.14.x system in cloud to ensure the data looks the same and the ad-hoc testing functions as expected. Check the scripts of `./find_rules.sh`, `./read_rules.sh` to ensure that the find REST route returns the legacy actions when they are there. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .eslintrc.js | 27 ++ .../security_solution/common/constants.ts | 3 +- .../notifications/legacy_add_tags.test.ts | 28 ++ .../notifications/legacy_add_tags.ts | 14 + .../legacy_create_notifications.test.ts | 76 ++++ .../legacy_create_notifications.ts | 41 +++ .../legacy_find_notifications.test.ts | 24 ++ .../legacy_find_notifications.ts | 45 +++ .../legacy_read_notifications.test.ts | 157 +++++++++ .../legacy_read_notifications.ts | 57 +++ ...gacy_rules_notification_alert_type.test.ts | 255 ++++++++++++++ .../legacy_rules_notification_alert_type.ts | 118 +++++++ .../notifications/legacy_types.ts | 133 +++++++ .../routes/__mocks__/request_responses.ts | 57 +++ .../routes/rules/find_rules_route.ts | 18 +- .../legacy_create_legacy_notification.ts | 110 ++++++ .../routes/rules/read_rules_route.ts | 14 +- .../routes/rules/utils.test.ts | 179 ++++------ .../detection_engine/routes/rules/utils.ts | 67 +--- .../detection_engine/routes/rules/validate.ts | 12 +- .../lib/detection_engine/routes/utils.test.ts | 164 --------- .../lib/detection_engine/routes/utils.ts | 70 ---- ...legacy_create_rule_actions_saved_object.ts | 50 +++ ...gacy_get_bulk_rule_actions_saved_object.ts | 53 +++ .../legacy_get_rule_actions_saved_object.ts | 57 +++ .../{migrations.ts => legacy_migrations.ts} | 19 +- ...ngs.ts => legacy_saved_object_mappings.ts} | 25 +- .../{types.ts => legacy_types.ts} | 20 +- ...ate_or_create_rule_actions_saved_object.ts | 59 ++++ ...legacy_update_rule_actions_saved_object.ts | 69 ++++ .../rule_actions/legacy_utils.ts | 41 +++ .../lib/detection_engine/rules/types.ts | 13 +- .../lib/detection_engine/rules/utils.test.ts | 332 +++++++++++++++--- .../lib/detection_engine/rules/utils.ts | 64 +++- .../schemas/rule_converters.ts | 16 +- .../legacy_notifications/one_action.json | 14 + .../scripts/post_legacy_notification.sh | 25 ++ .../security_solution/server/plugin.ts | 15 +- .../security_solution/server/routes/index.ts | 5 + .../security_solution/server/saved_objects.ts | 5 +- .../plugins/security_solution/server/types.ts | 2 + 41 files changed, 2025 insertions(+), 528 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_add_tags.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_add_tags.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_create_notifications.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_create_notifications.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_find_notifications.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_find_notifications.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_read_notifications.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_read_notifications.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_types.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/legacy_create_legacy_notification.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_create_rule_actions_saved_object.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_get_bulk_rule_actions_saved_object.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_get_rule_actions_saved_object.ts rename x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/{migrations.ts => legacy_migrations.ts} (81%) rename x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/{saved_object_mappings.ts => legacy_saved_object_mappings.ts} (51%) rename x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/{types.ts => legacy_types.ts} (69%) create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_update_or_create_rule_actions_saved_object.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_update_rule_actions_saved_object.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_utils.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/scripts/legacy_notifications/one_action.json create mode 100755 x-pack/plugins/security_solution/server/lib/detection_engine/scripts/post_legacy_notification.sh diff --git a/.eslintrc.js b/.eslintrc.js index 83afc27263248..c78a9f4aca2ac 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -930,6 +930,15 @@ module.exports = { '@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/no-useless-constructor': 'error', '@typescript-eslint/unified-signatures': 'error', + 'no-restricted-imports': [ + 'error', + { + // prevents code from importing files that contain the name "legacy" within their name. This is a mechanism + // to help deprecation and prevent accidental re-use/continued use of code we plan on removing. If you are + // finding yourself turning this off a lot for "new code" consider renaming the file and functions if it is has valid uses. + patterns: ['*legacy*'], + }, + ], }, }, { @@ -1192,6 +1201,15 @@ module.exports = { 'no-template-curly-in-string': 'error', 'sort-keys': 'error', 'prefer-destructuring': 'error', + 'no-restricted-imports': [ + 'error', + { + // prevents code from importing files that contain the name "legacy" within their name. This is a mechanism + // to help deprecation and prevent accidental re-use/continued use of code we plan on removing. If you are + // finding yourself turning this off a lot for "new code" consider renaming the file and functions if it has valid uses. + patterns: ['*legacy*'], + }, + ], }, }, /** @@ -1304,6 +1322,15 @@ module.exports = { 'no-template-curly-in-string': 'error', 'sort-keys': 'error', 'prefer-destructuring': 'error', + 'no-restricted-imports': [ + 'error', + { + // prevents code from importing files that contain the name "legacy" within their name. This is a mechanism + // to help deprecation and prevent accidental re-use/continued use of code we plan on removing. If you are + // finding yourself turning this off a lot for "new code" consider renaming the file and functions if it has valid uses. + patterns: ['*legacy*'], + }, + ], }, }, /** diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 18c029ce2300e..092875c57fbd0 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -202,8 +202,9 @@ export const THRESHOLD_RULE_TYPE_ID = `${RULE_TYPE_PREFIX}.thresholdRule` as con /** * Id for the notifications alerting type + * @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 NOTIFICATIONS_ID = `siem.notifications`; +export const LEGACY_NOTIFICATIONS_ID = `siem.notifications`; /** * Special internal structure for tags for signals. This is used diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_add_tags.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_add_tags.test.ts new file mode 100644 index 0000000000000..95051ad3d8021 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_add_tags.test.ts @@ -0,0 +1,28 @@ +/* + * 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. + */ + +// eslint-disable-next-line no-restricted-imports +import { legacyAddTags } from './legacy_add_tags'; +import { INTERNAL_RULE_ALERT_ID_KEY } from '../../../../common/constants'; + +describe('legacyAdd_tags', () => { + test('it should add a rule id as an internal structure', () => { + const tags = legacyAddTags([], 'rule-1'); + expect(tags).toEqual([`${INTERNAL_RULE_ALERT_ID_KEY}:rule-1`]); + }); + + test('it should not allow duplicate tags to be created', () => { + const tags = legacyAddTags(['tag-1', 'tag-1'], 'rule-1'); + expect(tags).toEqual(['tag-1', `${INTERNAL_RULE_ALERT_ID_KEY}:rule-1`]); + }); + + test('it should not allow duplicate internal tags to be created when called two times in a row', () => { + const tags1 = legacyAddTags(['tag-1'], 'rule-1'); + const tags2 = legacyAddTags(tags1, 'rule-1'); + expect(tags2).toEqual(['tag-1', `${INTERNAL_RULE_ALERT_ID_KEY}:rule-1`]); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_add_tags.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_add_tags.ts new file mode 100644 index 0000000000000..b17d8d7226a64 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_add_tags.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. + */ + +import { INTERNAL_RULE_ALERT_ID_KEY } from '../../../../common/constants'; + +/** + * @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 legacyAddTags = (tags: string[], ruleAlertId: string): string[] => + Array.from(new Set([...tags, `${INTERNAL_RULE_ALERT_ID_KEY}:${ruleAlertId}`])); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_create_notifications.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_create_notifications.test.ts new file mode 100644 index 0000000000000..cc9e885d49644 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_create_notifications.test.ts @@ -0,0 +1,76 @@ +/* + * 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 { rulesClientMock } from '../../../../../alerting/server/mocks'; +// eslint-disable-next-line no-restricted-imports +import { legacyCreateNotifications } from './legacy_create_notifications'; + +/** + * @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 + */ +describe('legacyCreateNotifications', () => { + let rulesClient: ReturnType; + + beforeEach(() => { + rulesClient = rulesClientMock.create(); + }); + + it('calls the rulesClient with proper params', async () => { + const ruleAlertId = 'rule-04128c15-0d1b-4716-a4c5-46997ac7f3bd'; + + await legacyCreateNotifications({ + rulesClient, + actions: [], + ruleAlertId, + enabled: true, + interval: '', + name: '', + }); + + expect(rulesClient.create).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + params: expect.objectContaining({ + ruleAlertId, + }), + }), + }) + ); + }); + + it('calls the rulesClient with transformed actions', async () => { + const action = { + group: 'default', + id: '99403909-ca9b-49ba-9d7a-7e5320e68d05', + params: { message: 'Rule generated {{state.signals_count}} signals' }, + actionTypeId: '.slack', + }; + await legacyCreateNotifications({ + rulesClient, + actions: [action], + ruleAlertId: 'new-rule-id', + enabled: true, + interval: '', + name: '', + }); + + expect(rulesClient.create).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + actions: expect.arrayContaining([ + { + group: action.group, + id: action.id, + params: action.params, + actionTypeId: '.slack', + }, + ]), + }), + }) + ); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_create_notifications.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_create_notifications.ts new file mode 100644 index 0000000000000..13e4b405ca26b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_create_notifications.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SanitizedAlert } from '../../../../../alerting/common'; +import { SERVER_APP_ID, LEGACY_NOTIFICATIONS_ID } from '../../../../common/constants'; +// eslint-disable-next-line no-restricted-imports +import { CreateNotificationParams, LegacyRuleNotificationAlertTypeParams } from './legacy_types'; +// eslint-disable-next-line no-restricted-imports +import { legacyAddTags } from './legacy_add_tags'; + +/** + * @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 legacyCreateNotifications = async ({ + rulesClient, + actions, + enabled, + ruleAlertId, + interval, + name, +}: CreateNotificationParams): Promise> => + rulesClient.create({ + data: { + name, + tags: legacyAddTags([], ruleAlertId), + alertTypeId: LEGACY_NOTIFICATIONS_ID, + consumer: SERVER_APP_ID, + params: { + ruleAlertId, + }, + schedule: { interval }, + enabled, + actions, + throttle: null, + notifyWhen: null, + }, + }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_find_notifications.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_find_notifications.test.ts new file mode 100644 index 0000000000000..32a1b0eacd55b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_find_notifications.test.ts @@ -0,0 +1,24 @@ +/* + * 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. + */ + +// eslint-disable-next-line no-restricted-imports +import { legacyGetFilter } from './legacy_find_notifications'; +import { LEGACY_NOTIFICATIONS_ID } from '../../../../common/constants'; + +describe('legacyFind_notifications', () => { + test('it returns a full filter with an AND if sent down', () => { + expect(legacyGetFilter('alert.attributes.enabled: true')).toEqual( + `alert.attributes.alertTypeId: ${LEGACY_NOTIFICATIONS_ID} AND alert.attributes.enabled: true` + ); + }); + + test('it returns existing filter with no AND when not set', () => { + expect(legacyGetFilter(null)).toEqual( + `alert.attributes.alertTypeId: ${LEGACY_NOTIFICATIONS_ID}` + ); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_find_notifications.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_find_notifications.ts new file mode 100644 index 0000000000000..51584879a4bd9 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_find_notifications.ts @@ -0,0 +1,45 @@ +/* + * 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 { AlertTypeParams, FindResult } from '../../../../../alerting/server'; +import { LEGACY_NOTIFICATIONS_ID } from '../../../../common/constants'; +// eslint-disable-next-line no-restricted-imports +import { LegacyFindNotificationParams } from './legacy_types'; + +/** + * @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 legacyGetFilter = (filter: string | null | undefined) => { + if (filter == null) { + return `alert.attributes.alertTypeId: ${LEGACY_NOTIFICATIONS_ID}`; + } else { + return `alert.attributes.alertTypeId: ${LEGACY_NOTIFICATIONS_ID} AND ${filter}`; + } +}; + +/** + * @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 legacyFindNotifications = async ({ + rulesClient, + perPage, + page, + fields, + filter, + sortField, + sortOrder, +}: LegacyFindNotificationParams): Promise> => + rulesClient.find({ + options: { + fields, + page, + perPage, + filter: legacyGetFilter(filter), + sortOrder, + sortField, + }, + }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_read_notifications.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_read_notifications.test.ts new file mode 100644 index 0000000000000..efe4e51e761b9 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_read_notifications.test.ts @@ -0,0 +1,157 @@ +/* + * 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. + */ + +// eslint-disable-next-line no-restricted-imports +import { legacyReadNotifications } from './legacy_read_notifications'; +import { rulesClientMock } from '../../../../../alerting/server/mocks'; +import { + legacyGetNotificationResult, + legacyGetFindNotificationsResultWithSingleHit, +} from '../routes/__mocks__/request_responses'; + +class LegacyTestError extends Error { + constructor() { + super(); + + this.name = 'CustomError'; + this.output = { statusCode: 404 }; + } + public output: { statusCode: number }; +} + +describe('legacyReadNotifications', () => { + let rulesClient: ReturnType; + + beforeEach(() => { + rulesClient = rulesClientMock.create(); + }); + + describe('readNotifications', () => { + test('should return the output from rulesClient if id is set but ruleAlertId is undefined', async () => { + rulesClient.get.mockResolvedValue(legacyGetNotificationResult()); + + const rule = await legacyReadNotifications({ + rulesClient, + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + ruleAlertId: undefined, + }); + expect(rule).toEqual(legacyGetNotificationResult()); + }); + test('should return null if saved object found by alerts client given id is not alert type', async () => { + const result = legacyGetNotificationResult(); + // @ts-expect-error + delete result.alertTypeId; + rulesClient.get.mockResolvedValue(result); + + const rule = await legacyReadNotifications({ + rulesClient, + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + ruleAlertId: undefined, + }); + expect(rule).toEqual(null); + }); + + test('should return error if alerts client throws 404 error on get', async () => { + rulesClient.get.mockImplementation(() => { + throw new LegacyTestError(); + }); + + const rule = await legacyReadNotifications({ + rulesClient, + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + ruleAlertId: undefined, + }); + expect(rule).toEqual(null); + }); + + test('should return error if alerts client throws error on get', async () => { + rulesClient.get.mockImplementation(() => { + throw new Error('Test error'); + }); + try { + await legacyReadNotifications({ + rulesClient, + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + ruleAlertId: undefined, + }); + } catch (exc) { + expect(exc.message).toEqual('Test error'); + } + }); + + test('should return the output from rulesClient if id is set but ruleAlertId is null', async () => { + rulesClient.get.mockResolvedValue(legacyGetNotificationResult()); + + const rule = await legacyReadNotifications({ + rulesClient, + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + ruleAlertId: null, + }); + expect(rule).toEqual(legacyGetNotificationResult()); + }); + + test('should return the output from rulesClient if id is undefined but ruleAlertId is set', async () => { + rulesClient.get.mockResolvedValue(legacyGetNotificationResult()); + rulesClient.find.mockResolvedValue(legacyGetFindNotificationsResultWithSingleHit()); + + const rule = await legacyReadNotifications({ + rulesClient, + id: undefined, + ruleAlertId: 'rule-1', + }); + expect(rule).toEqual(legacyGetNotificationResult()); + }); + + test('should return null if the output from rulesClient with ruleAlertId set is empty', async () => { + rulesClient.get.mockResolvedValue(legacyGetNotificationResult()); + rulesClient.find.mockResolvedValue({ data: [], page: 0, perPage: 1, total: 0 }); + + const rule = await legacyReadNotifications({ + rulesClient, + id: undefined, + ruleAlertId: 'rule-1', + }); + expect(rule).toEqual(null); + }); + + test('should return the output from rulesClient if id is null but ruleAlertId is set', async () => { + rulesClient.get.mockResolvedValue(legacyGetNotificationResult()); + rulesClient.find.mockResolvedValue(legacyGetFindNotificationsResultWithSingleHit()); + + const rule = await legacyReadNotifications({ + rulesClient, + id: null, + ruleAlertId: 'rule-1', + }); + expect(rule).toEqual(legacyGetNotificationResult()); + }); + + test('should return null if id and ruleAlertId are null', async () => { + rulesClient.get.mockResolvedValue(legacyGetNotificationResult()); + rulesClient.find.mockResolvedValue(legacyGetFindNotificationsResultWithSingleHit()); + + const rule = await legacyReadNotifications({ + rulesClient, + id: null, + ruleAlertId: null, + }); + expect(rule).toEqual(null); + }); + + test('should return null if id and ruleAlertId are undefined', async () => { + rulesClient.get.mockResolvedValue(legacyGetNotificationResult()); + rulesClient.find.mockResolvedValue(legacyGetFindNotificationsResultWithSingleHit()); + + const rule = await legacyReadNotifications({ + rulesClient, + id: undefined, + ruleAlertId: undefined, + }); + expect(rule).toEqual(null); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_read_notifications.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_read_notifications.ts new file mode 100644 index 0000000000000..9e2fb9515bb82 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_read_notifications.ts @@ -0,0 +1,57 @@ +/* + * 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 { AlertTypeParams, SanitizedAlert } from '../../../../../alerting/common'; +// eslint-disable-next-line no-restricted-imports +import { LegacyReadNotificationParams, legacyIsAlertType } from './legacy_types'; +// eslint-disable-next-line no-restricted-imports +import { legacyFindNotifications } from './legacy_find_notifications'; +import { INTERNAL_RULE_ALERT_ID_KEY } from '../../../../common/constants'; + +/** + * @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 legacyReadNotifications = async ({ + rulesClient, + id, + ruleAlertId, +}: LegacyReadNotificationParams): Promise | null> => { + if (id != null) { + try { + const notification = await rulesClient.get({ id }); + if (legacyIsAlertType(notification)) { + return notification; + } else { + return null; + } + } catch (err) { + if (err?.output?.statusCode === 404) { + return null; + } else { + // throw non-404 as they would be 500 or other internal errors + throw err; + } + } + } else if (ruleAlertId != null) { + const notificationFromFind = await legacyFindNotifications({ + rulesClient, + filter: `alert.attributes.tags: "${INTERNAL_RULE_ALERT_ID_KEY}:${ruleAlertId}"`, + page: 1, + }); + if ( + notificationFromFind.data.length === 0 || + !legacyIsAlertType(notificationFromFind.data[0]) + ) { + return null; + } else { + return notificationFromFind.data[0]; + } + } else { + // should never get here, and yet here we are. + return null; + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.test.ts new file mode 100644 index 0000000000000..6c0ffa65a9afa --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.test.ts @@ -0,0 +1,255 @@ +/* + * 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 { loggingSystemMock } from 'src/core/server/mocks'; +import { getAlertMock } from '../routes/__mocks__/request_responses'; +// eslint-disable-next-line no-restricted-imports +import { legacyRulesNotificationAlertType } from './legacy_rules_notification_alert_type'; +import { buildSignalsSearchQuery } from './build_signals_query'; +import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; +// eslint-disable-next-line no-restricted-imports +import { LegacyNotificationExecutorOptions } from './legacy_types'; +import { + sampleDocSearchResultsNoSortIdNoVersion, + sampleDocSearchResultsWithSortId, + sampleEmptyDocSearchResults, +} from '../signals/__mocks__/es_results'; +import { DEFAULT_RULE_NOTIFICATION_QUERY_SIZE } from '../../../../common/constants'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; +import { getQueryRuleParams } from '../schemas/rule_schemas.mock'; +jest.mock('./build_signals_query'); + +/** + * @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 + */ +describe('legacyRules_notification_alert_type', () => { + let payload: LegacyNotificationExecutorOptions; + let alert: ReturnType; + let logger: ReturnType; + let alertServices: AlertServicesMock; + + beforeEach(() => { + alertServices = alertsMock.createAlertServices(); + logger = loggingSystemMock.createLogger(); + + payload = { + alertId: '1111', + services: alertServices, + params: { ruleAlertId: '2222' }, + state: {}, + spaceId: '', + name: 'name', + tags: [], + startedAt: new Date('2019-12-14T16:40:33.400Z'), + previousStartedAt: new Date('2019-12-13T16:40:33.400Z'), + createdBy: 'elastic', + updatedBy: 'elastic', + rule: { + name: 'name', + tags: [], + consumer: 'foo', + producer: 'foo', + ruleTypeId: 'ruleType', + ruleTypeName: 'Name of rule', + enabled: true, + schedule: { + interval: '1h', + }, + actions: [], + createdBy: 'elastic', + updatedBy: 'elastic', + createdAt: new Date('2019-12-14T16:40:33.400Z'), + updatedAt: new Date('2019-12-14T16:40:33.400Z'), + throttle: null, + notifyWhen: null, + }, + }; + + alert = legacyRulesNotificationAlertType({ + logger, + }); + }); + + describe.each([ + ['Legacy', false], + ['RAC', true], + ])('executor - %s', (_, isRuleRegistryEnabled) => { + it('throws an error if rule alert was not found', async () => { + alertServices.savedObjectsClient.get.mockResolvedValue({ + id: 'id', + attributes: {}, + type: 'type', + references: [], + }); + await alert.executor(payload); + expect(logger.error).toHaveBeenCalledWith( + `Security Solution notification (Legacy) saved object for alert ${payload.params.ruleAlertId} was not found` + ); + }); + + it('should call buildSignalsSearchQuery with proper params', async () => { + const ruleAlert = getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()); + alertServices.savedObjectsClient.get.mockResolvedValue({ + id: 'id', + type: 'type', + references: [], + attributes: ruleAlert, + }); + alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise( + sampleDocSearchResultsWithSortId() + ) + ); + + await alert.executor(payload); + + expect(buildSignalsSearchQuery).toHaveBeenCalledWith( + expect.objectContaining({ + from: '1576255233400', + index: '.siem-signals', + ruleId: 'rule-1', + to: '1576341633400', + size: DEFAULT_RULE_NOTIFICATION_QUERY_SIZE, + }) + ); + }); + + it('should resolve results_link when meta is undefined to use "/app/security"', async () => { + const ruleAlert = getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()); + delete ruleAlert.params.meta; + alertServices.savedObjectsClient.get.mockResolvedValue({ + id: 'rule-id', + type: 'type', + references: [], + attributes: ruleAlert, + }); + alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise( + sampleDocSearchResultsWithSortId() + ) + ); + + await alert.executor(payload); + expect(alertServices.alertInstanceFactory).toHaveBeenCalled(); + + const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results; + expect(alertInstanceMock.scheduleActions).toHaveBeenCalledWith( + 'default', + expect.objectContaining({ + results_link: + '/app/security/detections/rules/id/rule-id?timerange=(global:(linkTo:!(timeline),timerange:(from:1576255233400,kind:absolute,to:1576341633400)),timeline:(linkTo:!(global),timerange:(from:1576255233400,kind:absolute,to:1576341633400)))', + }) + ); + }); + + it('should resolve results_link when meta is an empty object to use "/app/security"', async () => { + const ruleAlert = getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()); + ruleAlert.params.meta = {}; + alertServices.savedObjectsClient.get.mockResolvedValue({ + id: 'rule-id', + type: 'type', + references: [], + attributes: ruleAlert, + }); + alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise( + sampleDocSearchResultsWithSortId() + ) + ); + await alert.executor(payload); + expect(alertServices.alertInstanceFactory).toHaveBeenCalled(); + + const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results; + expect(alertInstanceMock.scheduleActions).toHaveBeenCalledWith( + 'default', + expect.objectContaining({ + results_link: + '/app/security/detections/rules/id/rule-id?timerange=(global:(linkTo:!(timeline),timerange:(from:1576255233400,kind:absolute,to:1576341633400)),timeline:(linkTo:!(global),timerange:(from:1576255233400,kind:absolute,to:1576341633400)))', + }) + ); + }); + + it('should resolve results_link to custom kibana link when given one', async () => { + const ruleAlert = getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()); + ruleAlert.params.meta = { + kibana_siem_app_url: 'http://localhost', + }; + alertServices.savedObjectsClient.get.mockResolvedValue({ + id: 'rule-id', + type: 'type', + references: [], + attributes: ruleAlert, + }); + alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise( + sampleDocSearchResultsWithSortId() + ) + ); + await alert.executor(payload); + expect(alertServices.alertInstanceFactory).toHaveBeenCalled(); + + const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results; + expect(alertInstanceMock.scheduleActions).toHaveBeenCalledWith( + 'default', + expect.objectContaining({ + results_link: + 'http://localhost/detections/rules/id/rule-id?timerange=(global:(linkTo:!(timeline),timerange:(from:1576255233400,kind:absolute,to:1576341633400)),timeline:(linkTo:!(global),timerange:(from:1576255233400,kind:absolute,to:1576341633400)))', + }) + ); + }); + + it('should not call alertInstanceFactory if signalsCount was 0', async () => { + const ruleAlert = getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()); + alertServices.savedObjectsClient.get.mockResolvedValue({ + id: 'id', + type: 'type', + references: [], + attributes: ruleAlert, + }); + alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise(sampleEmptyDocSearchResults()) + ); + + await alert.executor(payload); + + expect(alertServices.alertInstanceFactory).not.toHaveBeenCalled(); + }); + + it('should call scheduleActions if signalsCount was greater than 0', async () => { + const ruleAlert = getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()); + alertServices.savedObjectsClient.get.mockResolvedValue({ + id: 'id', + type: 'type', + references: [], + attributes: ruleAlert, + }); + alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise( + sampleDocSearchResultsNoSortIdNoVersion() + ) + ); + + await alert.executor(payload); + + expect(alertServices.alertInstanceFactory).toHaveBeenCalled(); + + const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results; + expect(alertInstanceMock.replaceState).toHaveBeenCalledWith( + expect.objectContaining({ signals_count: 100 }) + ); + expect(alertInstanceMock.scheduleActions).toHaveBeenCalledWith( + 'default', + expect.objectContaining({ + rule: expect.objectContaining({ + name: ruleAlert.name, + }), + }) + ); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.ts new file mode 100644 index 0000000000000..07f571bc7be1b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.ts @@ -0,0 +1,118 @@ +/* + * 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 { Logger } from 'src/core/server'; +import { schema } from '@kbn/config-schema'; +import { parseScheduleDates } from '@kbn/securitysolution-io-ts-utils'; +import { + DEFAULT_RULE_NOTIFICATION_QUERY_SIZE, + LEGACY_NOTIFICATIONS_ID, + SERVER_APP_ID, +} from '../../../../common/constants'; + +// eslint-disable-next-line no-restricted-imports +import { LegacyNotificationAlertTypeDefinition } from './legacy_types'; +import { AlertAttributes } from '../signals/types'; +import { siemRuleActionGroups } from '../signals/siem_rule_action_groups'; +import { scheduleNotificationActions } from './schedule_notification_actions'; +import { getNotificationResultsLink } from './utils'; +import { getSignals } from './get_signals'; + +/** + * @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 legacyRulesNotificationAlertType = ({ + logger, +}: { + logger: Logger; +}): LegacyNotificationAlertTypeDefinition => ({ + id: LEGACY_NOTIFICATIONS_ID, + name: 'Security Solution notification (Legacy)', + actionGroups: siemRuleActionGroups, + defaultActionGroupId: 'default', + producer: SERVER_APP_ID, + validate: { + params: schema.object({ + ruleAlertId: schema.string(), + }), + }, + minimumLicenseRequired: 'basic', + isExportable: false, + async executor({ startedAt, previousStartedAt, alertId, services, params }) { + // TODO: Change this to be a link to documentation on how to migrate: https://github.com/elastic/kibana/issues/113055 + logger.warn( + 'Security Solution notification (Legacy) system detected still running. Please see documentation on how to migrate to the new notification system.' + ); + const ruleAlertSavedObject = await services.savedObjectsClient.get( + 'alert', + params.ruleAlertId + ); + + if (!ruleAlertSavedObject.attributes.params) { + logger.error( + `Security Solution notification (Legacy) saved object for alert ${params.ruleAlertId} was not found` + ); + return; + } + logger.warn( + [ + 'Security Solution notification (Legacy) system still active for alert with', + `name: "${ruleAlertSavedObject.attributes.name}"`, + `description: "${ruleAlertSavedObject.attributes.params.description}"`, + `id: "${ruleAlertSavedObject.id}".`, + `Please see documentation on how to migrate to the new notification system.`, + ].join(' ') + ); + + const { params: ruleAlertParams, name: ruleName } = ruleAlertSavedObject.attributes; + const ruleParams = { ...ruleAlertParams, name: ruleName, id: ruleAlertSavedObject.id }; + + const fromInMs = parseScheduleDates( + previousStartedAt + ? previousStartedAt.toISOString() + : `now-${ruleAlertSavedObject.attributes.schedule.interval}` + )?.format('x'); + const toInMs = parseScheduleDates(startedAt.toISOString())?.format('x'); + + const results = await getSignals({ + from: fromInMs, + to: toInMs, + size: DEFAULT_RULE_NOTIFICATION_QUERY_SIZE, + index: ruleParams.outputIndex, + ruleId: ruleParams.ruleId, + esClient: services.scopedClusterClient.asCurrentUser, + }); + + const signals = results.hits.hits.map((hit) => hit._source); + + const signalsCount = + typeof results.hits.total === 'number' ? results.hits.total : results.hits.total.value; + + const resultsLink = getNotificationResultsLink({ + from: fromInMs, + to: toInMs, + id: ruleAlertSavedObject.id, + kibanaSiemAppUrl: (ruleAlertParams.meta as { kibana_siem_app_url?: string } | undefined) + ?.kibana_siem_app_url, + }); + + logger.debug( + `Security Solution notification (Legacy) found ${signalsCount} signals using signal rule name: "${ruleParams.name}", id: "${params.ruleAlertId}", rule_id: "${ruleParams.ruleId}" in "${ruleParams.outputIndex}" index` + ); + + if (signalsCount !== 0) { + const alertInstance = services.alertInstanceFactory(alertId); + scheduleNotificationActions({ + alertInstance, + signalsCount, + resultsLink, + ruleParams, + signals, + }); + } + }, +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_types.ts new file mode 100644 index 0000000000000..2a52f14379845 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_types.ts @@ -0,0 +1,133 @@ +/* + * 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 { + RulesClient, + PartialAlert, + AlertType, + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + AlertExecutorOptions, +} from '../../../../../alerting/server'; +import { Alert, AlertAction } from '../../../../../alerting/common'; +import { LEGACY_NOTIFICATIONS_ID } from '../../../../common/constants'; + +/** + * @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 interface LegacyRuleNotificationAlertTypeParams extends AlertTypeParams { + ruleAlertId: string; +} + +/** + * @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 type LegacyRuleNotificationAlertType = Alert; + +/** + * @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 interface LegacyFindNotificationParams { + rulesClient: RulesClient; + perPage?: number; + page?: number; + sortField?: string; + filter?: string; + fields?: string[]; + sortOrder?: 'asc' | 'desc'; +} + +/** + * @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 interface LegacyClients { + rulesClient: RulesClient; +} + +/** + * @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 interface LegacyNotificationAlertParams { + actions: AlertAction[]; + enabled: boolean; + ruleAlertId: string; + interval: string; + name: string; +} + +/** + * @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 type CreateNotificationParams = LegacyNotificationAlertParams & LegacyClients; + +/** + * @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 interface LegacyReadNotificationParams { + rulesClient: RulesClient; + id?: string | null; + ruleAlertId?: string | null; +} + +/** + * @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 legacyIsAlertType = ( + partialAlert: PartialAlert +): partialAlert is LegacyRuleNotificationAlertType => { + return partialAlert.alertTypeId === LEGACY_NOTIFICATIONS_ID; +}; + +/** + * @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 type LegacyNotificationExecutorOptions = AlertExecutorOptions< + LegacyRuleNotificationAlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext +>; + +/** + * This returns true because by default a NotificationAlertTypeDefinition is an AlertType + * since we are only increasing the strictness of params. + * @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 legacyIsNotificationAlertExecutor = ( + obj: LegacyNotificationAlertTypeDefinition +): obj is AlertType< + AlertTypeParams, + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext +> => { + return true; +}; + +/** + * @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 type LegacyNotificationAlertTypeDefinition = Omit< + AlertType< + AlertTypeParams, + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + 'default' + >, + 'executor' +> & { + executor: ({ + services, + params, + state, + }: LegacyNotificationExecutorOptions) => Promise; +}; 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 5d0f0c246f6b0..c3c3ac47baf9a 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 @@ -40,6 +40,8 @@ import { getPerformBulkActionSchemaMock } from '../../../../../common/detection_ import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas'; import { FindBulkExecutionLogResponse } from '../../rule_execution_log/types'; import { ruleTypeMappings } from '../../signals/utils'; +// eslint-disable-next-line no-restricted-imports +import type { LegacyRuleNotificationAlertType } from '../../notifications/legacy_types'; export const typicalSetStatusSignalByIdsPayload = (): SetSignalsStatusSchemaDecoded => ({ signal_ids: ['somefakeid1', 'somefakeid2'], @@ -593,3 +595,58 @@ export const getSignalsMigrationStatusRequest = () => path: DETECTION_ENGINE_SIGNALS_MIGRATION_STATUS_URL, query: getSignalsMigrationStatusSchemaMock(), }); + +/** + * @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', + name: 'Notification for Rule Test', + tags: ['__internal_rule_alert_id:85b64e8a-2e40-4096-86af-5ac172c10825'], + alertTypeId: 'siem.notifications', + consumer: 'siem', + params: { + ruleAlertId: '85b64e8a-2e40-4096-86af-5ac172c10825', + }, + schedule: { + interval: '5m', + }, + enabled: true, + actions: [ + { + actionTypeId: '.slack', + params: { + message: + 'Rule generated {{state.signals_count}} signals\n\n{{context.rule.name}}\n{{{context.results_link}}}', + }, + group: 'default', + id: '99403909-ca9b-49ba-9d7a-7e5320e68d05', + }, + ], + throttle: null, + notifyWhen: null, + 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 = + (): FindHit => ({ + page: 1, + perPage: 1, + total: 1, + data: [legacyGetNotificationResult()], + }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.ts index 26e8d1107237b..bd8d2bd9685cf 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.ts @@ -17,6 +17,8 @@ import { findRules } from '../../rules/find_rules'; import { buildSiemResponse } from '../utils'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; import { transformFindAlerts } from './utils'; +// eslint-disable-next-line no-restricted-imports +import { legacyGetBulkRuleActionsSavedObject } from '../../rule_actions/legacy_get_bulk_rule_actions_saved_object'; export const findRulesRoute = ( router: SecuritySolutionPluginRouter, @@ -44,6 +46,7 @@ export const findRulesRoute = ( try { const { query } = request; const rulesClient = context.alerting?.getRulesClient(); + const savedObjectsClient = context.core.savedObjects.client; if (!rulesClient) { return siemResponse.error({ statusCode: 404 }); @@ -62,12 +65,15 @@ export const findRulesRoute = ( }); const alertIds = rules.data.map((rule) => rule.id); - const ruleStatuses = await execLogClient.findBulk({ - ruleIds: alertIds, - logsCount: 1, - spaceId: context.securitySolution.getSpaceId(), - }); - const transformed = transformFindAlerts(rules, ruleStatuses); + const [ruleStatuses, ruleActions] = await Promise.all([ + execLogClient.findBulk({ + ruleIds: alertIds, + logsCount: 1, + spaceId: context.securitySolution.getSpaceId(), + }), + legacyGetBulkRuleActionsSavedObject({ alertIds, savedObjectsClient }), + ]); + const transformed = transformFindAlerts(rules, ruleStatuses, ruleActions); if (transformed == null) { return siemResponse.error({ statusCode: 500, body: 'Internal error transforming' }); } else { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/legacy_create_legacy_notification.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/legacy_create_legacy_notification.ts new file mode 100644 index 0000000000000..248b864bef9ed --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/legacy_create_legacy_notification.ts @@ -0,0 +1,110 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import type { SecuritySolutionPluginRouter } from '../../../../types'; +// eslint-disable-next-line no-restricted-imports +import { legacyUpdateOrCreateRuleActionsSavedObject } from '../../rule_actions/legacy_update_or_create_rule_actions_saved_object'; +// eslint-disable-next-line no-restricted-imports +import { legacyReadNotifications } from '../../notifications/legacy_read_notifications'; +// eslint-disable-next-line no-restricted-imports +import { LegacyRuleNotificationAlertTypeParams } from '../../notifications/legacy_types'; +// eslint-disable-next-line no-restricted-imports +import { legacyAddTags } from '../../notifications/legacy_add_tags'; +// eslint-disable-next-line no-restricted-imports +import { legacyCreateNotifications } from '../../notifications/legacy_create_notifications'; + +/** + * Given an "alert_id" and a valid "action_id" this will create a legacy notification. This is for testing + * purposes only and should not be used for production. It is behind a route with the words "internal" and + * "legacy" to announce its legacy and internal intent. + * @deprecated Once we no longer have legacy notifications and "side car actions" this can be removed. + * @param router The router + */ +export const legacyCreateLegacyNotificationRoute = (router: SecuritySolutionPluginRouter): void => { + router.post( + { + path: '/internal/api/detection/legacy/notifications', + validate: { + query: schema.object({ alert_id: schema.string() }), + body: schema.object({ + name: schema.string(), + interval: schema.string(), + actions: schema.arrayOf( + schema.object({ + id: schema.string(), + group: schema.string(), + params: schema.object({ + message: schema.string(), + }), + actionTypeId: schema.string(), + }) + ), + }), + }, + options: { + tags: ['access:securitySolution'], + }, + }, + async (context, request, response) => { + const rulesClient = context.alerting.getRulesClient(); + const savedObjectsClient = context.core.savedObjects.client; + const { alert_id: ruleAlertId } = request.query; + const { actions, interval, name } = request.body; + try { + // This is to ensure it exists before continuing. + await rulesClient.get({ id: ruleAlertId }); + const notification = await legacyReadNotifications({ + rulesClient, + id: undefined, + ruleAlertId, + }); + if (notification != null) { + await rulesClient.update({ + id: notification.id, + data: { + tags: legacyAddTags([], ruleAlertId), + name, + schedule: { + interval, + }, + actions, + params: { + ruleAlertId, + }, + throttle: null, + notifyWhen: null, + }, + }); + } else { + await legacyCreateNotifications({ + rulesClient, + actions, + enabled: true, + ruleAlertId, + interval, + name, + }); + } + await legacyUpdateOrCreateRuleActionsSavedObject({ + ruleAlertId, + savedObjectsClient, + actions, + throttle: interval, + }); + } catch (error) { + const message = error instanceof Error ? error.message : 'unknown'; + return response.badRequest({ body: message }); + } + return response.ok({ + body: { + ok: 'acknowledged', + }, + }); + } + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.ts index 6b643bde22b17..6abe3086d6bb8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.ts @@ -19,6 +19,8 @@ import { buildSiemResponse } from '../utils'; import { readRules } from '../../rules/read_rules'; import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas'; +// eslint-disable-next-line no-restricted-imports +import { legacyGetRuleActionsSavedObject } from '../../rule_actions/legacy_get_rule_actions_saved_object'; export const readRulesRoute = ( router: SecuritySolutionPluginRouter, @@ -53,6 +55,7 @@ export const readRulesRoute = ( } const ruleStatusClient = context.securitySolution.getExecutionLogClient(); + const savedObjectsClient = context.core.savedObjects.client; const rule = await readRules({ id, isRuleRegistryEnabled, @@ -60,6 +63,10 @@ export const readRulesRoute = ( ruleId, }); if (rule != null) { + const legacyRuleActions = await legacyGetRuleActionsSavedObject({ + savedObjectsClient, + ruleAlertId: rule.id, + }); const ruleStatuses = await ruleStatusClient.find({ logsCount: 1, ruleId: rule.id, @@ -74,7 +81,12 @@ export const readRulesRoute = ( rule.executionStatus.lastExecutionDate.toISOString(); currentStatus.attributes.status = RuleExecutionStatus.failed; } - const transformed = transform(rule, currentStatus, isRuleRegistryEnabled); + const transformed = transform( + rule, + currentStatus, + isRuleRegistryEnabled, + legacyRuleActions + ); if (transformed == null) { return siemResponse.error({ statusCode: 500, body: 'Internal error transforming' }); } else { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts index 8001b7d6bf4d4..672a834731b45 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts @@ -15,16 +15,14 @@ import { transform, transformTags, getIdBulkError, - transformOrBulkError, transformAlertsToRules, - transformOrImportError, getDuplicates, getTupleDuplicateErrorsAndUniqueRules, } from './utils'; import { getAlertMock } from '../__mocks__/request_responses'; import { INTERNAL_IDENTIFIER } from '../../../../../common/constants'; import { PartialFilter } from '../../types'; -import { BulkError, ImportSuccessError } from '../utils'; +import { BulkError } from '../utils'; import { getOutputRuleAlertForRest } from '../__mocks__/utils'; import { PartialAlert } from '../../../../../../alerting/server'; import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson'; @@ -38,6 +36,9 @@ import { getQueryRuleParams, getThreatRuleParams, } from '../../schemas/rule_schemas.mock'; +// eslint-disable-next-line no-restricted-imports +import { LegacyRulesActionsSavedObject } from '../../rule_actions/legacy_get_rule_actions_saved_object'; +import { RuleAlertAction } from '../../../../../common/detection_engine/types'; type PromiseFromStreams = ImportRulesSchemaDecoded | Error; @@ -258,7 +259,7 @@ describe.each([ describe('transformFindAlerts', () => { test('outputs empty data set when data set is empty correct', () => { - const output = transformFindAlerts({ data: [], page: 1, perPage: 0, total: 0 }, {}); + const output = transformFindAlerts({ data: [], page: 1, perPage: 0, total: 0 }, {}, {}); expect(output).toEqual({ data: [], page: 1, perPage: 0, total: 0 }); }); @@ -270,6 +271,7 @@ describe.each([ total: 0, data: [getAlertMock(isRuleRegistryEnabled, getQueryRuleParams())], }, + {}, {} ); const expected = getOutputRuleAlertForRest(); @@ -280,6 +282,69 @@ describe.each([ data: [expected], }); }); + + test('outputs 200 if the data is of type siem alert and has undefined for the legacyRuleActions', () => { + const output = transformFindAlerts( + { + page: 1, + perPage: 0, + total: 0, + data: [getAlertMock(isRuleRegistryEnabled, getQueryRuleParams())], + }, + {}, + { + '123': undefined, + } + ); + const expected = getOutputRuleAlertForRest(); + expect(output).toEqual({ + page: 1, + perPage: 0, + total: 0, + data: [expected], + }); + }); + + test('outputs 200 if the data is of type siem alert and has a legacy rule action', () => { + const actions: RuleAlertAction[] = [ + { + id: '456', + params: {}, + group: '', + action_type_id: 'action_123', + }, + ]; + + const legacyRuleActions: Record = { + [getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()).id]: { + id: '123', + actions, + alertThrottle: '1h', + ruleThrottle: '1h', + }, + }; + const output = transformFindAlerts( + { + page: 1, + perPage: 0, + total: 0, + data: [getAlertMock(isRuleRegistryEnabled, getQueryRuleParams())], + }, + {}, + legacyRuleActions + ); + const expected = { + ...getOutputRuleAlertForRest(), + throttle: '1h', + actions, + }; + expect(output).toEqual({ + page: 1, + perPage: 0, + total: 0, + data: [expected], + }); + }); }); describe('transform', () => { @@ -401,44 +466,6 @@ describe.each([ }); }); - describe('transformOrBulkError', () => { - test('outputs 200 if the data is of type siem alert', () => { - const output = transformOrBulkError( - 'rule-1', - getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()), - { - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - actions: [], - ruleThrottle: 'no_actions', - alertThrottle: null, - }, - isRuleRegistryEnabled - ); - const expected = getOutputRuleAlertForRest(); - expect(output).toEqual(expected); - }); - - test('returns 500 if the data is not of type siem alert', () => { - const unsafeCast = { name: 'something else' } as unknown as PartialAlert; - const output = transformOrBulkError( - 'rule-1', - unsafeCast, - { - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - actions: [], - ruleThrottle: 'no_actions', - alertThrottle: null, - }, - isRuleRegistryEnabled - ); - const expected: BulkError = { - rule_id: 'rule-1', - error: { message: 'Internal error transforming', status_code: 500 }, - }; - expect(output).toEqual(expected); - }); - }); - describe('transformAlertsToRules', () => { test('given an empty array returns an empty array', () => { expect(transformAlertsToRules([])).toEqual([]); @@ -466,74 +493,6 @@ describe.each([ }); }); - describe('transformOrImportError', () => { - test('returns 1 given success if the alert is an alert type and the existing success count is 0', () => { - const output = transformOrImportError( - 'rule-1', - getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()), - { - success: true, - success_count: 0, - errors: [], - }, - isRuleRegistryEnabled - ); - const expected: ImportSuccessError = { - success: true, - errors: [], - success_count: 1, - }; - expect(output).toEqual(expected); - }); - - test('returns 2 given successes if the alert is an alert type and the existing success count is 1', () => { - const output = transformOrImportError( - 'rule-1', - getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()), - { - success: true, - success_count: 1, - errors: [], - }, - isRuleRegistryEnabled - ); - const expected: ImportSuccessError = { - success: true, - errors: [], - success_count: 2, - }; - expect(output).toEqual(expected); - }); - - test('returns 1 error and success of false if the data is not of type siem alert', () => { - const unsafeCast = { name: 'something else' } as unknown as PartialAlert; - const output = transformOrImportError( - 'rule-1', - unsafeCast, - { - success: true, - success_count: 1, - errors: [], - }, - isRuleRegistryEnabled - ); - const expected: ImportSuccessError = { - success: false, - errors: [ - { - rule_id: 'rule-1', - error: { - message: 'Internal error transforming', - status_code: 500, - }, - }, - ], - success_count: 1, - }; - expect(output).toEqual(expected); - }); - }); - describe('getDuplicates', () => { test("returns array of ruleIds showing the duplicate keys of 'value2' and 'value3'", () => { const output = getDuplicates( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts index 4f023156fba06..afc48386a2986 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts @@ -18,21 +18,15 @@ import { RuleAlertType, isAlertType, IRuleSavedAttributesSavedObjectAttributes, - isRuleStatusFindType, isRuleStatusSavedObjectType, IRuleStatusSOAttributes, } from '../../rules/types'; -import { - createBulkErrorObject, - BulkError, - createSuccessObject, - ImportSuccessError, - createImportErrorObject, - OutputError, -} from '../utils'; +import { createBulkErrorObject, BulkError, OutputError } from '../utils'; import { internalRuleToAPIResponse } from '../../schemas/rule_converters'; import { RuleParams } from '../../schemas/rule_schemas'; import { SanitizedAlert } from '../../../../../../alerting/common'; +// eslint-disable-next-line no-restricted-imports +import { LegacyRulesActionsSavedObject } from '../../rule_actions/legacy_get_rule_actions_saved_object'; type PromiseFromStreams = ImportRulesSchemaDecoded | Error; @@ -103,9 +97,10 @@ export const transformTags = (tags: string[]): string[] => { // those on the export export const transformAlertToRule = ( alert: SanitizedAlert, - ruleStatus?: SavedObject + ruleStatus?: SavedObject, + legacyRuleActions?: LegacyRulesActionsSavedObject | null ): Partial => { - return internalRuleToAPIResponse(alert, ruleStatus?.attributes); + return internalRuleToAPIResponse(alert, ruleStatus?.attributes, legacyRuleActions); }; export const transformAlertsToRules = (alerts: RuleAlertType[]): Array> => { @@ -114,7 +109,8 @@ export const transformAlertsToRules = (alerts: RuleAlertType[]): Array, - ruleStatuses: { [key: string]: IRuleStatusSOAttributes[] | undefined } + ruleStatuses: { [key: string]: IRuleStatusSOAttributes[] | undefined }, + legacyRuleActions: Record ): { page: number; perPage: number; @@ -128,7 +124,7 @@ export const transformFindAlerts = ( data: findResults.data.map((alert) => { const statuses = ruleStatuses[alert.id]; const status = statuses ? statuses[0] : undefined; - return internalRuleToAPIResponse(alert, status); + return internalRuleToAPIResponse(alert, status, legacyRuleActions[alert.id]); }), }; }; @@ -136,57 +132,20 @@ export const transformFindAlerts = ( export const transform = ( alert: PartialAlert, ruleStatus?: SavedObject, - isRuleRegistryEnabled?: boolean + isRuleRegistryEnabled?: boolean, + legacyRuleActions?: LegacyRulesActionsSavedObject | null ): Partial | null => { if (isAlertType(isRuleRegistryEnabled ?? false, alert)) { return transformAlertToRule( alert, - isRuleStatusSavedObjectType(ruleStatus) ? ruleStatus : undefined + isRuleStatusSavedObjectType(ruleStatus) ? ruleStatus : undefined, + legacyRuleActions ); } return null; }; -export const transformOrBulkError = ( - ruleId: string, - alert: PartialAlert, - ruleStatus?: unknown, - isRuleRegistryEnabled?: boolean -): Partial | BulkError => { - if (isAlertType(isRuleRegistryEnabled ?? false, alert)) { - if (isRuleStatusFindType(ruleStatus) && ruleStatus?.saved_objects.length > 0) { - return transformAlertToRule(alert, ruleStatus?.saved_objects[0] ?? ruleStatus); - } else { - return transformAlertToRule(alert); - } - } else { - return createBulkErrorObject({ - ruleId, - statusCode: 500, - message: 'Internal error transforming', - }); - } -}; - -export const transformOrImportError = ( - ruleId: string, - alert: PartialAlert, - existingImportSuccessError: ImportSuccessError, - isRuleRegistryEnabled: boolean -): ImportSuccessError => { - if (isAlertType(isRuleRegistryEnabled, alert)) { - return createSuccessObject(existingImportSuccessError); - } else { - return createImportErrorObject({ - ruleId, - statusCode: 500, - message: 'Internal error transforming', - existingImportSuccessError, - }); - } -}; - export const getDuplicates = (ruleDefinitions: CreateRulesBulkSchema, by: 'rule_id'): string[] => { const mappedDuplicates = countBy( by, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts index c1969c5088bc0..307b6c96da3e5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts @@ -26,13 +26,16 @@ import { import { createBulkErrorObject, BulkError } from '../utils'; import { transform, transformAlertToRule } from './utils'; import { RuleParams } from '../../schemas/rule_schemas'; +// eslint-disable-next-line no-restricted-imports +import { LegacyRulesActionsSavedObject } from '../../rule_actions/legacy_get_rule_actions_saved_object'; export const transformValidate = ( alert: PartialAlert, ruleStatus?: SavedObject, - isRuleRegistryEnabled?: boolean + isRuleRegistryEnabled?: boolean, + legacyRuleActions?: LegacyRulesActionsSavedObject | null ): [RulesSchema | null, string | null] => { - const transformed = transform(alert, ruleStatus, isRuleRegistryEnabled); + const transformed = transform(alert, ruleStatus, isRuleRegistryEnabled, legacyRuleActions); if (transformed == null) { return [null, 'Internal error transforming']; } else { @@ -43,9 +46,10 @@ export const transformValidate = ( export const newTransformValidate = ( alert: PartialAlert, ruleStatus?: SavedObject, - isRuleRegistryEnabled?: boolean + isRuleRegistryEnabled?: boolean, + legacyRuleActions?: LegacyRulesActionsSavedObject | null ): [FullResponseSchema | null, string | null] => { - const transformed = transform(alert, ruleStatus, isRuleRegistryEnabled); + const transformed = transform(alert, ruleStatus, isRuleRegistryEnabled, legacyRuleActions); if (transformed == null) { return [null, 'Internal error transforming']; } else { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts index 23a65b456e6bc..10472fe1c0a03 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts @@ -15,10 +15,6 @@ import { BadRequestError } from '@kbn/securitysolution-es-utils'; import { transformBulkError, BulkError, - createSuccessObject, - ImportSuccessError, - createImportErrorObject, - transformImportError, convertToSnakeCase, SiemResponseFactory, mergeStatuses, @@ -86,166 +82,6 @@ describe.each([ }); }); - describe('createSuccessObject', () => { - test('it should increment the existing success object by 1', () => { - const success = createSuccessObject({ - success_count: 0, - success: true, - errors: [], - }); - const expected: ImportSuccessError = { - success_count: 1, - success: true, - errors: [], - }; - expect(success).toEqual(expected); - }); - - test('it should increment the existing success object by 1 and not touch the boolean or errors', () => { - const success = createSuccessObject({ - success_count: 0, - success: false, - errors: [ - { rule_id: 'rule-1', error: { status_code: 500, message: 'some sad sad sad error' } }, - ], - }); - const expected: ImportSuccessError = { - success_count: 1, - success: false, - errors: [ - { rule_id: 'rule-1', error: { status_code: 500, message: 'some sad sad sad error' } }, - ], - }; - expect(success).toEqual(expected); - }); - }); - - describe('createImportErrorObject', () => { - test('it creates an error message and does not increment the success count', () => { - const error = createImportErrorObject({ - ruleId: 'some-rule-id', - statusCode: 400, - message: 'some-message', - existingImportSuccessError: { - success_count: 1, - success: true, - errors: [], - }, - }); - const expected: ImportSuccessError = { - success_count: 1, - success: false, - errors: [{ rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }], - }; - expect(error).toEqual(expected); - }); - - test('appends a second error message and does not increment the success count', () => { - const error = createImportErrorObject({ - ruleId: 'some-rule-id', - statusCode: 400, - message: 'some-message', - existingImportSuccessError: { - success_count: 1, - success: false, - errors: [ - { rule_id: 'rule-1', error: { status_code: 500, message: 'some sad sad sad error' } }, - ], - }, - }); - const expected: ImportSuccessError = { - success_count: 1, - success: false, - errors: [ - { rule_id: 'rule-1', error: { status_code: 500, message: 'some sad sad sad error' } }, - { rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }, - ], - }; - expect(error).toEqual(expected); - }); - }); - - describe('transformImportError', () => { - test('returns transformed object if it is a boom object', () => { - const boom = new Boom.Boom('some boom message', { statusCode: 400 }); - const transformed = transformImportError('rule-1', boom, { - success_count: 1, - success: false, - errors: [{ rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }], - }); - const expected: ImportSuccessError = { - success_count: 1, - success: false, - errors: [ - { rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }, - { rule_id: 'rule-1', error: { status_code: 400, message: 'some boom message' } }, - ], - }; - expect(transformed).toEqual(expected); - }); - - test('returns a normal error if it is some non boom object that has a statusCode', () => { - const error: Error & { statusCode?: number } = { - statusCode: 403, - name: 'some name', - message: 'some message', - }; - const transformed = transformImportError('rule-1', error, { - success_count: 1, - success: false, - errors: [{ rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }], - }); - const expected: ImportSuccessError = { - success_count: 1, - success: false, - errors: [ - { rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }, - { rule_id: 'rule-1', error: { status_code: 403, message: 'some message' } }, - ], - }; - expect(transformed).toEqual(expected); - }); - - test('returns a 500 if the status code is not set', () => { - const error: Error & { statusCode?: number } = { - name: 'some name', - message: 'some message', - }; - const transformed = transformImportError('rule-1', error, { - success_count: 1, - success: false, - errors: [{ rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }], - }); - const expected: ImportSuccessError = { - success_count: 1, - success: false, - errors: [ - { rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }, - { rule_id: 'rule-1', error: { status_code: 500, message: 'some message' } }, - ], - }; - expect(transformed).toEqual(expected); - }); - - test('it detects a BadRequestError and returns a Boom status of 400', () => { - const error: BadRequestError = new BadRequestError('I have a type error'); - const transformed = transformImportError('rule-1', error, { - success_count: 1, - success: false, - errors: [{ rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }], - }); - const expected: ImportSuccessError = { - success_count: 1, - success: false, - errors: [ - { rule_id: 'some-rule-id', error: { status_code: 400, message: 'some-message' } }, - { rule_id: 'rule-1', error: { status_code: 400, message: 'I have a type error' } }, - ], - }; - expect(transformed).toEqual(expected); - }); - }); - describe('convertToSnakeCase', () => { it('converts camelCase to snakeCase', () => { const values = { myTestCamelCaseKey: 'something' }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.ts index b03050da59f49..a15dc4f176232 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.ts @@ -100,76 +100,6 @@ export const isImportRegular = ( return !has('error', importRuleResponse) && has('status_code', importRuleResponse); }; -export interface ImportSuccessError { - success: boolean; - success_count: number; - errors: BulkError[]; -} - -export const createSuccessObject = ( - existingImportSuccessError: ImportSuccessError -): ImportSuccessError => { - return { - success_count: existingImportSuccessError.success_count + 1, - success: existingImportSuccessError.success, - errors: existingImportSuccessError.errors, - }; -}; - -export const createImportErrorObject = ({ - ruleId, - statusCode, - message, - existingImportSuccessError, -}: { - ruleId: string; - statusCode: number; - message: string; - existingImportSuccessError: ImportSuccessError; -}): ImportSuccessError => { - return { - success: false, - errors: [ - ...existingImportSuccessError.errors, - createBulkErrorObject({ - ruleId, - statusCode, - message, - }), - ], - success_count: existingImportSuccessError.success_count, - }; -}; - -export const transformImportError = ( - ruleId: string, - err: Error & { statusCode?: number }, - existingImportSuccessError: ImportSuccessError -): ImportSuccessError => { - if (Boom.isBoom(err)) { - return createImportErrorObject({ - ruleId, - statusCode: err.output.statusCode, - message: err.message, - existingImportSuccessError, - }); - } else if (err instanceof BadRequestError) { - return createImportErrorObject({ - ruleId, - statusCode: 400, - message: err.message, - existingImportSuccessError, - }); - } else { - return createImportErrorObject({ - ruleId, - statusCode: err.statusCode ?? 500, - message: err.message, - existingImportSuccessError, - }); - } -}; - export const transformBulkError = ( ruleId: string, err: Error & { statusCode?: number } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_create_rule_actions_saved_object.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_create_rule_actions_saved_object.ts new file mode 100644 index 0000000000000..00607b884cec9 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_create_rule_actions_saved_object.ts @@ -0,0 +1,50 @@ +/* + * 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 { AlertServices } from '../../../../../alerting/server'; +// eslint-disable-next-line no-restricted-imports +import { legacyRuleActionsSavedObjectType } from './legacy_saved_object_mappings'; +// eslint-disable-next-line no-restricted-imports +import { LegacyIRuleActionsAttributesSavedObjectAttributes } from './legacy_types'; +// eslint-disable-next-line no-restricted-imports +import { legacyGetThrottleOptions, legacyGetRuleActionsFromSavedObject } from './legacy_utils'; +// eslint-disable-next-line no-restricted-imports +import { LegacyRulesActionsSavedObject } from './legacy_get_rule_actions_saved_object'; +import { AlertAction } from '../../../../../alerting/common'; +import { transformAlertToRuleAction } from '../../../../common/detection_engine/transform_actions'; + +/** + * @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 + */ +interface LegacyCreateRuleActionsSavedObject { + ruleAlertId: string; + savedObjectsClient: AlertServices['savedObjectsClient']; + actions: AlertAction[] | undefined; + throttle: string | null | undefined; +} + +/** + * @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 legacyCreateRuleActionsSavedObject = async ({ + ruleAlertId, + savedObjectsClient, + actions = [], + throttle, +}: LegacyCreateRuleActionsSavedObject): Promise => { + const ruleActionsSavedObject = + await savedObjectsClient.create( + legacyRuleActionsSavedObjectType, + { + ruleAlertId, + actions: actions.map((action) => transformAlertToRuleAction(action)), + ...legacyGetThrottleOptions(throttle), + } + ); + + return legacyGetRuleActionsFromSavedObject(ruleActionsSavedObject); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_get_bulk_rule_actions_saved_object.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_get_bulk_rule_actions_saved_object.ts new file mode 100644 index 0000000000000..40b359c30219d --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_get_bulk_rule_actions_saved_object.ts @@ -0,0 +1,53 @@ +/* + * 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 { AlertServices } from '../../../../../alerting/server'; +// eslint-disable-next-line no-restricted-imports +import { legacyRuleActionsSavedObjectType } from './legacy_saved_object_mappings'; +// eslint-disable-next-line no-restricted-imports +import { LegacyIRuleActionsAttributesSavedObjectAttributes } from './legacy_types'; +// eslint-disable-next-line no-restricted-imports +import { legacyGetRuleActionsFromSavedObject } from './legacy_utils'; +// eslint-disable-next-line no-restricted-imports +import { LegacyRulesActionsSavedObject } from './legacy_get_rule_actions_saved_object'; +import { buildChunkedOrFilter } from '../signals/utils'; + +/** + * @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 + */ +interface LegacyGetBulkRuleActionsSavedObject { + alertIds: string[]; + savedObjectsClient: AlertServices['savedObjectsClient']; +} + +/** + * @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 legacyGetBulkRuleActionsSavedObject = async ({ + alertIds, + savedObjectsClient, +}: LegacyGetBulkRuleActionsSavedObject): Promise> => { + const filter = buildChunkedOrFilter( + `${legacyRuleActionsSavedObjectType}.attributes.ruleAlertId`, + alertIds + ); + const { + // eslint-disable-next-line @typescript-eslint/naming-convention + saved_objects, + } = await savedObjectsClient.find({ + type: legacyRuleActionsSavedObjectType, + perPage: 10000, + filter, + }); + return saved_objects.reduce( + (acc: { [key: string]: LegacyRulesActionsSavedObject }, savedObject) => { + acc[savedObject.attributes.ruleAlertId] = legacyGetRuleActionsFromSavedObject(savedObject); + return acc; + }, + {} + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_get_rule_actions_saved_object.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_get_rule_actions_saved_object.ts new file mode 100644 index 0000000000000..e73735503825b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_get_rule_actions_saved_object.ts @@ -0,0 +1,57 @@ +/* + * 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 { RuleAlertAction } from '../../../../common/detection_engine/types'; +import { AlertServices } from '../../../../../alerting/server'; +// eslint-disable-next-line no-restricted-imports +import { legacyRuleActionsSavedObjectType } from './legacy_saved_object_mappings'; +// eslint-disable-next-line no-restricted-imports +import { LegacyIRuleActionsAttributesSavedObjectAttributes } from './legacy_types'; +// eslint-disable-next-line no-restricted-imports +import { legacyGetRuleActionsFromSavedObject } from './legacy_utils'; + +/** + * @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 + */ +interface LegacyGetRuleActionsSavedObject { + ruleAlertId: string; + savedObjectsClient: AlertServices['savedObjectsClient']; +} + +/** + * @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 interface LegacyRulesActionsSavedObject { + id: string; + actions: RuleAlertAction[]; + alertThrottle: string | null; + ruleThrottle: string; +} + +/** + * @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 legacyGetRuleActionsSavedObject = async ({ + ruleAlertId, + savedObjectsClient, +}: LegacyGetRuleActionsSavedObject): Promise => { + const { + // eslint-disable-next-line @typescript-eslint/naming-convention + saved_objects, + } = await savedObjectsClient.find({ + type: legacyRuleActionsSavedObjectType, + perPage: 1, + search: `${ruleAlertId}`, + searchFields: ['ruleAlertId'], + }); + + if (!saved_objects[0]) { + return null; + } else { + return legacyGetRuleActionsFromSavedObject(saved_objects[0]); + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/migrations.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_migrations.ts similarity index 81% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/migrations.ts rename to x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_migrations.ts index 2853ca92e2a58..8edb372e62e44 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/migrations.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_migrations.ts @@ -11,13 +11,11 @@ import { SavedObjectSanitizedDoc, SavedObjectAttributes, } from '../../../../../../../src/core/server'; -import { IRuleActionsAttributesSavedObjectAttributes } from './types'; +// eslint-disable-next-line no-restricted-imports +import { LegacyIRuleActionsAttributesSavedObjectAttributes } from './legacy_types'; /** - * We keep this around to migrate and update data for the old deprecated rule actions saved object mapping but we - * do not use it anymore within the code base. Once we feel comfortable that users are upgrade far enough and this is no longer - * needed then it will be safe to remove this saved object and all its migrations - * @deprecated Remove this once we no longer need legacy migrations for rule actions (8.0.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 */ function isEmptyObject(obj: {}) { for (const attr in obj) { @@ -29,15 +27,12 @@ function isEmptyObject(obj: {}) { } /** - * We keep this around to migrate and update data for the old deprecated rule actions saved object mapping but we - * do not use it anymore within the code base. Once we feel comfortable that users are upgrade far enough and this is no longer - * needed then it will be safe to remove this saved object and all its migrations - * @deprecated Remove this once we no longer need legacy migrations for rule actions (8.0.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 ruleActionsSavedObjectMigration = { +export const legacyRuleActionsSavedObjectMigration = { '7.11.2': ( - doc: SavedObjectUnsanitizedDoc - ): SavedObjectSanitizedDoc => { + doc: SavedObjectUnsanitizedDoc + ): SavedObjectSanitizedDoc => { const { actions } = doc.attributes; const newActions = actions.reduce((acc, action) => { if ( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/saved_object_mappings.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_saved_object_mappings.ts similarity index 51% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/saved_object_mappings.ts rename to x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_saved_object_mappings.ts index 6522cb431d0fb..d821ca851b6b1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/saved_object_mappings.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_saved_object_mappings.ts @@ -6,23 +6,18 @@ */ import { SavedObjectsType } from '../../../../../../../src/core/server'; -import { ruleActionsSavedObjectMigration } from './migrations'; +// eslint-disable-next-line no-restricted-imports +import { legacyRuleActionsSavedObjectMigration } from './legacy_migrations'; /** - * We keep this around to migrate and update data for the old deprecated rule actions saved object mapping but we - * do not use it anymore within the code base. Once we feel comfortable that users are upgrade far enough and this is no longer - * needed then it will be safe to remove this saved object and all its migrations. - * * @deprecated Remove this once we no longer need legacy migrations for rule actions (8.0.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 */ -const ruleActionsSavedObjectType = 'siem-detection-engine-rule-actions'; +export const legacyRuleActionsSavedObjectType = 'siem-detection-engine-rule-actions'; /** - * We keep this around to migrate and update data for the old deprecated rule actions saved object mapping but we - * do not use it anymore within the code base. Once we feel comfortable that users are upgrade far enough and this is no longer - * needed then it will be safe to remove this saved object and all its migrations. - * * @deprecated Remove this once we no longer need legacy migrations for rule actions (8.0.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 */ -const ruleActionsSavedObjectMappings: SavedObjectsType['mappings'] = { +const legacyRuleActionsSavedObjectMappings: SavedObjectsType['mappings'] = { properties: { alertThrottle: { type: 'keyword', @@ -59,10 +54,10 @@ const ruleActionsSavedObjectMappings: SavedObjectsType['mappings'] = { * needed then it will be safe to remove this saved object and all its migrations. * @deprecated Remove this once we no longer need legacy migrations for rule actions (8.0.0) */ -export const type: SavedObjectsType = { - name: ruleActionsSavedObjectType, +export const legacyType: SavedObjectsType = { + name: legacyRuleActionsSavedObjectType, hidden: false, namespaceType: 'single', - mappings: ruleActionsSavedObjectMappings, - migrations: ruleActionsSavedObjectMigration, + mappings: legacyRuleActionsSavedObjectMappings, + migrations: legacyRuleActionsSavedObjectMigration, }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_types.ts similarity index 69% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/types.ts rename to x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_types.ts index e43e49b669424..0573829755c79 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_types.ts @@ -12,10 +12,10 @@ import { RuleAlertAction } from '../../../../common/detection_engine/types'; * We keep this around to migrate and update data for the old deprecated rule actions saved object mapping but we * do not use it anymore within the code base. Once we feel comfortable that users are upgrade far enough and this is no longer * needed then it will be safe to remove this saved object and all its migrations. - * @deprecated + * @deprecated Remove this once the legacy notification/side car is gone */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -export interface IRuleActionsAttributes extends Record { +export interface LegacyIRuleActionsAttributes extends Record { ruleAlertId: string; actions: RuleAlertAction[]; ruleThrottle: string; @@ -26,8 +26,18 @@ export interface IRuleActionsAttributes extends Record { * We keep this around to migrate and update data for the old deprecated rule actions saved object mapping but we * do not use it anymore within the code base. Once we feel comfortable that users are upgrade far enough and this is no longer * needed then it will be safe to remove this saved object and all its migrations. - * @deprecated + * @deprecated Remove this once the legacy notification/side car is gone */ -export interface IRuleActionsAttributesSavedObjectAttributes - extends IRuleActionsAttributes, +export interface LegacyIRuleActionsAttributesSavedObjectAttributes + extends LegacyIRuleActionsAttributes, SavedObjectAttributes {} + +/** + * @deprecated Remove this once the legacy notification/side car is gone + */ +export interface LegacyRuleActions { + id: string; + actions: RuleAlertAction[]; + ruleThrottle: string; + alertThrottle: string | null; +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_update_or_create_rule_actions_saved_object.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_update_or_create_rule_actions_saved_object.ts new file mode 100644 index 0000000000000..ce78bf92af490 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_update_or_create_rule_actions_saved_object.ts @@ -0,0 +1,59 @@ +/* + * 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 { AlertAction } from '../../../../../alerting/common'; +import { AlertServices } from '../../../../../alerting/server'; +// eslint-disable-next-line no-restricted-imports +import { legacyGetRuleActionsSavedObject } from './legacy_get_rule_actions_saved_object'; +// eslint-disable-next-line no-restricted-imports +import { legacyCreateRuleActionsSavedObject } from './legacy_create_rule_actions_saved_object'; +// eslint-disable-next-line no-restricted-imports +import { legacyUpdateRuleActionsSavedObject } from './legacy_update_rule_actions_saved_object'; +// eslint-disable-next-line no-restricted-imports +import { LegacyRuleActions } from './legacy_types'; + +/** + * @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 + */ +interface LegacyUpdateOrCreateRuleActionsSavedObject { + ruleAlertId: string; + savedObjectsClient: AlertServices['savedObjectsClient']; + actions: AlertAction[] | undefined; + throttle: string | null | undefined; +} + +/** + * @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 legacyUpdateOrCreateRuleActionsSavedObject = async ({ + savedObjectsClient, + ruleAlertId, + actions, + throttle, +}: LegacyUpdateOrCreateRuleActionsSavedObject): Promise => { + const ruleActions = await legacyGetRuleActionsSavedObject({ + ruleAlertId, + savedObjectsClient, + }); + + if (ruleActions != null) { + return legacyUpdateRuleActionsSavedObject({ + ruleAlertId, + savedObjectsClient, + actions, + throttle, + ruleActions, + }); + } else { + return legacyCreateRuleActionsSavedObject({ + ruleAlertId, + savedObjectsClient, + actions, + throttle, + }); + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_update_rule_actions_saved_object.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_update_rule_actions_saved_object.ts new file mode 100644 index 0000000000000..84c64c6a0d531 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_update_rule_actions_saved_object.ts @@ -0,0 +1,69 @@ +/* + * 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 { AlertServices } from '../../../../../alerting/server'; +// eslint-disable-next-line no-restricted-imports +import { legacyRuleActionsSavedObjectType } from './legacy_saved_object_mappings'; +// eslint-disable-next-line no-restricted-imports +import { LegacyRulesActionsSavedObject } from './legacy_get_rule_actions_saved_object'; +// eslint-disable-next-line no-restricted-imports +import { legacyGetThrottleOptions } from './legacy_utils'; +// eslint-disable-next-line no-restricted-imports +import { LegacyIRuleActionsAttributesSavedObjectAttributes } from './legacy_types'; +import { AlertAction } from '../../../../../alerting/common'; +import { transformAlertToRuleAction } from '../../../../common/detection_engine/transform_actions'; + +/** + * @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 + */ +interface LegacyUpdateRuleActionsSavedObject { + ruleAlertId: string; + savedObjectsClient: AlertServices['savedObjectsClient']; + actions: AlertAction[] | undefined; + throttle: string | null | undefined; + ruleActions: LegacyRulesActionsSavedObject; +} + +/** + * @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 legacyUpdateRuleActionsSavedObject = async ({ + ruleAlertId, + savedObjectsClient, + actions, + throttle, + ruleActions, +}: LegacyUpdateRuleActionsSavedObject): Promise => { + const throttleOptions = throttle + ? legacyGetThrottleOptions(throttle) + : { + ruleThrottle: ruleActions.ruleThrottle, + alertThrottle: ruleActions.alertThrottle, + }; + + const options = { + actions: + actions != null + ? actions.map((action) => transformAlertToRuleAction(action)) + : ruleActions.actions, + ...throttleOptions, + }; + + await savedObjectsClient.update( + legacyRuleActionsSavedObjectType, + ruleActions.id, + { + ruleAlertId, + ...options, + } + ); + + return { + id: ruleActions.id, + ...options, + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_utils.ts new file mode 100644 index 0000000000000..6be894c391b81 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_utils.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObjectsUpdateResponse } from 'kibana/server'; +import { RuleAlertAction } from '../../../../common/detection_engine/types'; +// eslint-disable-next-line no-restricted-imports +import { LegacyIRuleActionsAttributesSavedObjectAttributes } from './legacy_types'; + +/** + * @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 legacyGetThrottleOptions = ( + throttle: string | undefined | null = 'no_actions' +): { + ruleThrottle: string; + alertThrottle: string | null; +} => ({ + ruleThrottle: throttle ?? 'no_actions', + alertThrottle: ['no_actions', 'rule'].includes(throttle ?? 'no_actions') ? null : throttle, +}); + +/** + * @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 legacyGetRuleActionsFromSavedObject = ( + savedObject: SavedObjectsUpdateResponse +): { + id: string; + actions: RuleAlertAction[]; + alertThrottle: string | null; + ruleThrottle: string; +} => ({ + id: savedObject.id, + actions: savedObject.attributes.actions || [], + alertThrottle: savedObject.attributes.alertThrottle || null, + ruleThrottle: savedObject.attributes.ruleThrottle || 'no_actions', +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts index 5417417caad6b..cceda063e987b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts @@ -8,12 +8,7 @@ import { get } from 'lodash/fp'; import { Readable } from 'stream'; -import { - SavedObject, - SavedObjectAttributes, - SavedObjectsFindResponse, - SavedObjectsFindResult, -} from 'kibana/server'; +import { SavedObject, SavedObjectAttributes, SavedObjectsFindResult } from 'kibana/server'; import type { MachineLearningJobIdOrUndefined, From, @@ -216,12 +211,6 @@ export const isRuleStatusSavedObjectType = ( return get('attributes', obj) != null; }; -export const isRuleStatusFindType = ( - obj: unknown -): obj is SavedObjectsFindResponse => { - return get('saved_objects', obj) != null; -}; - export interface CreateRulesOptions { rulesClient: RulesClient; anomalyThreshold: AnomalyThresholdOrUndefined; 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 574b16207786f..2cf7e95f3c621 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 @@ -12,13 +12,17 @@ import { transformToNotifyWhen, transformToAlertThrottle, transformFromAlertThrottle, + transformActions, } from './utils'; -import { SanitizedAlert } from '../../../../../alerting/common'; +import { AlertAction, SanitizedAlert } from '../../../../../alerting/common'; import { RuleParams } from '../schemas/rule_schemas'; import { NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE, } from '../../../../common/constants'; +import { FullResponseSchema } from '../../../../common/detection_engine/schemas/request'; +// eslint-disable-next-line no-restricted-imports +import { LegacyRuleActions } from '../rule_actions/legacy_types'; describe('utils', () => { describe('#calculateInterval', () => { @@ -278,73 +282,307 @@ describe('utils', () => { describe('#transformFromAlertThrottle', () => { test('muteAll returns "NOTIFICATION_THROTTLE_NO_ACTIONS" even with notifyWhen set and actions has an array element', () => { expect( - transformFromAlertThrottle({ - muteAll: true, - notifyWhen: 'onActiveAlert', - actions: [ - { - group: 'group', - id: 'id-123', - actionTypeId: 'id-456', - params: {}, - }, - ], - } as SanitizedAlert) + transformFromAlertThrottle( + { + muteAll: true, + notifyWhen: 'onActiveAlert', + actions: [ + { + group: 'group', + id: 'id-123', + actionTypeId: 'id-456', + params: {}, + }, + ], + } as SanitizedAlert, + undefined + ) ).toEqual(NOTIFICATION_THROTTLE_NO_ACTIONS); }); test('returns "NOTIFICATION_THROTTLE_NO_ACTIONS" if actions is an empty array and we do not have a throttle', () => { expect( - transformFromAlertThrottle({ - muteAll: false, - notifyWhen: 'onActiveAlert', - actions: [], - } as unknown as SanitizedAlert) + transformFromAlertThrottle( + { + muteAll: false, + notifyWhen: 'onActiveAlert', + actions: [], + } as unknown as SanitizedAlert, + undefined + ) ).toEqual(NOTIFICATION_THROTTLE_NO_ACTIONS); }); test('returns "NOTIFICATION_THROTTLE_NO_ACTIONS" if actions is an empty array and we have a throttle', () => { expect( - transformFromAlertThrottle({ - muteAll: false, - notifyWhen: 'onThrottleInterval', - actions: [], - throttle: '1d', - } as unknown as SanitizedAlert) + transformFromAlertThrottle( + { + muteAll: false, + notifyWhen: 'onThrottleInterval', + actions: [], + throttle: '1d', + } as unknown as SanitizedAlert, + undefined + ) ).toEqual(NOTIFICATION_THROTTLE_NO_ACTIONS); }); test('it returns "NOTIFICATION_THROTTLE_RULE" if "notifyWhen" is set, muteAll is false and we have an actions array', () => { expect( - transformFromAlertThrottle({ - muteAll: false, - notifyWhen: 'onActiveAlert', - actions: [ - { - group: 'group', - id: 'id-123', - actionTypeId: 'id-456', - params: {}, - }, - ], - } as SanitizedAlert) + transformFromAlertThrottle( + { + muteAll: false, + notifyWhen: 'onActiveAlert', + actions: [ + { + group: 'group', + id: 'id-123', + actionTypeId: 'id-456', + params: {}, + }, + ], + } as SanitizedAlert, + undefined + ) ).toEqual(NOTIFICATION_THROTTLE_RULE); }); test('it returns "NOTIFICATION_THROTTLE_RULE" if "notifyWhen" and "throttle" are not set, but we have an actions array', () => { expect( - transformFromAlertThrottle({ - muteAll: false, - actions: [ - { - group: 'group', - id: 'id-123', - actionTypeId: 'id-456', - params: {}, - }, - ], - } as SanitizedAlert) + transformFromAlertThrottle( + { + muteAll: false, + actions: [ + { + group: 'group', + id: 'id-123', + actionTypeId: 'id-456', + params: {}, + }, + ], + } as SanitizedAlert, + undefined + ) ).toEqual(NOTIFICATION_THROTTLE_RULE); }); + + test('it will use the "rule" and not the "legacyRuleActions" if the rule and actions is defined', () => { + const legacyRuleActions: LegacyRuleActions = { + id: 'id_1', + ruleThrottle: '', + alertThrottle: '', + actions: [ + { + id: 'id_2', + group: 'group', + action_type_id: 'actionTypeId', + params: {}, + }, + ], + }; + + expect( + transformFromAlertThrottle( + { + muteAll: true, + notifyWhen: 'onActiveAlert', + actions: [ + { + group: 'group', + id: 'id-123', + actionTypeId: 'id-456', + params: {}, + }, + ], + } as SanitizedAlert, + legacyRuleActions + ) + ).toEqual(NOTIFICATION_THROTTLE_NO_ACTIONS); + }); + + test('it will use the "legacyRuleActions" and not the "rule" if the rule actions is an empty array', () => { + const legacyRuleActions: LegacyRuleActions = { + id: 'id_1', + ruleThrottle: NOTIFICATION_THROTTLE_RULE, + alertThrottle: null, + actions: [ + { + id: 'id_2', + group: 'group', + action_type_id: 'actionTypeId', + params: {}, + }, + ], + }; + + expect( + transformFromAlertThrottle( + { + muteAll: true, + notifyWhen: 'onActiveAlert', + actions: [], + } as unknown as SanitizedAlert, + legacyRuleActions + ) + ).toEqual(NOTIFICATION_THROTTLE_RULE); + }); + + test('it will use the "legacyRuleActions" and not the "rule" if the rule actions is a null', () => { + const legacyRuleActions: LegacyRuleActions = { + id: 'id_1', + ruleThrottle: NOTIFICATION_THROTTLE_RULE, + alertThrottle: null, + actions: [ + { + id: 'id_2', + group: 'group', + action_type_id: 'actionTypeId', + params: {}, + }, + ], + }; + + expect( + transformFromAlertThrottle( + { + muteAll: true, + notifyWhen: 'onActiveAlert', + actions: null, + } as unknown as SanitizedAlert, + legacyRuleActions + ) + ).toEqual(NOTIFICATION_THROTTLE_RULE); + }); + }); + + describe('#transformActions', () => { + test('It transforms two alert actions', () => { + const alertAction: AlertAction[] = [ + { + id: 'id_1', + group: 'group', + actionTypeId: 'actionTypeId', + params: {}, + }, + { + id: 'id_2', + group: 'group', + actionTypeId: 'actionTypeId', + params: {}, + }, + ]; + + const transformed = transformActions(alertAction, null); + expect(transformed).toEqual([ + { + id: 'id_1', + group: 'group', + action_type_id: 'actionTypeId', + params: {}, + }, + { + id: 'id_2', + group: 'group', + action_type_id: 'actionTypeId', + params: {}, + }, + ]); + }); + + test('It transforms two alert actions but not a legacyRuleActions if this is also passed in', () => { + const alertAction: AlertAction[] = [ + { + id: 'id_1', + group: 'group', + actionTypeId: 'actionTypeId', + params: {}, + }, + { + id: 'id_2', + group: 'group', + actionTypeId: 'actionTypeId', + params: {}, + }, + ]; + const legacyRuleActions: LegacyRuleActions = { + id: 'id_1', + ruleThrottle: '', + alertThrottle: '', + actions: [ + { + id: 'id_2', + group: 'group', + action_type_id: 'actionTypeId', + params: {}, + }, + ], + }; + const transformed = transformActions(alertAction, legacyRuleActions); + expect(transformed).toEqual([ + { + id: 'id_1', + group: 'group', + action_type_id: 'actionTypeId', + params: {}, + }, + { + id: 'id_2', + group: 'group', + action_type_id: 'actionTypeId', + params: {}, + }, + ]); + }); + + test('It will transform the legacyRuleActions if the alertAction is an empty array', () => { + const alertAction: AlertAction[] = []; + const legacyRuleActions: LegacyRuleActions = { + id: 'id_1', + ruleThrottle: '', + alertThrottle: '', + actions: [ + { + id: 'id_2', + group: 'group', + action_type_id: 'actionTypeId', + params: {}, + }, + ], + }; + const transformed = transformActions(alertAction, legacyRuleActions); + expect(transformed).toEqual([ + { + id: 'id_2', + group: 'group', + action_type_id: 'actionTypeId', + params: {}, + }, + ]); + }); + + test('It will transform the legacyRuleActions if the alertAction is undefined', () => { + const legacyRuleActions: LegacyRuleActions = { + id: 'id_1', + ruleThrottle: '', + alertThrottle: '', + actions: [ + { + id: 'id_2', + group: 'group', + action_type_id: 'actionTypeId', + params: {}, + }, + ], + }; + const transformed = transformActions(undefined, legacyRuleActions); + expect(transformed).toEqual([ + { + id: 'id_2', + group: 'group', + action_type_id: 'actionTypeId', + params: {}, + }, + ]); + }); }); }); 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 3fdd97b7d933f..4647a4a9951df 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 @@ -27,7 +27,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 { AlertNotifyWhenType, SanitizedAlert } from '../../../../../alerting/common'; +import { AlertAction, AlertNotifyWhenType, SanitizedAlert } from '../../../../../alerting/common'; import { DescriptionOrUndefined, AnomalyThresholdOrUndefined, @@ -61,6 +61,10 @@ import { NOTIFICATION_THROTTLE_RULE, } from '../../../../common/constants'; import { RulesClient } from '../../../../../alerting/server'; +// eslint-disable-next-line no-restricted-imports +import { LegacyRuleActions } from '../rule_actions/legacy_types'; +import { FullResponseSchema } from '../../../../common/detection_engine/schemas/request'; +import { transformAlertToRuleAction } from '../../../../common/detection_engine/transform_actions'; export const calculateInterval = ( interval: string | undefined, @@ -213,24 +217,56 @@ export const transformToAlertThrottle = (throttle: string | null | undefined): s } }; +/** + * Given a set of actions from an "alerting" Saved Object (SO) this will transform it into a "security_solution" alert action. + * If this detects any legacy rule actions it will transform it. If both are sent in which is not typical but possible due to + * the split nature of the API's this will prefer the usage of the non-legacy version. Eventually the "legacyRuleActions" should + * be removed. + * @param alertAction The alert action form a "alerting" Saved Object (SO). + * @param legacyRuleActions Legacy "side car" rule actions that if it detects it being passed it in will transform using it. + * @returns The actions of the FullResponseSchema + */ +export const transformActions = ( + alertAction: AlertAction[] | undefined, + legacyRuleActions: LegacyRuleActions | null | undefined +): FullResponseSchema['actions'] => { + if (alertAction != null && alertAction.length !== 0) { + return alertAction.map((action) => transformAlertToRuleAction(action)); + } else if (legacyRuleActions != null) { + return legacyRuleActions.actions; + } else { + return []; + } +}; + /** * Given a throttle from an "alerting" Saved Object (SO) this will transform it into a "security_solution" - * throttle type. - * @params throttle The throttle from a "alerting" Saved Object (SO) + * throttle type. If given the "legacyRuleActions" but we detect that the rule for an unknown reason has actions + * on it to which should not be typical but possible due to the split nature of the API's, this will prefer the + * usage of the non-legacy version. Eventually the "legacyRuleActions" should be removed. + * @param throttle The throttle from a "alerting" Saved Object (SO) + * @param legacyRuleActions Legacy "side car" rule actions that if it detects it being passed it in will transform using it. * @returns The "security_solution" throttle */ -export const transformFromAlertThrottle = (rule: SanitizedAlert): string => { - if (rule.muteAll || rule.actions.length === 0) { - return NOTIFICATION_THROTTLE_NO_ACTIONS; - } else if ( - rule.notifyWhen === 'onActiveAlert' || - (rule.throttle == null && rule.notifyWhen == null) - ) { - return NOTIFICATION_THROTTLE_RULE; - } else if (rule.throttle == null) { - return NOTIFICATION_THROTTLE_NO_ACTIONS; +export const transformFromAlertThrottle = ( + rule: SanitizedAlert, + legacyRuleActions: LegacyRuleActions | null | undefined +): string => { + if (legacyRuleActions == null || (rule.actions != null && rule.actions.length > 0)) { + if (rule.muteAll || rule.actions.length === 0) { + return NOTIFICATION_THROTTLE_NO_ACTIONS; + } else if ( + rule.notifyWhen === 'onActiveAlert' || + (rule.throttle == null && rule.notifyWhen == null) + ) { + return NOTIFICATION_THROTTLE_RULE; + } else if (rule.throttle == null) { + return NOTIFICATION_THROTTLE_NO_ACTIONS; + } else { + return rule.throttle; + } } else { - return rule.throttle; + return legacyRuleActions.ruleThrottle; } }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts index 5214be513a0e6..eef20af0e564d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts @@ -35,8 +35,11 @@ import { transformFromAlertThrottle, transformToAlertThrottle, transformToNotifyWhen, + transformActions, } from '../rules/utils'; import { ruleTypeMappings } from '../signals/utils'; +// eslint-disable-next-line no-restricted-imports +import { LegacyRuleActions } from '../rule_actions/legacy_types'; // These functions provide conversions from the request API schema to the internal rule schema and from the internal rule schema // to the response API schema. This provides static type-check assurances that the internal schema is in sync with the API schema for @@ -279,7 +282,8 @@ export const commonParamsCamelToSnake = (params: BaseRuleParams) => { export const internalRuleToAPIResponse = ( rule: SanitizedAlert, - ruleStatus?: IRuleStatusSOAttributes + ruleStatus?: IRuleStatusSOAttributes, + legacyRuleActions?: LegacyRuleActions | null ): FullResponseSchema => { const mergedStatus = ruleStatus ? mergeAlertWithSidecarStatus(rule, ruleStatus) : undefined; return { @@ -298,14 +302,8 @@ export const internalRuleToAPIResponse = ( // Type specific security solution rule params ...typeSpecificCamelToSnake(rule.params), // Actions - throttle: transformFromAlertThrottle(rule), - actions: - rule?.actions.map((action) => ({ - group: action.group, - id: action.id, - action_type_id: action.actionTypeId, - params: action.params, - })) ?? [], + throttle: transformFromAlertThrottle(rule, legacyRuleActions), + actions: transformActions(rule.actions, legacyRuleActions), // Rule status status: mergedStatus?.status ?? undefined, status_date: mergedStatus?.statusDate ?? undefined, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/legacy_notifications/one_action.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/legacy_notifications/one_action.json new file mode 100644 index 0000000000000..b1500ac6fa6b7 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/legacy_notifications/one_action.json @@ -0,0 +1,14 @@ +{ + "name": "Legacy notification with one action", + "interval": "1m", + "actions": [ + { + "id": "879e8ff0-1be1-11ec-a722-83da1c22a481", + "group": "default", + "params": { + "message": "Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts" + }, + "actionTypeId": ".slack" + } + ] +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/post_legacy_notification.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/post_legacy_notification.sh new file mode 100755 index 0000000000000..f160d1e899a55 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/post_legacy_notification.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Uses a default if no argument is specified +NOTIFICATIONS=${2:-./legacy_notifications/one_action.json} + +# Posts a legacy notification "side car". This should be removed once there are no more legacy notifications. +# First argument should be a valid alert_id and the second argument should be to a notification file which contains the legacy notification +# Example: ./post_legacy_notification.sh acd008d0-1b19-11ec-b5bd-7733d658a2ea +# Example: ./post_legacy_notification.sh acd008d0-1b19-11ec-b5bd-7733d658a2ea ./legacy_notifications/one_action.json +curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/internal/api/detection/legacy/notifications?alert_id="$1" \ + -d @${NOTIFICATIONS} | jq . diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 14da8ca650960..59bf5057f2796 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -67,7 +67,7 @@ import { APP_ID, SERVER_APP_ID, SIGNALS_ID, - NOTIFICATIONS_ID, + LEGACY_NOTIFICATIONS_ID, QUERY_RULE_TYPE_ID, DEFAULT_SPACE_ID, INDICATOR_RULE_TYPE_ID, @@ -104,6 +104,10 @@ import { getKibanaPrivilegesFeaturePrivileges } from './features'; import { EndpointMetadataService } from './endpoint/services/metadata'; import { CreateRuleOptions } from './lib/detection_engine/rule_types/types'; import { ctiFieldMap } from './lib/detection_engine/rule_types/field_maps/cti'; +// eslint-disable-next-line no-restricted-imports +import { legacyRulesNotificationAlertType } from './lib/detection_engine/notifications/legacy_rules_notification_alert_type'; +// eslint-disable-next-line no-restricted-imports +import { legacyIsNotificationAlertExecutor } from './lib/detection_engine/notifications/legacy_types'; export interface SetupPlugins { alerting: AlertingSetup; @@ -296,7 +300,7 @@ export class Plugin implements IPlugin { diff --git a/x-pack/plugins/security_solution/server/routes/index.ts b/x-pack/plugins/security_solution/server/routes/index.ts index d68bfa0846b96..95ec33868f52d 100644 --- a/x-pack/plugins/security_solution/server/routes/index.ts +++ b/x-pack/plugins/security_solution/server/routes/index.ts @@ -56,6 +56,8 @@ import { persistPinnedEventRoute } from '../lib/timeline/routes/pinned_events'; import { SetupPlugins } from '../plugin'; import { ConfigType } from '../config'; import { installPrepackedTimelinesRoute } from '../lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines'; +// eslint-disable-next-line no-restricted-imports +import { legacyCreateLegacyNotificationRoute } from '../lib/detection_engine/routes/rules/legacy_create_legacy_notification'; export const initRoutes = ( router: SecuritySolutionPluginRouter, @@ -75,6 +77,9 @@ export const initRoutes = ( deleteRulesRoute(router, isRuleRegistryEnabled); findRulesRoute(router, isRuleRegistryEnabled); + // Once we no longer have the legacy notifications system/"side car actions" this should be removed. + legacyCreateLegacyNotificationRoute(router); + // TODO: pass isRuleRegistryEnabled to all relevant routes addPrepackedRulesRoute(router, config, security, isRuleRegistryEnabled); diff --git a/x-pack/plugins/security_solution/server/saved_objects.ts b/x-pack/plugins/security_solution/server/saved_objects.ts index 42abb3dab2ac4..1523b3ddf4cbf 100644 --- a/x-pack/plugins/security_solution/server/saved_objects.ts +++ b/x-pack/plugins/security_solution/server/saved_objects.ts @@ -12,7 +12,8 @@ import { type as ruleStatusType, ruleAssetType, } from './lib/detection_engine/rules/saved_object_mappings'; -import { type as ruleActionsType } from './lib/detection_engine/rule_actions/saved_object_mappings'; +// eslint-disable-next-line no-restricted-imports +import { legacyType as legacyRuleActionsType } from './lib/detection_engine/rule_actions/legacy_saved_object_mappings'; import { type as signalsMigrationType } from './lib/detection_engine/migrations/saved_objects'; import { exceptionsArtifactType, @@ -22,7 +23,7 @@ import { const types = [ noteType, pinnedEventType, - ruleActionsType, + legacyRuleActionsType, ruleStatusType, ruleAssetType, timelineType, diff --git a/x-pack/plugins/security_solution/server/types.ts b/x-pack/plugins/security_solution/server/types.ts index f47944b5fd392..7822a5b8ba3c5 100644 --- a/x-pack/plugins/security_solution/server/types.ts +++ b/x-pack/plugins/security_solution/server/types.ts @@ -12,6 +12,7 @@ import type { AlertingApiRequestHandlerContext } from '../../alerting/server'; import { AppClient } from './client'; import { RuleExecutionLogClient } from './lib/detection_engine/rule_execution_log/rule_execution_log_client'; +import type { ActionsApiRequestHandlerContext } from '../../actions/server'; export { AppClient }; @@ -25,6 +26,7 @@ export type SecuritySolutionRequestHandlerContext = RequestHandlerContext & { securitySolution: AppRequestContext; licensing: LicensingApiRequestHandlerContext; alerting: AlertingApiRequestHandlerContext; + actions: ActionsApiRequestHandlerContext; lists?: ListsApiRequestHandlerContext; };