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 index 6c0ffa65a9afa..f064380cc4a13 100644 --- 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 @@ -88,7 +88,7 @@ describe('legacyRules_notification_alert_type', () => { }); await alert.executor(payload); expect(logger.error).toHaveBeenCalledWith( - `Security Solution notification (Legacy) saved object for alert ${payload.params.ruleAlertId} was not found` + `Security Solution notification (Legacy) saved object for alert ${payload.params.ruleAlertId} was not found with id: \"1111\". space id: \"\" This indicates a dangling (Legacy) notification alert. You should delete this rule through \"Kibana UI -> Stack Management -> Rules and Connectors\" to remove this error message.` ); }); 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 index fa05b1fb5b07a..a5622ae68b6b9 100644 --- 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 @@ -51,11 +51,7 @@ export const legacyRulesNotificationAlertType = ({ }, 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.' - ); + async executor({ startedAt, previousStartedAt, alertId, services, params, spaceId }) { const ruleAlertSavedObject = await services.savedObjectsClient.get( 'alert', params.ruleAlertId @@ -63,17 +59,26 @@ export const legacyRulesNotificationAlertType = ({ if (!ruleAlertSavedObject.attributes.params) { logger.error( - `Security Solution notification (Legacy) saved object for alert ${params.ruleAlertId} was not found` + [ + `Security Solution notification (Legacy) saved object for alert ${params.ruleAlertId} was not found with`, + `id: "${alertId}".`, + `space id: "${spaceId}"`, + 'This indicates a dangling (Legacy) notification alert.', + 'You should delete this rule through "Kibana UI -> Stack Management -> Rules and Connectors" to remove this error message.', + ].join(' ') ); 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.`, + `space id: "${spaceId}"`, + 'Editing or updating this rule through "Kibana UI -> Security -> Alerts -> Manage Rules"', + 'will auto-migrate the rule to the new notification system and remove this warning message.', ].join(' ') ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts index 054238cf6fa45..149227084ace0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts @@ -25,6 +25,7 @@ import { transformValidateBulkError } from './validate'; import { transformBulkError, buildSiemResponse, createBulkErrorObject } from '../utils'; import { deleteRules } from '../../rules/delete_rules'; import { readRules } from '../../rules/read_rules'; +import { legacyMigrate } from '../../rules/utils'; type Config = RouteConfig; type Handler = RequestHandler< @@ -60,6 +61,7 @@ export const deleteRulesBulkRoute = ( } const ruleStatusClient = context.securitySolution.getExecutionLogClient(); + const savedObjectsClient = context.core.savedObjects.client; const rules = await Promise.all( request.body.map(async (payloadRule) => { @@ -76,22 +78,27 @@ export const deleteRulesBulkRoute = ( try { const rule = await readRules({ rulesClient, id, ruleId, isRuleRegistryEnabled }); - if (!rule) { + const migratedRule = await legacyMigrate({ + rulesClient, + savedObjectsClient, + rule, + }); + if (!migratedRule) { return getIdBulkError({ id, ruleId }); } const ruleStatus = await ruleStatusClient.getCurrentStatus({ - ruleId: rule.id, + ruleId: migratedRule.id, spaceId: context.securitySolution.getSpaceId(), }); await deleteRules({ - ruleId: rule.id, + ruleId: migratedRule.id, rulesClient, ruleStatusClient, }); return transformValidateBulkError( idOrRuleIdOrUnknown, - rule, + migratedRule, ruleStatus, isRuleRegistryEnabled ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.ts index abcf0d07a33b6..3bb7778e5bc5e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.ts @@ -19,6 +19,7 @@ import { getIdError, transform } from './utils'; import { buildSiemResponse } from '../utils'; import { readRules } from '../../rules/read_rules'; +import { legacyMigrate } from '../../rules/utils'; export const deleteRulesRoute = ( router: SecuritySolutionPluginRouter, @@ -47,14 +48,20 @@ export const deleteRulesRoute = ( const { id, rule_id: ruleId } = request.query; const rulesClient = context.alerting?.getRulesClient(); - + const savedObjectsClient = context.core.savedObjects.client; if (!rulesClient) { return siemResponse.error({ statusCode: 404 }); } const ruleStatusClient = context.securitySolution.getExecutionLogClient(); const rule = await readRules({ isRuleRegistryEnabled, rulesClient, id, ruleId }); - if (!rule) { + const migratedRule = await legacyMigrate({ + rulesClient, + savedObjectsClient, + rule, + }); + + if (!migratedRule) { const error = getIdError({ id, ruleId }); return siemResponse.error({ body: error.message, @@ -63,15 +70,16 @@ export const deleteRulesRoute = ( } const currentStatus = await ruleStatusClient.getCurrentStatus({ - ruleId: rule.id, + ruleId: migratedRule.id, spaceId: context.securitySolution.getSpaceId(), }); + await deleteRules({ - ruleId: rule.id, + ruleId: migratedRule.id, rulesClient, ruleStatusClient, }); - const transformed = transform(rule, currentStatus, isRuleRegistryEnabled); + const transformed = transform(migratedRule, currentStatus, isRuleRegistryEnabled); if (transformed == null) { return siemResponse.error({ statusCode: 500, body: 'failed to transform alert' }); } else { 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 4ab8afd796f6d..b57424f495c6c 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 @@ -316,7 +316,7 @@ export const legacyMigrate = async ({ } /** * On update / patch I'm going to take the actions as they are, better off taking rules client.find (siem.notification) result - * and putting that into the actions array of the rule, then set the rules onThrottle property, notifyWhen and throttle from null -> actualy value (1hr etc..) + * and putting that into the actions array of the rule, then set the rules onThrottle property, notifyWhen and throttle from null -> actual value (1hr etc..) * Then use the rules client to delete the siem.notification * Then with the legacy Rule Actions saved object type, just delete it. */ @@ -325,6 +325,7 @@ export const legacyMigrate = async ({ const [siemNotification, legacyRuleActionsSO] = await Promise.all([ rulesClient.find({ options: { + filter: 'alert.attributes.alertTypeId:(siem.notifications)', hasReference: { type: 'alert', id: rule.id, diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules.ts index e5f828d0f862d..19076506998a9 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules.ts @@ -7,9 +7,11 @@ import expect from '@kbn/expect'; +import { BASE_ALERTING_API_PATH } from '../../../../plugins/alerting/common'; import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/security_solution/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { + createLegacyRuleAction, createRule, createSignalsIndex, deleteAllAlerts, @@ -18,6 +20,7 @@ import { getSimpleRuleOutput, getSimpleRuleOutputWithoutRuleId, getSimpleRuleWithoutRuleId, + getWebHookAction, removeServerGeneratedProperties, removeServerGeneratedPropertiesIncludingRuleId, } from '../../utils'; @@ -100,6 +103,119 @@ export default ({ getService }: FtrProviderContext): void => { status_code: 404, }); }); + + /** + * @deprecated Once the legacy notification system is removed, remove this test too. + */ + it('should have exactly 1 legacy action before a delete within alerting', async () => { + // create an action + const { body: hookAction } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'true') + .send(getWebHookAction()) + .expect(200); + + // create a rule without actions + const createRuleBody = await createRule(supertest, log, getSimpleRule('rule-1')); + + // Add a legacy rule action to the body of the rule + await createLegacyRuleAction(supertest, createRuleBody.id, hookAction.id); + + // Test to ensure that we have exactly 1 legacy action by querying the Alerting client REST API directly + // See: https://www.elastic.co/guide/en/kibana/current/find-rules-api.html + // Note: We specifically query for both the filter of type "siem.notifications" and the "has_reference" to keep it very specific + const { body: alertFind } = await supertest + .get(`${BASE_ALERTING_API_PATH}/rules/_find`) + .query({ + page: 1, + per_page: 10, + filter: 'alert.attributes.alertTypeId:(siem.notifications)', + has_reference: JSON.stringify({ id: createRuleBody.id, type: 'alert' }), + }) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + // Expect that we have exactly 1 legacy rule before the deletion + expect(alertFind.total).to.eql(1); + }); + + /** + * @deprecated Once the legacy notification system is removed, remove this test too. + */ + it('should return the legacy action in the response body when it deletes a rule that has one', async () => { + // create an action + const { body: hookAction } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'true') + .send(getWebHookAction()) + .expect(200); + + // create a rule without actions + const createRuleBody = await createRule(supertest, log, getSimpleRule('rule-1')); + + // Add a legacy rule action to the body of the rule + await createLegacyRuleAction(supertest, createRuleBody.id, hookAction.id); + + // delete the rule with the legacy action + const { body } = await supertest + .delete(`${DETECTION_ENGINE_RULES_URL}?id=${createRuleBody.id}`) + .set('kbn-xsrf', 'true') + .expect(200); + + // ensure the actions contains the response + expect(body.actions).to.eql([ + { + id: hookAction.id, + action_type_id: hookAction.actionTypeId, + group: 'default', + params: { + message: + 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', + }, + }, + ]); + }); + + /** + * @deprecated Once the legacy notification system is removed, remove this test too. + */ + it('should delete a legacy action when it deletes a rule that has one', async () => { + // create an action + const { body: hookAction } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'true') + .send(getWebHookAction()) + .expect(200); + + // create a rule without actions + const createRuleBody = await createRule(supertest, log, getSimpleRule('rule-1')); + + // Add a legacy rule action to the body of the rule + await createLegacyRuleAction(supertest, createRuleBody.id, hookAction.id); + + await supertest + .delete(`${DETECTION_ENGINE_RULES_URL}?id=${createRuleBody.id}`) + .set('kbn-xsrf', 'true') + .expect(200); + + // Test to ensure that we have exactly 0 legacy actions by querying the Alerting client REST API directly + // See: https://www.elastic.co/guide/en/kibana/current/find-rules-api.html + // Note: We specifically query for both the filter of type "siem.notifications" and the "has_reference" to keep it very specific + const { body: bodyAfterDelete } = await supertest + .get(`${BASE_ALERTING_API_PATH}/rules/_find`) + .query({ + page: 1, + per_page: 10, + filter: 'alert.attributes.alertTypeId:(siem.notifications)', + has_reference: JSON.stringify({ id: createRuleBody.id, type: 'alert' }), + }) + .set('kbn-xsrf', 'true') + .send(); + + // Expect that we have exactly 0 legacy rules after the deletion + expect(bodyAfterDelete.total).to.eql(0); + }); }); }); }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules_bulk.ts index b7517697ad2a9..69be1f2eb0aff 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules_bulk.ts @@ -7,9 +7,11 @@ import expect from '@kbn/expect'; +import { BASE_ALERTING_API_PATH } from '../../../../plugins/alerting/common'; import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/security_solution/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { + createLegacyRuleAction, createRule, createSignalsIndex, deleteAllAlerts, @@ -18,6 +20,7 @@ import { getSimpleRuleOutput, getSimpleRuleOutputWithoutRuleId, getSimpleRuleWithoutRuleId, + getWebHookAction, removeServerGeneratedProperties, removeServerGeneratedPropertiesIncludingRuleId, } from '../../utils'; @@ -249,6 +252,148 @@ export default ({ getService }: FtrProviderContext): void => { }, ]); }); + + /** + * @deprecated Once the legacy notification system is removed, remove this test too. + */ + it('should return the legacy action in the response body when it deletes a rule that has one', async () => { + // create an action + const { body: hookAction } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'true') + .send(getWebHookAction()) + .expect(200); + + // create a rule without actions + const createRuleBody = await createRule(supertest, log, getSimpleRule('rule-1')); + + // Add a legacy rule action to the body of the rule + await createLegacyRuleAction(supertest, createRuleBody.id, hookAction.id); + + // delete the rule with the legacy action + const { body } = await supertest + .delete(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) + .send([{ id: createRuleBody.id }]) + .set('kbn-xsrf', 'true') + .expect(200); + + // ensure we only get one body back + expect(body.length).to.eql(1); + + // ensure that its actions equal what we expect + expect(body[0].actions).to.eql([ + { + id: hookAction.id, + action_type_id: hookAction.actionTypeId, + group: 'default', + params: { + message: + 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', + }, + }, + ]); + }); + + /** + * @deprecated Once the legacy notification system is removed, remove this test too. + */ + it('should return 2 legacy actions in the response body when it deletes 2 rules', async () => { + // create two different actions + const { body: hookAction1 } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'true') + .send(getWebHookAction()) + .expect(200); + const { body: hookAction2 } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'true') + .send(getWebHookAction()) + .expect(200); + + // create 2 rules without actions + const createRuleBody1 = await createRule(supertest, log, getSimpleRule('rule-1')); + const createRuleBody2 = await createRule(supertest, log, getSimpleRule('rule-2')); + + // Add a legacy rule action to the body of the 2 rules + await createLegacyRuleAction(supertest, createRuleBody1.id, hookAction1.id); + await createLegacyRuleAction(supertest, createRuleBody2.id, hookAction2.id); + + // delete 2 rules where both have legacy actions + const { body } = await supertest + .delete(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) + .send([{ id: createRuleBody1.id }, { id: createRuleBody2.id }]) + .set('kbn-xsrf', 'true') + .expect(200); + + // ensure we only get two bodies back + expect(body.length).to.eql(2); + + // ensure that its actions equal what we expect for both responses + expect(body[0].actions).to.eql([ + { + id: hookAction1.id, + action_type_id: hookAction1.actionTypeId, + group: 'default', + params: { + message: + 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', + }, + }, + ]); + expect(body[1].actions).to.eql([ + { + id: hookAction2.id, + action_type_id: hookAction2.actionTypeId, + group: 'default', + params: { + message: + 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', + }, + }, + ]); + }); + + /** + * @deprecated Once the legacy notification system is removed, remove this test too. + */ + it('should delete a legacy action when it deletes a rule that has one', async () => { + // create an action + const { body: hookAction } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'true') + .send(getWebHookAction()) + .expect(200); + + // create a rule without actions + const createRuleBody = await createRule(supertest, log, getSimpleRule('rule-1')); + + // Add a legacy rule action to the body of the rule + await createLegacyRuleAction(supertest, createRuleBody.id, hookAction.id); + + // bulk delete the rule + await supertest + .delete(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) + .send([{ id: createRuleBody.id }]) + .set('kbn-xsrf', 'true') + .expect(200); + + // Test to ensure that we have exactly 0 legacy actions by querying the Alerting client REST API directly + // See: https://www.elastic.co/guide/en/kibana/current/find-rules-api.html + // Note: We specifically query for both the filter of type "siem.notifications" and the "has_reference" to keep it very specific + const { body: bodyAfterDelete } = await supertest + .get(`${BASE_ALERTING_API_PATH}/rules/_find`) + .query({ + page: 1, + per_page: 10, + filter: 'alert.attributes.alertTypeId:(siem.notifications)', + has_reference: JSON.stringify({ id: createRuleBody.id, type: 'alert' }), + }) + .set('kbn-xsrf', 'true') + .send(); + + // Expect that we have exactly 0 legacy rules after the deletion + expect(bodyAfterDelete.total).to.eql(0); + }); }); }); }; diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts index 2ebaed7defe67..2f9fba7430d59 100644 --- a/x-pack/test/detection_engine_api_integration/utils.ts +++ b/x-pack/test/detection_engine_api_integration/utils.ts @@ -604,6 +604,7 @@ export const createLegacyRuleAction = async ( }, ], }); + /** * Deletes the signals index for use inside of afterEach blocks of tests * @param supertest The supertest client library