[Alerts] Fix broken alert's actions when upgrading from 7.10 to 7.11 (#93611)

Co-authored-by: Steph Milovic <stephanie.milovic@elastic.co>
This commit is contained in:
Christos Nasikas 2021-03-06 02:03:45 +02:00 committed by GitHub
parent 972bf0adc1
commit d0f356dde3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 739 additions and 6 deletions

View file

@ -23,14 +23,15 @@ export function getMigrations(
): SavedObjectMigrationMap {
const migrationActionsTen = encryptedSavedObjects.createMigration<RawAction, RawAction>(
(doc): doc is SavedObjectUnsanitizedDoc<RawAction> =>
!!doc.attributes.config?.casesConfiguration || doc.attributes.actionTypeId === '.email',
doc.attributes.config?.hasOwnProperty('casesConfiguration') ||
doc.attributes.actionTypeId === '.email',
pipeMigrations(renameCasesConfigurationObject, addHasAuthConfigurationObject)
);
const migrationActionsEleven = encryptedSavedObjects.createMigration<RawAction, RawAction>(
(doc): doc is SavedObjectUnsanitizedDoc<RawAction> =>
!!doc.attributes.config?.isCaseOwned ||
!!doc.attributes.config?.incidentConfiguration ||
doc.attributes.config?.hasOwnProperty('isCaseOwned') ||
doc.attributes.config?.hasOwnProperty('incidentConfiguration') ||
doc.attributes.actionTypeId === '.webhook',
pipeMigrations(removeCasesFieldMappings, addHasAuthConfigurationObject)
);

View file

@ -6,7 +6,7 @@
*/
import uuid from 'uuid';
import { getMigrations } from './migrations';
import { getMigrations, isAnyActionSupportIncidents } from './migrations';
import { RawAlert } from '../types';
import { SavedObjectUnsanitizedDoc } from 'kibana/server';
import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/mocks';
@ -324,6 +324,255 @@ describe('7.11.0', () => {
});
});
describe('7.11.2', () => {
beforeEach(() => {
jest.resetAllMocks();
encryptedSavedObjectsSetup.createMigration.mockImplementation(
(shouldMigrateWhenPredicate, migration) => migration
);
});
test('transforms connectors that support incident correctly', () => {
const migration7112 = getMigrations(encryptedSavedObjectsSetup)['7.11.2'];
const alert = getMockData({
actions: [
{
actionTypeId: '.jira',
group: 'threshold met',
params: {
subAction: 'pushToService',
subActionParams: {
title: 'Jira summary',
issueType: '10001',
comments: [
{
commentId: '1',
comment: 'jira comment',
},
],
description: 'Jira description',
savedObjectId: '{{alertId}}',
priority: 'Highest',
parent: 'CASES-78',
labels: ['test'],
},
},
id: 'b1abe42d-ae1a-4a6a-b5ec-482ce0492c14',
},
{
actionTypeId: '.resilient',
group: 'threshold met',
params: {
subAction: 'pushToService',
subActionParams: {
savedObjectId: '{{alertId}}',
incidentTypes: ['17', '21'],
severityCode: '5',
title: 'IBM name',
description: 'IBM description',
comments: [
{
commentId: 'alert-comment',
comment: 'IBM comment',
},
],
},
},
id: '75d63268-9a83-460f-9026-0028f4f7dac4',
},
{
actionTypeId: '.servicenow',
group: 'threshold met',
params: {
subAction: 'pushToService',
subActionParams: {
severity: '2',
impact: '2',
urgency: '2',
savedObjectId: '{{alertId}}',
title: 'SN short desc',
description: 'SN desc',
comment: 'sn comment',
},
},
id: '1266562a-4e1f-4305-99ca-1b44c469b26e',
},
],
});
expect(migration7112(alert, migrationContext)).toEqual({
...alert,
attributes: {
...alert.attributes,
actions: [
{
actionTypeId: '.jira',
group: 'threshold met',
params: {
subAction: 'pushToService',
subActionParams: {
incident: {
summary: 'Jira summary',
description: 'Jira description',
issueType: '10001',
priority: 'Highest',
parent: 'CASES-78',
labels: ['test'],
},
comments: [
{
commentId: '1',
comment: 'jira comment',
},
],
},
},
id: 'b1abe42d-ae1a-4a6a-b5ec-482ce0492c14',
},
{
actionTypeId: '.resilient',
group: 'threshold met',
params: {
subAction: 'pushToService',
subActionParams: {
incident: {
name: 'IBM name',
description: 'IBM description',
incidentTypes: ['17', '21'],
severityCode: '5',
},
comments: [
{
commentId: 'alert-comment',
comment: 'IBM comment',
},
],
},
},
id: '75d63268-9a83-460f-9026-0028f4f7dac4',
},
{
actionTypeId: '.servicenow',
group: 'threshold met',
params: {
subAction: 'pushToService',
subActionParams: {
incident: {
short_description: 'SN short desc',
description: 'SN desc',
severity: '2',
impact: '2',
urgency: '2',
},
comments: [{ commentId: '1', comment: 'sn comment' }],
},
},
id: '1266562a-4e1f-4305-99ca-1b44c469b26e',
},
],
},
});
});
test('it transforms only subAction=pushToService', () => {
const migration7112 = getMigrations(encryptedSavedObjectsSetup)['7.11.2'];
const alert = getMockData({
actions: [
{
actionTypeId: '.jira',
group: 'threshold met',
params: {
subAction: 'issues',
subActionParams: { issues: 'Task' },
},
id: '1266562a-4e1f-4305-99ca-1b44c469b26e',
},
],
});
expect(migration7112(alert, migrationContext)).toEqual(alert);
});
test('it does not transforms other connectors', () => {
const migration7112 = getMigrations(encryptedSavedObjectsSetup)['7.11.2'];
const alert = getMockData({
actions: [
{
actionTypeId: '.server-log',
group: 'threshold met',
params: {
level: 'info',
message: 'log message',
},
id: '99257478-e591-4560-b264-441bdd4fe1d9',
},
{
actionTypeId: '.servicenow',
group: 'threshold met',
params: {
subAction: 'pushToService',
subActionParams: {
severity: '2',
impact: '2',
urgency: '2',
savedObjectId: '{{alertId}}',
title: 'SN short desc',
description: 'SN desc',
comment: 'sn comment',
},
},
id: '1266562a-4e1f-4305-99ca-1b44c469b26e',
},
],
});
expect(migration7112(alert, migrationContext)).toEqual({
...alert,
attributes: {
...alert.attributes,
actions: [
alert.attributes.actions![0],
{
actionTypeId: '.servicenow',
group: 'threshold met',
params: {
subAction: 'pushToService',
subActionParams: {
incident: {
short_description: 'SN short desc',
description: 'SN desc',
severity: '2',
impact: '2',
urgency: '2',
},
comments: [{ commentId: '1', comment: 'sn comment' }],
},
},
id: '1266562a-4e1f-4305-99ca-1b44c469b26e',
},
],
},
});
});
test.each(['.jira', '.servicenow', '.resilient'])(
'isAnyActionSupportIncidents should return true when %s is in actions',
(actionTypeId) => {
const doc = {
attributes: { actions: [{ actionTypeId }, { actionTypeId: '.server-log' }] },
} as SavedObjectUnsanitizedDoc<RawAlert>;
expect(isAnyActionSupportIncidents(doc)).toBe(true);
}
);
test('isAnyActionSupportIncidents should return false when there is no connector that supports incidents', () => {
const doc = {
attributes: { actions: [{ actionTypeId: '.server-log' }] },
} as SavedObjectUnsanitizedDoc<RawAlert>;
expect(isAnyActionSupportIncidents(doc)).toBe(false);
});
});
function getUpdatedAt(): string {
const updatedAt = new Date();
updatedAt.setHours(updatedAt.getHours() + 2);

View file

@ -11,7 +11,7 @@ import {
SavedObjectMigrationFn,
SavedObjectMigrationContext,
} from '../../../../../src/core/server';
import { RawAlert } from '../types';
import { RawAlert, RawAlertAction } from '../types';
import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server';
const SIEM_APP_ID = 'securitySolution';
@ -22,6 +22,13 @@ type AlertMigration = (
doc: SavedObjectUnsanitizedDoc<RawAlert>
) => SavedObjectUnsanitizedDoc<RawAlert>;
const SUPPORT_INCIDENTS_ACTION_TYPES = ['.servicenow', '.jira', '.resilient'];
export const isAnyActionSupportIncidents = (doc: SavedObjectUnsanitizedDoc<RawAlert>): boolean =>
doc.attributes.actions.some((action) =>
SUPPORT_INCIDENTS_ACTION_TYPES.includes(action.actionTypeId)
);
export function getMigrations(
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup
): SavedObjectMigrationMap {
@ -46,9 +53,15 @@ export function getMigrations(
pipeMigrations(setAlertUpdatedAtDate, setNotifyWhen)
);
const migrationActions7112 = encryptedSavedObjects.createMigration<RawAlert, RawAlert>(
(doc): doc is SavedObjectUnsanitizedDoc<RawAlert> => isAnyActionSupportIncidents(doc),
pipeMigrations(restructureConnectorsThatSupportIncident)
);
return {
'7.10.0': executeMigrationWithErrorHandling(migrationWhenRBACWasIntroduced, '7.10.0'),
'7.11.0': executeMigrationWithErrorHandling(migrationAlertUpdatedAtAndNotifyWhen, '7.11.0'),
'7.11.2': executeMigrationWithErrorHandling(migrationActions7112, '7.11.2'),
};
}
@ -167,6 +180,124 @@ function initializeExecutionStatus(
};
}
function restructureConnectorsThatSupportIncident(
doc: SavedObjectUnsanitizedDoc<RawAlert>
): SavedObjectUnsanitizedDoc<RawAlert> {
const { actions } = doc.attributes;
const newActions = actions.reduce((acc, action) => {
if (action.params.subAction !== 'pushToService') {
return [...acc, action];
}
if (action.actionTypeId === '.servicenow') {
const { title, comments, comment, description, severity, urgency, impact } = action.params
.subActionParams as {
title: string;
description?: string;
severity?: string;
urgency?: string;
impact?: string;
comment?: string;
comments?: Array<{ commentId: string; comment: string }>;
};
return [
...acc,
{
...action,
params: {
subAction: 'pushToService',
subActionParams: {
incident: {
short_description: title,
description,
severity,
urgency,
impact,
},
comments: [
...(comments ?? []),
...(comment != null ? [{ commentId: '1', comment }] : []),
],
},
},
},
] as RawAlertAction[];
}
if (action.actionTypeId === '.jira') {
const { title, comments, description, issueType, priority, labels, parent } = action.params
.subActionParams as {
title: string;
description: string;
issueType: string;
priority?: string;
labels?: string[];
parent?: string;
comments?: unknown[];
};
return [
...acc,
{
...action,
params: {
subAction: 'pushToService',
subActionParams: {
incident: {
summary: title,
description,
issueType,
priority,
labels,
parent,
},
comments,
},
},
},
] as RawAlertAction[];
}
if (action.actionTypeId === '.resilient') {
const { title, comments, description, incidentTypes, severityCode } = action.params
.subActionParams as {
title: string;
description: string;
incidentTypes?: number[];
severityCode?: number;
comments?: unknown[];
};
return [
...acc,
{
...action,
params: {
subAction: 'pushToService',
subActionParams: {
incident: {
name: title,
description,
incidentTypes,
severityCode,
},
comments,
},
},
},
] as RawAlertAction[];
}
return acc;
}, [] as RawAlertAction[]);
return {
...doc,
attributes: {
...doc.attributes,
actions: newActions,
},
};
}
function pipeMigrations(...migrations: AlertMigration[]): AlertMigration {
return (doc: SavedObjectUnsanitizedDoc<RawAlert>) =>
migrations.reduce((migratedDoc, nextMigration) => nextMigration(migratedDoc), doc);

View file

@ -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 {
SavedObjectUnsanitizedDoc,
SavedObjectSanitizedDoc,
} from '../../../../../../../src/core/server';
import { IRuleActionsAttributesSavedObjectAttributes, RuleAlertAction } from './types';
export const ruleActionsSavedObjectMigration = {
'7.11.2': (
doc: SavedObjectUnsanitizedDoc<IRuleActionsAttributesSavedObjectAttributes>
): SavedObjectSanitizedDoc<IRuleActionsAttributesSavedObjectAttributes> => {
const { actions } = doc.attributes;
const newActions = actions.reduce((acc, action) => {
if (action.params.subAction !== 'pushToService') {
return [...acc, action];
}
if (action.action_type_id === '.servicenow') {
const { title, comments, comment, description, severity, urgency, impact } = action.params
.subActionParams as {
title: string;
description?: string;
severity?: string;
urgency?: string;
impact?: string;
comment?: string;
comments?: Array<{ commentId: string; comment: string }>;
};
return [
...acc,
{
...action,
params: {
subAction: 'pushToService',
subActionParams: {
incident: {
short_description: title,
description,
severity,
urgency,
impact,
},
comments: [
...(comments ?? []),
...(comment != null ? [{ commentId: '1', comment }] : []),
],
},
},
},
] as RuleAlertAction[];
}
if (action.action_type_id === '.jira') {
const { title, comments, description, issueType, priority, labels, parent } = action.params
.subActionParams as {
title: string;
description: string;
issueType: string;
priority?: string;
labels?: string[];
parent?: string;
comments?: unknown[];
};
return [
...acc,
{
...action,
params: {
subAction: 'pushToService',
subActionParams: {
incident: {
summary: title,
description,
issueType,
priority,
labels,
parent,
},
comments,
},
},
},
] as RuleAlertAction[];
}
if (action.action_type_id === '.resilient') {
const { title, comments, description, incidentTypes, severityCode } = action.params
.subActionParams as {
title: string;
description: string;
incidentTypes?: number[];
severityCode?: number;
comments?: unknown[];
};
return [
...acc,
{
...action,
params: {
subAction: 'pushToService',
subActionParams: {
incident: {
name: title,
description,
incidentTypes,
severityCode,
},
comments,
},
},
},
] as RuleAlertAction[];
}
return acc;
}, [] as RuleAlertAction[]);
return {
...doc,
attributes: {
...doc.attributes,
actions: newActions,
},
references: doc.references || [],
};
},
};

View file

@ -6,6 +6,7 @@
*/
import { SavedObjectsType } from '../../../../../../../src/core/server';
import { ruleActionsSavedObjectMigration } from './migrations';
export const ruleActionsSavedObjectType = 'siem-detection-engine-rule-actions';
@ -45,4 +46,5 @@ export const type: SavedObjectsType = {
hidden: false,
namespaceType: 'single',
mappings: ruleActionsSavedObjectMappings,
migrations: ruleActionsSavedObjectMigration,
};

View file

@ -9,6 +9,8 @@ import { get } from 'lodash/fp';
import { SavedObject, SavedObjectAttributes, SavedObjectsFindResponse } from 'kibana/server';
import { RuleAlertAction } from '../../../../common/detection_engine/types';
export { RuleAlertAction };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export interface IRuleActionsAttributes extends Record<string, any> {
ruleAlertId: string;

View file

@ -101,5 +101,79 @@ export default function createGetTests({ getService }: FtrProviderContext) {
expect(response.status).to.eql(200);
expect(response.body.notifyWhen).to.eql('onActiveAlert');
});
it('7.11.2 migrates alerts with case actions, case fields are nested in an incident object', async () => {
const response = await supertest.get(
`${getUrlPrefix(``)}/api/alerts/alert/99f3e6d7-b7bb-477d-ac28-92ee22726969`
);
expect(response.status).to.eql(200);
expect(response.body.actions).to.eql([
{
id: '66a8ab7a-35cf-445e-ade3-215a029c6969',
actionTypeId: '.servicenow',
group: 'threshold met',
params: {
subAction: 'pushToService',
subActionParams: {
incident: {
severity: '2',
impact: '2',
urgency: '2',
short_description: 'SN short desc',
description: 'SN desc',
},
comments: [{ commentId: '1', comment: 'sn comment' }],
},
},
},
{
id: '66a8ab7a-35cf-445e-ade3-215a029c6969',
actionTypeId: '.jira',
group: 'threshold met',
params: {
subAction: 'pushToService',
subActionParams: {
incident: {
summary: 'Jira summary',
issueType: '10001',
description: 'Jira description',
priority: 'Highest',
parent: 'CASES-78',
labels: ['test'],
},
comments: [
{
commentId: '1',
comment: 'jira comment',
},
],
},
},
},
{
id: '66a8ab7a-35cf-445e-ade3-215a029c6969',
actionTypeId: '.resilient',
group: 'threshold met',
params: {
subAction: 'pushToService',
subActionParams: {
incident: {
incidentTypes: ['17', '21'],
severityCode: '5',
name: 'IBM name',
description: 'IBM description',
},
comments: [
{
commentId: 'alert-comment',
comment: 'IBM comment',
},
],
},
},
},
]);
});
});
}

View file

@ -40,6 +40,147 @@
}
}
{
"type": "doc",
"value": {
"id": "alert:99f3e6d7-b7bb-477d-ac28-92ee22726969",
"index": ".kibana_1",
"source": {
"alert": {
"actions": [{
"actionRef": "action_0",
"actionTypeId": ".servicenow",
"group": "threshold met",
"params": {
"subAction": "pushToService",
"subActionParams": {
"severity":"2",
"impact":"2",
"urgency":"2",
"savedObjectId":"{{alertId}}",
"title":"SN short desc",
"description":"SN desc",
"comment":"sn comment"
}
}
},
{
"actionRef": "action_1",
"actionTypeId": ".jira",
"group": "threshold met",
"params": {
"subAction": "pushToService",
"subActionParams": {
"title":"Jira summary",
"issueType":"10001",
"comments":[
{
"commentId":"1",
"comment":"jira comment"
}
],
"description":"Jira description",
"savedObjectId":"{{alertId}}",
"priority":"Highest",
"parent":"CASES-78",
"labels":[
"test"
]
}
}
},
{
"actionRef": "action_2",
"actionTypeId": ".resilient",
"group": "threshold met",
"params": {
"subAction": "pushToService",
"subActionParams": {
"savedObjectId":"{{alertId}}",
"incidentTypes":[
"17",
"21"
],
"severityCode":"5",
"title":"IBM name",
"description":"IBM description",
"comments":[
{
"commentId":"alert-comment",
"comment":"IBM comment"
}
]
}
}
}],
"alertTypeId": "test.noop",
"apiKey": null,
"apiKeyOwner": null,
"consumer": "alertsFixture",
"createdAt": "2020-09-22T15:16:07.451Z",
"createdBy": null,
"enabled": true,
"muteAll": false,
"mutedInstanceIds": [
],
"name": "rbg",
"params": {
},
"schedule": {
"interval": "1m"
},
"scheduledTaskId": "329798f0-b0b0-11ea-9510-fdf248d5f2a4",
"tags": [
],
"throttle": null,
"updatedBy": "elastic"
},
"migrationVersion": {
"alert": "7.11.0"
},
"references": [{
"id": "66a8ab7a-35cf-445e-ade3-215a029c6969",
"name": "action_0",
"type": "action"
},
{
"id": "66a8ab7a-35cf-445e-ade3-215a029c6969",
"name": "action_1",
"type": "action"
},
{
"id": "66a8ab7a-35cf-445e-ade3-215a029c6969",
"name": "action_2",
"type": "action"
}],
"type": "alert",
"updated_at": "2020-06-17T15:35:39.839Z"
}
}
}
{
"type": "doc",
"value": {
"id": "action:66a8ab7a-35cf-445e-ade3-215a029c6969",
"index": ".kibana_1",
"source": {
"action": {
"actionTypeId": ".servicenow",
"config": {
"apiUrl": "http://elastic:changeme@localhost:5620/api/_actions-FTS-external-service-simulators/servicenow"
},
"name": "A servicenow 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": {
@ -191,4 +332,4 @@
"updated_at": "2020-09-22T15:16:08.456Z"
}
}
}
}