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; };