diff --git a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts index f3c64889e08a9..e406b37ae61fb 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts @@ -169,6 +169,16 @@ describe('validateParams()', () => { }); }).toThrowError(`error validating action params: error parsing timestamp "${timestamp}"`); }); + + test('should validate and throw error when dedupKey is missing on resolve', () => { + expect(() => { + validateParams(actionType, { + eventAction: 'resolve', + }); + }).toThrowError( + `error validating action params: DedupKey is required when eventAction is "resolve"` + ); + }); }); describe('execute()', () => { diff --git a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts index 84960c023bd2f..a1a0bb0461315 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts @@ -51,6 +51,10 @@ export type ActionParamsType = TypeOf; const EVENT_ACTION_TRIGGER = 'trigger'; const EVENT_ACTION_RESOLVE = 'resolve'; const EVENT_ACTION_ACKNOWLEDGE = 'acknowledge'; +const EVENT_ACTIONS_WITH_REQUIRED_DEDUPKEY = new Set([ + EVENT_ACTION_RESOLVE, + EVENT_ACTION_ACKNOWLEDGE, +]); const EventActionSchema = schema.oneOf([ schema.literal(EVENT_ACTION_TRIGGER), @@ -81,7 +85,7 @@ const ParamsSchema = schema.object( ); function validateParams(paramsObject: unknown): string | void { - const { timestamp } = paramsObject as ActionParamsType; + const { timestamp, eventAction, dedupKey } = paramsObject as ActionParamsType; if (timestamp != null) { try { const date = Date.parse(timestamp); @@ -103,6 +107,14 @@ function validateParams(paramsObject: unknown): string | void { }); } } + if (EVENT_ACTIONS_WITH_REQUIRED_DEDUPKEY.has(eventAction) && !dedupKey) { + return i18n.translate('xpack.actions.builtin.pagerduty.missingDedupkeyErrorMessage', { + defaultMessage: `DedupKey is required when eventAction is "{eventAction}"`, + values: { + eventAction, + }, + }); + } } // action type definition diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts index 1c1261ae3fa08..10e1a9ae421b7 100644 --- a/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts +++ b/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts @@ -85,6 +85,120 @@ describe('7.10.0', () => { }, }); }); + + test('migrates PagerDuty actions to set a default dedupkey of the AlertId', () => { + const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0']; + const alert = getMockData({ + actions: [ + { + actionTypeId: '.pagerduty', + group: 'default', + params: { + summary: 'fired {{alertInstanceId}}', + eventAction: 'resolve', + component: '', + }, + id: 'b62ea790-5366-4abc-a7df-33db1db78410', + }, + ], + }); + expect(migration710(alert, { log })).toMatchObject({ + ...alert, + attributes: { + ...alert.attributes, + actions: [ + { + actionTypeId: '.pagerduty', + group: 'default', + params: { + summary: 'fired {{alertInstanceId}}', + eventAction: 'resolve', + dedupKey: '{{alertId}}', + component: '', + }, + id: 'b62ea790-5366-4abc-a7df-33db1db78410', + }, + ], + }, + }); + }); + + test('skips PagerDuty actions with a specified dedupkey', () => { + const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0']; + const alert = getMockData({ + actions: [ + { + actionTypeId: '.pagerduty', + group: 'default', + params: { + summary: 'fired {{alertInstanceId}}', + eventAction: 'trigger', + dedupKey: '{{alertInstanceId}}', + component: '', + }, + id: 'b62ea790-5366-4abc-a7df-33db1db78410', + }, + ], + }); + expect(migration710(alert, { log })).toMatchObject({ + ...alert, + attributes: { + ...alert.attributes, + actions: [ + { + actionTypeId: '.pagerduty', + group: 'default', + params: { + summary: 'fired {{alertInstanceId}}', + eventAction: 'trigger', + dedupKey: '{{alertInstanceId}}', + component: '', + }, + id: 'b62ea790-5366-4abc-a7df-33db1db78410', + }, + ], + }, + }); + }); + + test('skips PagerDuty actions with an eventAction of "trigger"', () => { + const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0']; + const alert = getMockData({ + actions: [ + { + actionTypeId: '.pagerduty', + group: 'default', + params: { + summary: 'fired {{alertInstanceId}}', + eventAction: 'trigger', + component: '', + }, + id: 'b62ea790-5366-4abc-a7df-33db1db78410', + }, + ], + }); + expect(migration710(alert, { log })).toEqual({ + ...alert, + attributes: { + ...alert.attributes, + meta: { + versionApiKeyLastmodified: 'pre-7.10.0', + }, + actions: [ + { + actionTypeId: '.pagerduty', + group: 'default', + params: { + summary: 'fired {{alertInstanceId}}', + eventAction: 'trigger', + component: '', + }, + id: 'b62ea790-5366-4abc-a7df-33db1db78410', + }, + ], + }, + }); + }); }); describe('7.10.0 migrates with failure', () => { diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.ts index c88f4d786c212..537c21e85c0bd 100644 --- a/x-pack/plugins/alerts/server/saved_objects/migrations.ts +++ b/x-pack/plugins/alerts/server/saved_objects/migrations.ts @@ -18,10 +18,20 @@ import { export const LEGACY_LAST_MODIFIED_VERSION = 'pre-7.10.0'; +type AlertMigration = ( + doc: SavedObjectUnsanitizedDoc +) => SavedObjectUnsanitizedDoc; + export function getMigrations( encryptedSavedObjects: EncryptedSavedObjectsPluginSetup ): SavedObjectMigrationMap { - const migrationWhenRBACWasIntroduced = markAsLegacyAndChangeConsumer(encryptedSavedObjects); + const migrationWhenRBACWasIntroduced = encryptedSavedObjects.createMigration( + function shouldBeMigrated(doc): doc is SavedObjectUnsanitizedDoc { + // migrate all documents in 7.10 in order to add the "meta" RBAC field + return true; + }, + pipeMigrations(markAsLegacyAndChangeConsumer, setAlertIdAsDefaultDedupkeyOnPagerDutyActions) + ); return { '7.10.0': executeMigrationWithErrorHandling(migrationWhenRBACWasIntroduced, '7.10.0'), @@ -52,29 +62,55 @@ const consumersToChange: Map = new Map( [SIEM_APP_ID]: SIEM_SERVER_APP_ID, }) ); + function markAsLegacyAndChangeConsumer( - encryptedSavedObjects: EncryptedSavedObjectsPluginSetup -): SavedObjectMigrationFn { - return encryptedSavedObjects.createMigration( - function shouldBeMigrated(doc): doc is SavedObjectUnsanitizedDoc { - // migrate all documents in 7.10 in order to add the "meta" RBAC field - return true; + doc: SavedObjectUnsanitizedDoc +): SavedObjectUnsanitizedDoc { + const { + attributes: { consumer }, + } = doc; + return { + ...doc, + attributes: { + ...doc.attributes, + consumer: consumersToChange.get(consumer) ?? consumer, + // mark any alert predating 7.10 as a legacy alert + meta: { + versionApiKeyLastmodified: LEGACY_LAST_MODIFIED_VERSION, + }, }, - (doc: SavedObjectUnsanitizedDoc): SavedObjectUnsanitizedDoc => { - const { - attributes: { consumer }, - } = doc; - return { - ...doc, - attributes: { - ...doc.attributes, - consumer: consumersToChange.get(consumer) ?? consumer, - // mark any alert predating 7.10 as a legacy alert - meta: { - versionApiKeyLastmodified: LEGACY_LAST_MODIFIED_VERSION, - }, - }, - }; - } - ); + }; +} + +function setAlertIdAsDefaultDedupkeyOnPagerDutyActions( + doc: SavedObjectUnsanitizedDoc +): SavedObjectUnsanitizedDoc { + const { attributes } = doc; + return { + ...doc, + attributes: { + ...attributes, + ...(attributes.actions + ? { + actions: attributes.actions.map((action) => { + if (action.actionTypeId !== '.pagerduty' || action.params.eventAction === 'trigger') { + return action; + } + return { + ...action, + params: { + ...action.params, + dedupKey: action.params.dedupKey ?? '{{alertId}}', + }, + }; + }), + } + : {}), + }, + }; +} + +function pipeMigrations(...migrations: AlertMigration[]): AlertMigration { + return (doc: SavedObjectUnsanitizedDoc) => + migrations.reduce((migratedDoc, nextMigration) => nextMigration(migratedDoc), doc); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.tsx index 90d8da346c71d..03bfbb38da6f2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.tsx @@ -50,8 +50,22 @@ export function getActionType(): ActionTypeModel { const errors = { summary: new Array(), timestamp: new Array(), + dedupKey: new Array(), }; validationResult.errors = errors; + if ( + !actionParams.dedupKey?.length && + (actionParams.eventAction === 'resolve' || actionParams.eventAction === 'acknowledge') + ) { + errors.dedupKey.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.requiredDedupKeyText', + { + defaultMessage: 'DedupKey is required when resolving or acknowledging an incident.', + } + ) + ); + } if (!actionParams.summary?.length) { errors.summary.push( i18n.translate( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.tsx index c8ad5f5b7080e..39800865ed761 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.tsx @@ -94,6 +94,9 @@ const PagerDutyParamsFields: React.FunctionComponent @@ -144,12 +147,23 @@ const PagerDutyParamsFields: React.FunctionComponent 0} + label={ + isDedupeKeyRequired + ? i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.dedupKeyTextRequiredFieldLabel', + { + defaultMessage: 'DedupKey', + } + ) + : i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.dedupKeyTextFieldLabel', + { + defaultMessage: 'DedupKey (optional)', + } + ) + } > { + const response = await supertest.get( + `${getUrlPrefix(``)}/api/alerts/alert/b6087f72-994f-46fb-8120-c6e5c50d0f8f` + ); + + expect(response.status).to.eql(200); + + expect(response.body.actions).to.eql([ + { + actionTypeId: '.pagerduty', + id: 'a6a8ab7a-35cf-445e-ade3-215a029c2ee3', + group: 'default', + params: { + component: '', + dedupKey: '{{alertId}}', + eventAction: 'trigger', + summary: 'fired {{alertInstanceId}}', + }, + }, + { + actionTypeId: '.pagerduty', + id: 'a6a8ab7a-35cf-445e-ade3-215a029c2ee3', + group: 'default', + params: { + component: '', + dedupKey: '{{alertId}}', + eventAction: 'resolve', + summary: 'fired {{alertInstanceId}}', + }, + }, + { + actionTypeId: '.pagerduty', + id: 'a6a8ab7a-35cf-445e-ade3-215a029c2ee3', + group: 'default', + params: { + component: '', + dedupKey: '{{alertInstanceId}}', + eventAction: 'resolve', + summary: 'fired {{alertInstanceId}}', + }, + }, + ]); + }); }); } diff --git a/x-pack/test/functional/es_archives/alerts/data.json b/x-pack/test/functional/es_archives/alerts/data.json index cc246b0fe44da..4e879116d8cda 100644 --- a/x-pack/test/functional/es_archives/alerts/data.json +++ b/x-pack/test/functional/es_archives/alerts/data.json @@ -80,4 +80,115 @@ "updated_at": "2020-06-17T15:35:39.839Z" } } +} + +{ + "type": "doc", + "value": { + "id": "action:a6a8ab7a-35cf-445e-ade3-215a029c2ee3", + "index": ".kibana_1", + "source": { + "action": { + "actionTypeId": ".pagerduty", + "config": { + "apiUrl": "http://elastic:changeme@localhost:5620/api/_actions-FTS-external-service-simulators/pagerduty" + }, + "name": "A pagerduty action", + "secrets": "kvjaTWYKGmCqptyv4giaN+nQGgsZrKXmlULcbAP8KK3JmR8Ei9ADqh5mB+uVC+x+Q7/vTQ5SKZCj3dHv3pmNzZ5WGyZYQFBaaa63Mkp3kIcnpE1OdSAv+3Z/Y+XihHAM19zUm3JRpojnIpYegoS5/vMx1sOzcf/+miYUuZw2lgo0lNE=" + }, + "references": [ + ], + "type": "action", + "updated_at": "2020-09-22T15:16:06.924Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "alert:b6087f72-994f-46fb-8120-c6e5c50d0f8f", + "index": ".kibana_1", + "source": { + "alert": { + "actions": [ + { + "actionRef": "action_0", + "actionTypeId": ".pagerduty", + "group": "default", + "params": { + "component": "", + "eventAction": "trigger", + "summary": "fired {{alertInstanceId}}" + } + }, + { + "actionRef": "action_1", + "actionTypeId": ".pagerduty", + "group": "default", + "params": { + "component": "", + "eventAction": "resolve", + "summary": "fired {{alertInstanceId}}" + } + }, + { + "actionRef": "action_2", + "actionTypeId": ".pagerduty", + "group": "default", + "params": { + "component": "", + "dedupKey": "{{alertInstanceId}}", + "eventAction": "resolve", + "summary": "fired {{alertInstanceId}}" + } + } + ], + "alertTypeId": "test.noop", + "apiKey": null, + "apiKeyOwner": null, + "consumer": "alertsFixture", + "createdAt": "2020-09-22T15:16:07.451Z", + "createdBy": null, + "enabled": true, + "muteAll": false, + "mutedInstanceIds": [ + ], + "name": "abc", + "params": { + }, + "schedule": { + "interval": "1m" + }, + "scheduledTaskId": "8a7c6ff0-fce6-11ea-a888-9337d77a2c25", + "tags": [ + "foo" + ], + "throttle": "1m", + "updatedBy": null + }, + "migrationVersion": { + "alert": "7.9.0" + }, + "references": [ + { + "id": "a6a8ab7a-35cf-445e-ade3-215a029c2ee3", + "name": "action_0", + "type": "action" + }, + { + "id": "a6a8ab7a-35cf-445e-ade3-215a029c2ee3", + "name": "action_1", + "type": "action" + }, + { + "id": "a6a8ab7a-35cf-445e-ade3-215a029c2ee3", + "name": "action_2", + "type": "action" + } + ], + "type": "alert", + "updated_at": "2020-09-22T15:16:08.456Z" + } + } } \ No newline at end of file