Skip to content

Commit

Permalink
[7.x] [Security Solution][Case] Sync cases with alerts (#84731) (#85845)
Browse files Browse the repository at this point in the history
  • Loading branch information
cnasikas authored Dec 15, 2020
1 parent bab33f6 commit b404c47
Show file tree
Hide file tree
Showing 59 changed files with 996 additions and 108 deletions.
8 changes: 7 additions & 1 deletion x-pack/plugins/case/common/api/cases/case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,17 @@ const CaseStatusRt = rt.union([

export const caseStatuses = Object.values(CaseStatuses);

const SettingsRt = rt.type({
syncAlerts: rt.boolean,
});

const CaseBasicRt = rt.type({
connector: CaseConnectorRt,
description: rt.string,
status: CaseStatusRt,
tags: rt.array(rt.string),
title: rt.string,
connector: CaseConnectorRt,
settings: SettingsRt,
});

const CaseExternalServiceBasicRt = rt.type({
Expand Down Expand Up @@ -74,6 +79,7 @@ export const CasePostRequestRt = rt.type({
tags: rt.array(rt.string),
title: rt.string,
connector: CaseConnectorRt,
settings: SettingsRt,
});

export const CaseExternalServiceRequestRt = CaseExternalServiceBasicRt;
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/case/common/api/cases/user_actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const UserActionFieldRt = rt.array(
rt.literal('tags'),
rt.literal('title'),
rt.literal('status'),
rt.literal('settings'),
])
);
const UserActionRt = rt.union([
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/case/kibana.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"configPath": ["xpack", "case"],
"id": "case",
"kibanaVersion": "kibana",
"requiredPlugins": ["actions"],
"requiredPlugins": ["actions", "securitySolution"],
"optionalPlugins": [
"spaces",
"security"
Expand Down
25 changes: 25 additions & 0 deletions x-pack/plugins/case/server/client/alerts/update_status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import Boom from '@hapi/boom';
import { CaseClientUpdateAlertsStatus, CaseClientFactoryArguments } from '../types';

export const updateAlertsStatus = ({
alertsService,
request,
context,
}: CaseClientFactoryArguments) => async ({
ids,
status,
}: CaseClientUpdateAlertsStatus): Promise<void> => {
const securitySolutionClient = context?.securitySolution?.getAppClient();
if (securitySolutionClient == null) {
throw Boom.notFound('securitySolutionClient client have not been found');
}

const index = securitySolutionClient.getSignalsIndex();
await alertsService.updateAlertsStatus({ ids, status, index, request });
};
28 changes: 26 additions & 2 deletions x-pack/plugins/case/server/client/cases/create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ describe('create', () => {
type: ConnectorTypes.jira,
fields: { issueType: 'Task', priority: 'High', parent: null },
},
settings: {
syncAlerts: true,
},
} as CasePostRequest;

const savedObjectsClient = createMockSavedObjectsRepository({
Expand Down Expand Up @@ -65,6 +68,9 @@ describe('create', () => {
updated_at: null,
updated_by: null,
version: 'WzksMV0=',
settings: {
syncAlerts: true,
},
});

expect(
Expand All @@ -79,9 +85,9 @@ describe('create', () => {
full_name: 'Awesome D00d',
username: 'awesome',
},
action_field: ['description', 'status', 'tags', 'title', 'connector'],
action_field: ['description', 'status', 'tags', 'title', 'connector', 'settings'],
new_value:
'{"description":"This is a brand new case of a bad meanie defacing data","title":"Super Bad Security Issue","tags":["defacement"],"connector":{"id":"123","name":"Jira","type":".jira","fields":{"issueType":"Task","priority":"High","parent":null}}}',
'{"description":"This is a brand new case of a bad meanie defacing data","title":"Super Bad Security Issue","tags":["defacement"],"connector":{"id":"123","name":"Jira","type":".jira","fields":{"issueType":"Task","priority":"High","parent":null}},"settings":{"syncAlerts":true}}',
old_value: null,
},
references: [
Expand All @@ -106,6 +112,9 @@ describe('create', () => {
type: ConnectorTypes.none,
fields: null,
},
settings: {
syncAlerts: true,
},
};

const savedObjectsClient = createMockSavedObjectsRepository({
Expand All @@ -131,6 +140,9 @@ describe('create', () => {
updated_at: null,
updated_by: null,
version: 'WzksMV0=',
settings: {
syncAlerts: true,
},
});
});

Expand All @@ -145,6 +157,9 @@ describe('create', () => {
type: ConnectorTypes.none,
fields: null,
},
settings: {
syncAlerts: true,
},
};

const savedObjectsClient = createMockSavedObjectsRepository({
Expand Down Expand Up @@ -174,6 +189,9 @@ describe('create', () => {
updated_at: null,
updated_by: null,
version: 'WzksMV0=',
settings: {
syncAlerts: true,
},
});
});
});
Expand Down Expand Up @@ -323,6 +341,9 @@ describe('create', () => {
type: ConnectorTypes.none,
fields: null,
},
settings: {
syncAlerts: true,
},
};

const savedObjectsClient = createMockSavedObjectsRepository({
Expand All @@ -347,6 +368,9 @@ describe('create', () => {
type: ConnectorTypes.none,
fields: null,
},
settings: {
syncAlerts: true,
},
};
const savedObjectsClient = createMockSavedObjectsRepository({
caseSavedObject: mockCases,
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/case/server/client/cases/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export const create = ({
actionAt: createdDate,
actionBy: { username, full_name, email },
caseId: newCase.id,
fields: ['description', 'status', 'tags', 'title', 'connector'],
fields: ['description', 'status', 'tags', 'title', 'connector', 'settings'],
newValue: JSON.stringify(query),
}),
],
Expand Down
38 changes: 31 additions & 7 deletions x-pack/plugins/case/server/client/cases/update.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ describe('update', () => {
});

const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
const res = await caseClient.client.update({ cases: patchCases });
const res = await caseClient.client.update({
caseClient: caseClient.client,
cases: patchCases,
});

expect(res).toEqual([
{
Expand All @@ -63,6 +66,9 @@ describe('update', () => {
updated_at: '2019-11-25T21:54:48.952Z',
updated_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' },
version: 'WzE3LDFd',
settings: {
syncAlerts: true,
},
},
]);

Expand Down Expand Up @@ -115,7 +121,10 @@ describe('update', () => {
});

const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
const res = await caseClient.client.update({ cases: patchCases });
const res = await caseClient.client.update({
caseClient: caseClient.client,
cases: patchCases,
});

expect(res).toEqual([
{
Expand All @@ -140,6 +149,9 @@ describe('update', () => {
updated_at: '2019-11-25T21:54:48.952Z',
updated_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' },
version: 'WzE3LDFd',
settings: {
syncAlerts: true,
},
},
]);
});
Expand All @@ -160,7 +172,10 @@ describe('update', () => {
});

const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
const res = await caseClient.client.update({ cases: patchCases });
const res = await caseClient.client.update({
caseClient: caseClient.client,
cases: patchCases,
});

expect(res).toEqual([
{
Expand All @@ -185,6 +200,9 @@ describe('update', () => {
updated_at: '2019-11-25T21:54:48.952Z',
updated_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' },
version: 'WzE3LDFd',
settings: {
syncAlerts: true,
},
},
]);
});
Expand All @@ -210,7 +228,10 @@ describe('update', () => {
});

const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
const res = await caseClient.client.update({ cases: patchCases });
const res = await caseClient.client.update({
caseClient: caseClient.client,
cases: patchCases,
});

expect(res).toEqual([
{
Expand Down Expand Up @@ -243,6 +264,9 @@ describe('update', () => {
username: 'awesome',
},
version: 'WzE3LDFd',
settings: {
syncAlerts: true,
},
},
]);
});
Expand Down Expand Up @@ -328,7 +352,7 @@ describe('update', () => {
});

const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
caseClient.client.update({ cases: patchCases }).catch((e) => {
caseClient.client.update({ caseClient: caseClient.client, cases: patchCases }).catch((e) => {
expect(e).not.toBeNull();
expect(e.isBoom).toBe(true);
expect(e.output.statusCode).toBe(406);
Expand Down Expand Up @@ -358,7 +382,7 @@ describe('update', () => {
});

const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
caseClient.client.update({ cases: patchCases }).catch((e) => {
caseClient.client.update({ caseClient: caseClient.client, cases: patchCases }).catch((e) => {
expect(e).not.toBeNull();
expect(e.isBoom).toBe(true);
expect(e.output.statusCode).toBe(404);
Expand All @@ -385,7 +409,7 @@ describe('update', () => {
});

const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
caseClient.client.update({ cases: patchCases }).catch((e) => {
caseClient.client.update({ caseClient: caseClient.client, cases: patchCases }).catch((e) => {
expect(e).not.toBeNull();
expect(e.isBoom).toBe(true);
expect(e.output.statusCode).toBe(409);
Expand Down
65 changes: 64 additions & 1 deletion x-pack/plugins/case/server/client/cases/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable';
import { fold } from 'fp-ts/lib/Either';
import { identity } from 'fp-ts/lib/function';

import { SavedObjectsFindResponse } from 'kibana/server';
import { flattenCaseSavedObject } from '../../routes/api/utils';

import {
Expand All @@ -34,7 +35,10 @@ export const update = ({
caseService,
userActionService,
request,
}: CaseClientFactoryArguments) => async ({ cases }: CaseClientUpdate): Promise<CasesResponse> => {
}: CaseClientFactoryArguments) => async ({
caseClient,
cases,
}: CaseClientUpdate): Promise<CasesResponse> => {
const query = pipe(
excess(CasesPatchRequestRt).decode(cases),
fold(throwErrors(Boom.badRequest), identity)
Expand Down Expand Up @@ -126,6 +130,65 @@ export const update = ({
}),
});

// If a status update occurred and the case is synced then we need to update all alerts' status
// attached to the case to the new status.
const casesWithStatusChangedAndSynced = updateFilterCases.filter((caseToUpdate) => {
const currentCase = myCases.saved_objects.find((c) => c.id === caseToUpdate.id);
return (
currentCase != null &&
caseToUpdate.status != null &&
currentCase.attributes.status !== caseToUpdate.status &&
currentCase.attributes.settings.syncAlerts
);
});

// If syncAlerts setting turned on we need to update all alerts' status
// attached to the case to the current status.
const casesWithSyncSettingChangedToOn = updateFilterCases.filter((caseToUpdate) => {
const currentCase = myCases.saved_objects.find((c) => c.id === caseToUpdate.id);
return (
currentCase != null &&
caseToUpdate.settings?.syncAlerts != null &&
currentCase.attributes.settings.syncAlerts !== caseToUpdate.settings.syncAlerts &&
caseToUpdate.settings.syncAlerts
);
});

for (const theCase of [
...casesWithSyncSettingChangedToOn,
...casesWithStatusChangedAndSynced,
]) {
const currentCase = myCases.saved_objects.find((c) => c.id === theCase.id);
const totalComments = await caseService.getAllCaseComments({
client: savedObjectsClient,
caseId: theCase.id,
options: {
fields: [],
filter: 'cases-comments.attributes.type: alert',
page: 1,
perPage: 1,
},
});

const caseComments = (await caseService.getAllCaseComments({
client: savedObjectsClient,
caseId: theCase.id,
options: {
fields: [],
filter: 'cases-comments.attributes.type: alert',
page: 1,
perPage: totalComments.total,
},
// The filter guarantees that the comments will be of type alert
})) as SavedObjectsFindResponse<{ alertId: string }>;

caseClient.updateAlertsStatus({
ids: caseComments.saved_objects.map(({ attributes: { alertId } }) => alertId),
// Either there is a status update or the syncAlerts got turned on.
status: theCase.status ?? currentCase?.attributes.status ?? CaseStatuses.open,
});
}

const returnUpdatedCase = myCases.saved_objects
.filter((myCase) =>
updatedCases.saved_objects.some((updatedCase) => updatedCase.id === myCase.id)
Expand Down
Loading

0 comments on commit b404c47

Please sign in to comment.