Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution][Case] Add in-progress status to case #84321

Merged
merged 17 commits into from
Dec 4, 2020
Merged
19 changes: 15 additions & 4 deletions x-pack/plugins/case/common/api/cases/case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,24 @@ import { CaseConnectorRt, ESCaseConnector, ConnectorPartialFieldsRt } from '../c
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
export { ActionTypeExecutorResult } from '../../../../actions/server/types';

const StatusRt = rt.union([rt.literal('open'), rt.literal('closed')]);
export enum CaseStatuses {
open = 'open',
'in-progress' = 'in-progress',
patrykkopycinski marked this conversation as resolved.
Show resolved Hide resolved
closed = 'closed',
}

const CaseStatusRt = rt.union([
rt.literal(CaseStatuses.open),
rt.literal(CaseStatuses['in-progress']),
rt.literal(CaseStatuses.closed),
]);

export const caseStatuses = Object.values(CaseStatuses);

const CaseBasicRt = rt.type({
connector: CaseConnectorRt,
description: rt.string,
status: StatusRt,
status: CaseStatusRt,
tags: rt.array(rt.string),
title: rt.string,
});
Expand Down Expand Up @@ -68,7 +80,7 @@ export const CaseExternalServiceRequestRt = CaseExternalServiceBasicRt;

export const CasesFindRequestRt = rt.partial({
tags: rt.union([rt.array(rt.string), rt.string]),
status: StatusRt,
status: CaseStatusRt,
reporters: rt.union([rt.array(rt.string), rt.string]),
defaultSearchOperator: rt.union([rt.literal('AND'), rt.literal('OR')]),
fields: rt.array(rt.string),
Expand Down Expand Up @@ -177,7 +189,6 @@ export type CasesResponse = rt.TypeOf<typeof CasesResponseRt>;
export type CasesFindResponse = rt.TypeOf<typeof CasesFindResponseRt>;
export type CasePatchRequest = rt.TypeOf<typeof CasePatchRequestRt>;
export type CasesPatchRequest = rt.TypeOf<typeof CasesPatchRequestRt>;
export type Status = rt.TypeOf<typeof StatusRt>;
export type CaseExternalServiceRequest = rt.TypeOf<typeof CaseExternalServiceRequestRt>;
export type ServiceConnectorCaseParams = rt.TypeOf<typeof ServiceConnectorCaseParamsRt>;
export type ServiceConnectorCaseResponse = rt.TypeOf<typeof ServiceConnectorCaseResponseRt>;
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/case/common/api/cases/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as rt from 'io-ts';

export const CasesStatusResponseRt = rt.type({
count_open_cases: rt.number,
count_in_progress_cases: rt.number,
count_closed_cases: rt.number,
});

Expand Down
10 changes: 5 additions & 5 deletions x-pack/plugins/case/server/client/cases/create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { ConnectorTypes, CasePostRequest } from '../../../common/api';
import { ConnectorTypes, CasePostRequest, CaseStatuses } from '../../../common/api';

import {
createMockSavedObjectsRepository,
Expand Down Expand Up @@ -60,7 +60,7 @@ describe('create', () => {
description: 'This is a brand new case of a bad meanie defacing data',
external_service: null,
title: 'Super Bad Security Issue',
status: 'open',
status: CaseStatuses.open,
tags: ['defacement'],
updated_at: null,
updated_by: null,
Expand Down Expand Up @@ -126,7 +126,7 @@ describe('create', () => {
description: 'This is a brand new case of a bad meanie defacing data',
external_service: null,
title: 'Super Bad Security Issue',
status: 'open',
status: CaseStatuses.open,
tags: ['defacement'],
updated_at: null,
updated_by: null,
Expand Down Expand Up @@ -169,7 +169,7 @@ describe('create', () => {
description: 'This is a brand new case of a bad meanie defacing data',
external_service: null,
title: 'Super Bad Security Issue',
status: 'open',
status: CaseStatuses.open,
tags: ['defacement'],
updated_at: null,
updated_by: null,
Expand Down Expand Up @@ -316,7 +316,7 @@ describe('create', () => {
title: 'a title',
description: 'This is a brand new case of a bad meanie defacing data',
tags: ['defacement'],
status: 'closed',
status: CaseStatuses.closed,
connector: {
id: 'none',
name: 'none',
Expand Down
27 changes: 15 additions & 12 deletions x-pack/plugins/case/server/client/cases/update.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { ConnectorTypes, CasesPatchRequest } from '../../../common/api';
import { ConnectorTypes, CasesPatchRequest, CaseStatuses } from '../../../common/api';
import {
createMockSavedObjectsRepository,
mockCaseNoConnectorId,
Expand All @@ -27,7 +27,7 @@ describe('update', () => {
cases: [
{
id: 'mock-id-1',
status: 'closed' as const,
status: CaseStatuses.closed,
version: 'WzAsMV0=',
},
],
Expand Down Expand Up @@ -56,7 +56,7 @@ describe('update', () => {
description: 'This is a brand new case of a bad meanie defacing data',
id: 'mock-id-1',
external_service: null,
status: 'closed',
status: CaseStatuses.closed,
tags: ['defacement'],
title: 'Super Bad Security Issue',
totalComment: 0,
Expand All @@ -79,8 +79,8 @@ describe('update', () => {
username: 'awesome',
},
action_field: ['status'],
new_value: 'closed',
old_value: 'open',
new_value: CaseStatuses.closed,
old_value: CaseStatuses.open,
},
references: [
{
Expand All @@ -98,15 +98,18 @@ describe('update', () => {
cases: [
{
id: 'mock-id-1',
status: 'open' as const,
status: CaseStatuses.open,
version: 'WzAsMV0=',
},
],
};

const savedObjectsClient = createMockSavedObjectsRepository({
caseSavedObject: [
{ ...mockCases[0], attributes: { ...mockCases[0].attributes, status: 'closed' } },
{
...mockCases[0],
attributes: { ...mockCases[0].attributes, status: CaseStatuses.closed },
},
...mockCases.slice(1),
],
});
Expand All @@ -130,7 +133,7 @@ describe('update', () => {
description: 'This is a brand new case of a bad meanie defacing data',
id: 'mock-id-1',
external_service: null,
status: 'open',
status: CaseStatuses.open,
tags: ['defacement'],
title: 'Super Bad Security Issue',
totalComment: 0,
Expand All @@ -146,7 +149,7 @@ describe('update', () => {
cases: [
{
id: 'mock-no-connector_id',
status: 'closed' as const,
status: CaseStatuses.closed,
version: 'WzAsMV0=',
},
],
Expand Down Expand Up @@ -177,7 +180,7 @@ describe('update', () => {
description: 'This is a brand new case of a bad meanie defacing data',
external_service: null,
title: 'Super Bad Security Issue',
status: 'closed',
status: CaseStatuses.closed,
tags: ['defacement'],
updated_at: '2019-11-25T21:54:48.952Z',
updated_by: { email: '[email protected]', full_name: 'Awesome D00d', username: 'awesome' },
Expand Down Expand Up @@ -231,7 +234,7 @@ describe('update', () => {
description: 'Oh no, a bad meanie going LOLBins all over the place!',
external_service: null,
title: 'Another bad one',
status: 'open',
status: CaseStatuses.open,
tags: ['LOLBins'],
updated_at: '2019-11-25T21:54:48.952Z',
updated_by: {
Expand Down Expand Up @@ -314,7 +317,7 @@ describe('update', () => {
cases: [
{
id: 'mock-id-1',
status: 'open' as const,
status: CaseStatuses.open,
version: 'WzAsMV0=',
},
],
Expand Down
8 changes: 6 additions & 2 deletions x-pack/plugins/case/server/client/cases/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
ESCasePatchRequest,
CasePatchRequest,
CasesResponse,
CaseStatuses,
} from '../../../common/api';
import { buildCaseUserActions } from '../../services/user_actions/helpers';
import {
Expand Down Expand Up @@ -98,12 +99,15 @@ export const update = ({
cases: updateFilterCases.map((thisCase) => {
const { id: caseId, version, ...updateCaseAttributes } = thisCase;
let closedInfo = {};
if (updateCaseAttributes.status && updateCaseAttributes.status === 'closed') {
if (updateCaseAttributes.status && updateCaseAttributes.status === CaseStatuses.closed) {
closedInfo = {
closed_at: updatedDt,
closed_by: { email, full_name, username },
};
} else if (updateCaseAttributes.status && updateCaseAttributes.status === 'open') {
} else if (
updateCaseAttributes.status &&
updateCaseAttributes.status === CaseStatuses.open
) {
closedInfo = {
closed_at: null,
closed_by: null,
Expand Down
8 changes: 4 additions & 4 deletions x-pack/plugins/case/server/connectors/case/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Logger } from '../../../../../../src/core/server';
import { loggingSystemMock } from '../../../../../../src/core/server/mocks';
import { actionsMock } from '../../../../actions/server/mocks';
import { validateParams } from '../../../../actions/server/lib';
import { ConnectorTypes, CommentType } from '../../../common/api';
import { ConnectorTypes, CommentType, CaseStatuses } from '../../../common/api';
import {
createCaseServiceMock,
createConfigureServiceMock,
Expand Down Expand Up @@ -785,7 +785,7 @@ describe('case connector', () => {
tags: ['case', 'connector'],
description: 'Yo fields!!',
external_service: null,
status: 'open' as const,
status: CaseStatuses.open,
updated_at: null,
updated_by: null,
version: 'WzksMV0=',
Expand Down Expand Up @@ -868,7 +868,7 @@ describe('case connector', () => {
description: 'This is a brand new case of a bad meanie defacing data',
id: 'mock-id-1',
external_service: null,
status: 'open' as const,
status: CaseStatuses.open,
tags: ['defacement'],
title: 'Update title',
totalComment: 0,
Expand Down Expand Up @@ -937,7 +937,7 @@ describe('case connector', () => {
description: 'This is a brand new case of a bad meanie defacing data',
external_service: null,
title: 'Super Bad Security Issue',
status: 'open' as const,
status: CaseStatuses.open,
tags: ['defacement'],
updated_at: null,
updated_by: null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
ESCaseAttributes,
ConnectorTypes,
CommentType,
CaseStatuses,
} from '../../../../common/api';

export const mockCases: Array<SavedObject<ESCaseAttributes>> = [
Expand All @@ -35,7 +36,7 @@ export const mockCases: Array<SavedObject<ESCaseAttributes>> = [
description: 'This is a brand new case of a bad meanie defacing data',
external_service: null,
title: 'Super Bad Security Issue',
status: 'open',
status: CaseStatuses.open,
tags: ['defacement'],
updated_at: '2019-11-25T21:54:48.952Z',
updated_by: {
Expand Down Expand Up @@ -69,7 +70,7 @@ export const mockCases: Array<SavedObject<ESCaseAttributes>> = [
description: 'Oh no, a bad meanie destroying data!',
external_service: null,
title: 'Damaging Data Destruction Detected',
status: 'open',
status: CaseStatuses.open,
tags: ['Data Destruction'],
updated_at: '2019-11-25T22:32:00.900Z',
updated_by: {
Expand Down Expand Up @@ -107,7 +108,7 @@ export const mockCases: Array<SavedObject<ESCaseAttributes>> = [
description: 'Oh no, a bad meanie going LOLBins all over the place!',
external_service: null,
title: 'Another bad one',
status: 'open',
status: CaseStatuses.open,
tags: ['LOLBins'],
updated_at: '2019-11-25T22:32:17.947Z',
updated_by: {
Expand Down Expand Up @@ -148,7 +149,7 @@ export const mockCases: Array<SavedObject<ESCaseAttributes>> = [
},
description: 'Oh no, a bad meanie going LOLBins all over the place!',
external_service: null,
status: 'closed',
status: CaseStatuses.closed,
title: 'Another bad one',
tags: ['LOLBins'],
updated_at: '2019-11-25T22:32:17.947Z',
Expand Down Expand Up @@ -179,7 +180,7 @@ export const mockCaseNoConnectorId: SavedObject<Partial<ESCaseAttributes>> = {
description: 'This is a brand new case of a bad meanie defacing data',
external_service: null,
title: 'Super Bad Security Issue',
status: 'open',
status: CaseStatuses.open,
tags: ['defacement'],
updated_at: '2019-11-25T21:54:48.952Z',
updated_by: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ describe('FIND all cases', () => {
const response = await routeHandler(theContext, request, kibanaResponseFactory);
expect(response.status).toEqual(200);
expect(response.payload.cases).toHaveLength(4);
// mockSavedObjectsRepository do not support filters and returns all cases every time.
expect(response.payload.count_open_cases).toEqual(4);
expect(response.payload.count_closed_cases).toEqual(4);
expect(response.payload.count_in_progress_cases).toEqual(4);
});

it(`has proper connector id on cases with configured connector`, async () => {
Expand Down
34 changes: 16 additions & 18 deletions x-pack/plugins/case/server/routes/api/cases/find_cases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ import { fold } from 'fp-ts/lib/Either';
import { identity } from 'fp-ts/lib/function';

import { isEmpty } from 'lodash';
import { CasesFindResponseRt, CasesFindRequestRt, throwErrors } from '../../../../common/api';
import {
CasesFindResponseRt,
CasesFindRequestRt,
throwErrors,
CaseStatuses,
caseStatuses,
} from '../../../../common/api';
import { transformCases, sortToSnake, wrapError, escapeHatch } from '../utils';
import { RouteDeps, TotalCommentByCase } from '../types';
import { CASE_SAVED_OBJECT } from '../../../saved_object_types';
Expand All @@ -20,7 +26,7 @@ import { CASES_URL } from '../../../../common/constants';
const combineFilters = (filters: string[], operator: 'OR' | 'AND'): string =>
filters?.filter((i) => i !== '').join(` ${operator} `);

const getStatusFilter = (status: 'open' | 'closed', appendFilter?: string) =>
const getStatusFilter = (status: CaseStatuses, appendFilter?: string) =>
`${CASE_SAVED_OBJECT}.attributes.status: ${status}${
!isEmpty(appendFilter) ? ` AND ${appendFilter}` : ''
}`;
Expand Down Expand Up @@ -75,30 +81,21 @@ export function initFindCasesApi({ caseService, caseConfigureService, router }:
client,
};

const argsOpenCases = {
const statusArgs = caseStatuses.map((caseStatus) => ({
client,
options: {
fields: [],
page: 1,
perPage: 1,
filter: getStatusFilter('open', myFilters),
filter: getStatusFilter(caseStatus, myFilters),
},
};
}));

const argsClosedCases = {
client,
options: {
fields: [],
page: 1,
perPage: 1,
filter: getStatusFilter('closed', myFilters),
},
};
const [cases, openCases, closesCases] = await Promise.all([
const [cases, openCases, inProgressCases, closedCases] = await Promise.all([
caseService.findCases(args),
caseService.findCases(argsOpenCases),
caseService.findCases(argsClosedCases),
...statusArgs.map((arg) => caseService.findCases(arg)),
]);

const totalCommentsFindByCases = await Promise.all(
cases.saved_objects.map((c) =>
caseService.getAllCaseComments({
Expand Down Expand Up @@ -133,7 +130,8 @@ export function initFindCasesApi({ caseService, caseConfigureService, router }:
transformCases(
cases,
openCases.total ?? 0,
closesCases.total ?? 0,
inProgressCases.total ?? 0,
closedCases.total ?? 0,
totalCommentsByCases
)
),
Expand Down
Loading