From a0294e59f1c712cd0e46c56e4aa667c9bc4b4f9d Mon Sep 17 00:00:00 2001 From: Esteban Beltran <academo@users.noreply.github.com> Date: Thu, 5 May 2022 10:53:03 +0200 Subject: [PATCH] [Cases] Add the severity field to the cases API (#131394) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add severity field to create API and migration * Adds integration test for severity field migration * remove exclusive test * Change severity levels * Update integration tests for post case * Add more integration tests * Fix all cases list test * Fix some server test * Fix util server test * Fix client util test * Convert event log's duration from number to string in Kibana (keep as "long" in Elasticsearch) (#130819) * Convert event.duration to string in TypeScript, keep as long in Elasticsearch * Fix jest test * Fix functional tests * Add ecsStringOrNumber to event log schema * Fix jest test * Add utility functions to event log plugin * Use new event log utility functions * PR fixes Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> * filter o11y rule aggregations (#131301) * [Cloud Posture] Display and save rules per benchmark (#131412) * Adding aria-label for discover data grid select document checkbox (#131277) * Update API docs (#130999) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> * [CI] Use GCS buckets for bazel remote caching (#131345) * [Actionable Observability] Add license modal to rules table (#131232) * Add fix license link * fix localization * fix CI error * fix more translation issues Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> * [RAM] Add shareable rule status filter (#130705) * rule state filter * turn off experiment * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * Status filter API call * Fix tests * rename state to status, added tests * Address comments and fix tests * Revert experiment flag * Remove unused translations * Addressed comments Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> * [storybook] Watch for changes in packages (#131467) * [storybook] Watch for changes in packages * Update default_config.ts * Improve saved objects migrations failure errors and logs (#131359) * [Unified observability] Add tour step to guided setup (#131149) * [Lens] Improved interval input (#131372) * [Vega] Adjust vega doc for usage of ems files (#130948) * adjust vega doc * Update docs/user/dashboard/vega-reference.asciidoc Co-authored-by: Nick Peihl <nickpeihl@gmail.com> * Update docs/user/dashboard/vega-reference.asciidoc Co-authored-by: Nick Peihl <nickpeihl@gmail.com> * Update docs/user/dashboard/vega-reference.asciidoc Co-authored-by: Nick Peihl <nickpeihl@gmail.com> * Update docs/user/dashboard/vega-reference.asciidoc Co-authored-by: Nick Peihl <nickpeihl@gmail.com> * Update docs/user/dashboard/vega-reference.asciidoc Co-authored-by: Nick Peihl <nickpeihl@gmail.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Nick Peihl <nickpeihl@gmail.com> * Excess intersections * Create severity user action * Add severity to create_case user action * Fix and add integration tests * Minor improvements Co-authored-by: Mike Côté <mikecote@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: mgiota <panagiota.mitsopoulou@elastic.co> Co-authored-by: Jordan <51442161+JordanSh@users.noreply.github.com> Co-authored-by: Bhavya RM <bhavya@elastic.co> Co-authored-by: Thomas Neirynck <thomas@elastic.co> Co-authored-by: Brian Seeders <brian.seeders@elastic.co> Co-authored-by: Jiawei Wu <74562234+JiaweiWu@users.noreply.github.com> Co-authored-by: Clint Andrew Hall <clint.hall@elastic.co> Co-authored-by: Christiane (Tina) Heiligers <christiane.heiligers@elastic.co> Co-authored-by: Alejandro Fernández Gómez <alejandro.fernandez@elastic.co> Co-authored-by: Joe Reuter <johannes.reuter@elastic.co> Co-authored-by: Nick Peihl <nickpeihl@gmail.com> Co-authored-by: Christos Nasikas <christos.nasikas@elastic.co> --- x-pack/plugins/cases/common/api/cases/case.ts | 81 +++++++---- .../common/api/cases/user_actions/common.ts | 1 + .../api/cases/user_actions/create_case.ts | 1 + .../common/api/cases/user_actions/index.ts | 2 + .../common/api/cases/user_actions/severity.ts | 19 +++ .../plugins/cases/common/api/runtime_types.ts | 94 +++++++----- .../all_cases/all_cases_list.test.tsx | 3 +- .../components/user_actions/builder.tsx | 4 + .../plugins/cases/public/containers/mock.ts | 4 + .../cases/server/client/cases/create.ts | 5 +- .../plugins/cases/server/client/cases/mock.ts | 1 + .../plugins/cases/server/client/utils.test.ts | 71 --------- .../plugins/cases/server/common/utils.test.ts | 135 ++++++++++++++++++ x-pack/plugins/cases/server/common/utils.ts | 2 + .../api/__fixtures__/mock_saved_objects.ts | 5 + .../cases/server/saved_object_types/cases.ts | 3 + .../migrations/cases.test.ts | 44 +++++- .../saved_object_types/migrations/cases.ts | 12 +- .../saved_object_types/migrations/utils.ts | 9 +- .../cases/server/services/cases/index.test.ts | 2 + .../cases/server/services/test_utils.ts | 8 +- .../user_actions/builder_factory.test.ts | 36 +++++ .../services/user_actions/builder_factory.ts | 2 + .../user_actions/builders/severity.ts | 22 +++ .../services/user_actions/index.test.ts | 55 +++++++ .../server/services/user_actions/mocks.ts | 4 +- .../server/services/user_actions/types.ts | 4 + .../cases_api_integration/common/lib/mock.ts | 2 + .../tests/common/cases/import_export.ts | 6 + .../tests/common/cases/migrations.ts | 56 +++++--- .../tests/common/cases/patch_cases.ts | 45 ++++++ .../tests/common/cases/post_case.ts | 33 +++++ .../user_actions/get_all_user_actions.ts | 25 ++++ 33 files changed, 629 insertions(+), 167 deletions(-) create mode 100644 x-pack/plugins/cases/common/api/cases/user_actions/severity.ts create mode 100644 x-pack/plugins/cases/server/services/user_actions/builders/severity.ts diff --git a/x-pack/plugins/cases/common/api/cases/case.ts b/x-pack/plugins/cases/common/api/cases/case.ts index 3f42e5b5c875c..b3dbe4801f544 100644 --- a/x-pack/plugins/cases/common/api/cases/case.ts +++ b/x-pack/plugins/cases/common/api/cases/case.ts @@ -39,6 +39,20 @@ export const SettingsRt = rt.type({ syncAlerts: rt.boolean, }); +export enum CaseSeverity { + LOW = 'low', + MEDIUM = 'medium', + HIGH = 'high', + CRITICAL = 'critical', +} + +export const CaseSeverityRt = rt.union([ + rt.literal(CaseSeverity.LOW), + rt.literal(CaseSeverity.MEDIUM), + rt.literal(CaseSeverity.HIGH), + rt.literal(CaseSeverity.CRITICAL), +]); + const CaseBasicRt = rt.type({ /** * The description of the case @@ -68,6 +82,10 @@ const CaseBasicRt = rt.type({ * The plugin owner of the case */ owner: rt.string, + /** + * The severity of the case + */ + severity: CaseSeverityRt, }); /** @@ -106,33 +124,42 @@ export const CaseAttributesRt = rt.intersection([ }), ]); -export const CasePostRequestRt = rt.type({ - /** - * Description of the case - */ - description: rt.string, - /** - * Identifiers for the case. - */ - tags: rt.array(rt.string), - /** - * Title of the case - */ - title: rt.string, - /** - * The external configuration for the case - */ - connector: CaseConnectorRt, - /** - * Sync settings for alerts - */ - settings: SettingsRt, - /** - * The owner here must match the string used when a plugin registers a feature with access to the cases plugin. The user - * creating this case must also be granted access to that plugin's feature. - */ - owner: rt.string, -}); +export const CasePostRequestRt = rt.intersection([ + rt.type({ + /** + * Description of the case + */ + description: rt.string, + /** + * Identifiers for the case. + */ + tags: rt.array(rt.string), + /** + * Title of the case + */ + title: rt.string, + /** + * The external configuration for the case + */ + connector: CaseConnectorRt, + /** + * Sync settings for alerts + */ + settings: SettingsRt, + /** + * The owner here must match the string used when a plugin registers a feature with access to the cases plugin. The user + * creating this case must also be granted access to that plugin's feature. + */ + owner: rt.string, + }), + rt.partial({ + /** + * The severity of the case. The severity is + * default it to "low" if not provided. + */ + severity: CaseSeverityRt, + }), +]); export const CasesFindRequestRt = rt.partial({ /** diff --git a/x-pack/plugins/cases/common/api/cases/user_actions/common.ts b/x-pack/plugins/cases/common/api/cases/user_actions/common.ts index a6d12d135c142..5665ab524071a 100644 --- a/x-pack/plugins/cases/common/api/cases/user_actions/common.ts +++ b/x-pack/plugins/cases/common/api/cases/user_actions/common.ts @@ -17,6 +17,7 @@ export const ActionTypes = { title: 'title', status: 'status', settings: 'settings', + severity: 'severity', create_case: 'create_case', delete_case: 'delete_case', } as const; diff --git a/x-pack/plugins/cases/common/api/cases/user_actions/create_case.ts b/x-pack/plugins/cases/common/api/cases/user_actions/create_case.ts index c491cc519132f..53d2320b5afd4 100644 --- a/x-pack/plugins/cases/common/api/cases/user_actions/create_case.ts +++ b/x-pack/plugins/cases/common/api/cases/user_actions/create_case.ts @@ -23,6 +23,7 @@ export const CommonFieldsRt = rt.type({ const CommonPayloadAttributesRt = rt.type({ description: DescriptionUserActionPayloadRt.props.description, status: rt.string, + severity: rt.string, tags: TagsUserActionPayloadRt.props.tags, title: TitleUserActionPayloadRt.props.title, settings: SettingsUserActionPayloadRt.props.settings, diff --git a/x-pack/plugins/cases/common/api/cases/user_actions/index.ts b/x-pack/plugins/cases/common/api/cases/user_actions/index.ts index 3f974d89fc79a..d19ee5fcbe9f8 100644 --- a/x-pack/plugins/cases/common/api/cases/user_actions/index.ts +++ b/x-pack/plugins/cases/common/api/cases/user_actions/index.ts @@ -23,6 +23,7 @@ import { TitleUserActionRt } from './title'; import { SettingsUserActionRt } from './settings'; import { StatusUserActionRt } from './status'; import { DeleteCaseUserActionRt } from './delete_case'; +import { SeverityUserActionRt } from './severity'; export * from './common'; export * from './comment'; @@ -43,6 +44,7 @@ const CommonUserActionsRt = rt.union([ TitleUserActionRt, SettingsUserActionRt, StatusUserActionRt, + SeverityUserActionRt, ]); export const UserActionsRt = rt.union([ diff --git a/x-pack/plugins/cases/common/api/cases/user_actions/severity.ts b/x-pack/plugins/cases/common/api/cases/user_actions/severity.ts new file mode 100644 index 0000000000000..2db5a0880dc09 --- /dev/null +++ b/x-pack/plugins/cases/common/api/cases/user_actions/severity.ts @@ -0,0 +1,19 @@ +/* + * 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 * as rt from 'io-ts'; +import { CaseSeverityRt } from '../case'; +import { ActionTypes, UserActionWithAttributes } from './common'; + +export const SeverityUserActionPayloadRt = rt.type({ severity: CaseSeverityRt }); + +export const SeverityUserActionRt = rt.type({ + type: rt.literal(ActionTypes.severity), + payload: SeverityUserActionPayloadRt, +}); + +export type SeverityUserAction = UserActionWithAttributes<rt.TypeOf<typeof SeverityUserActionRt>>; diff --git a/x-pack/plugins/cases/common/api/runtime_types.ts b/x-pack/plugins/cases/common/api/runtime_types.ts index c807d4b31b751..0a31479b29da8 100644 --- a/x-pack/plugins/cases/common/api/runtime_types.ts +++ b/x-pack/plugins/cases/common/api/runtime_types.ts @@ -9,42 +9,18 @@ import { either, fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; import * as rt from 'io-ts'; -import { isObject } from 'lodash/fp'; +import { formatErrors } from '@kbn/securitysolution-io-ts-utils'; type ErrorFactory = (message: string) => Error; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils/src/format_errors/index.ts - * Bug fix for the TODO is in the format_errors package - */ -export const formatErrors = (errors: rt.Errors): string[] => { - const err = errors.map((error) => { - if (error.message != null) { - return error.message; - } else { - const keyContext = error.context - .filter( - (entry) => entry.key != null && !Number.isInteger(+entry.key) && entry.key.trim() !== '' - ) - .map((entry) => entry.key) - .join(','); - - const nameContext = error.context.find((entry) => { - // TODO: Put in fix for optional chaining https://github.com/cypress-io/cypress/issues/9298 - if (entry.type && entry.type.name) { - return entry.type.name.length > 0; - } - return false; - }); - const suppliedValue = - keyContext !== '' ? keyContext : nameContext != null ? nameContext.type.name : ''; - const value = isObject(error.value) ? JSON.stringify(error.value) : error.value; - return `Invalid value "${value}" supplied to "${suppliedValue}"`; - } - }); - - return [...new Set(err)]; -}; +export type GenericIntersectionC = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | rt.IntersectionC<[any, any]> + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | rt.IntersectionC<[any, any, any]> + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | rt.IntersectionC<[any, any, any, any]> + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | rt.IntersectionC<[any, any, any, any, any]>; export const createPlainError = (message: string) => new Error(message); @@ -57,6 +33,40 @@ export const decodeOrThrow = (inputValue: I) => pipe(runtimeType.decode(inputValue), fold(throwErrors(createError), identity)); +const getProps = ( + codec: + | rt.HasProps + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | rt.RecordC<rt.StringC, any> + | GenericIntersectionC +): rt.Props | null => { + if (codec == null) { + return null; + } + switch (codec._tag) { + case 'DictionaryType': + if (codec.codomain.props != null) { + return codec.codomain.props; + } + const dTypes: rt.HasProps[] = codec.codomain.types; + return dTypes.reduce<rt.Props>((props, type) => Object.assign(props, getProps(type)), {}); + case 'RefinementType': + case 'ReadonlyType': + return getProps(codec.type); + case 'InterfaceType': + case 'StrictType': + case 'PartialType': + return codec.props; + case 'IntersectionType': + const iTypes = codec.types as rt.HasProps[]; + return iTypes.reduce<rt.Props>((props, type) => { + return Object.assign(props, getProps(type) as rt.Props); + }, {} as rt.Props) as rt.Props; + default: + return null; + } +}; + const getExcessProps = (props: rt.Props, r: Record<string, unknown>): string[] => { const ex: string[] = []; for (const k of Object.keys(r)) { @@ -67,15 +77,21 @@ const getExcessProps = (props: rt.Props, r: Record<string, unknown>): string[] = return ex; }; -export function excess<C extends rt.InterfaceType<rt.Props> | rt.PartialType<rt.Props>>( - codec: C -): C { +export function excess< + C extends rt.InterfaceType<rt.Props> | GenericIntersectionC | rt.PartialType<rt.Props> +>(codec: C): C { + const codecProps = getProps(codec); + const r = new rt.InterfaceType( codec.name, codec.is, (i, c) => either.chain(rt.UnknownRecord.validate(i, c), (s: Record<string, unknown>) => { - const ex = getExcessProps(codec.props, s); + if (codecProps == null) { + return rt.failure(i, c, 'unknown codec'); + } + + const ex = getExcessProps(codecProps, s); return ex.length > 0 ? rt.failure( i, @@ -87,7 +103,7 @@ export function excess<C extends rt.InterfaceType<rt.Props> | rt.PartialType<rt. : codec.validate(i, c); }), codec.encode, - codec.props + codecProps ); return r as C; } diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx index 87d53aae14e28..f0a3502fd6813 100644 --- a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx @@ -17,7 +17,7 @@ import { TestProviders } from '../../common/mock'; import { casesStatus, useGetCasesMockState, mockCase, connectorsMock } from '../../containers/mock'; import { StatusAll } from '../../../common/ui/types'; -import { CaseStatuses } from '../../../common/api'; +import { CaseSeverity, CaseStatuses } from '../../../common/api'; import { SECURITY_SOLUTION_OWNER } from '../../../common/constants'; import { getEmptyTagValue } from '../empty_value'; import { useDeleteCases } from '../../containers/use_delete_cases'; @@ -560,6 +560,7 @@ describe('AllCasesListGeneric', () => { username: 'lknope', }, description: 'Security banana Issue', + severity: CaseSeverity.LOW, duration: null, externalService: { connectorId: '123', diff --git a/x-pack/plugins/cases/public/components/user_actions/builder.tsx b/x-pack/plugins/cases/public/components/user_actions/builder.tsx index 5e1c11fbdd2df..019e37396a7ce 100644 --- a/x-pack/plugins/cases/public/components/user_actions/builder.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/builder.tsx @@ -20,6 +20,10 @@ export const builderMap: UserActionBuilderMap = { tags: createTagsUserActionBuilder, title: createTitleUserActionBuilder, status: createStatusUserActionBuilder, + // TODO: Build severity user action + severity: () => ({ + build: () => [], + }), pushed: createPushedUserActionBuilder, comment: createCommentUserActionBuilder, description: createDescriptionUserActionBuilder, diff --git a/x-pack/plugins/cases/public/containers/mock.ts b/x-pack/plugins/cases/public/containers/mock.ts index 8a31d8cac2b1e..ed9e9ebd1ff8f 100644 --- a/x-pack/plugins/cases/public/containers/mock.ts +++ b/x-pack/plugins/cases/public/containers/mock.ts @@ -31,6 +31,7 @@ import { UserActionTypes, UserActionWithResponse, CommentUserAction, + CaseSeverity, } from '../../common/api'; import { SECURITY_SOLUTION_OWNER } from '../../common/constants'; import { UseGetCasesState, DEFAULT_FILTER_OPTIONS, DEFAULT_QUERY_PARAMS } from './use_get_cases'; @@ -154,6 +155,7 @@ export const basicCase: Case = { fields: null, }, description: 'Security banana Issue', + severity: CaseSeverity.LOW, duration: null, externalService: null, status: CaseStatuses.open, @@ -247,6 +249,7 @@ export const mockCase: Case = { fields: null, }, duration: null, + severity: CaseSeverity.LOW, description: 'Security banana Issue', externalService: null, status: CaseStatuses.open, @@ -512,6 +515,7 @@ export const getUserAction = ( description: 'a desc', connector: { ...getJiraConnector() }, status: CaseStatuses.open, + severity: CaseSeverity.LOW, title: 'a title', tags: ['a tag'], settings: { syncAlerts: true }, diff --git a/x-pack/plugins/cases/server/client/cases/create.ts b/x-pack/plugins/cases/server/client/cases/create.ts index ab9f6a4305800..714c8199d11a5 100644 --- a/x-pack/plugins/cases/server/client/cases/create.ts +++ b/x-pack/plugins/cases/server/client/cases/create.ts @@ -14,12 +14,13 @@ import { SavedObjectsUtils } from '@kbn/core/server'; import { throwErrors, - excess, CaseResponseRt, CaseResponse, CasePostRequest, ActionTypes, CasePostRequestRt, + excess, + CaseSeverity, } from '../../../common/api'; import { MAX_TITLE_LENGTH } from '../../../common/constants'; import { isInvalidTag } from '../../../common/utils/validators'; @@ -85,7 +86,7 @@ export const create = async ( unsecuredSavedObjectsClient, caseId: newCase.id, user, - payload: query, + payload: { ...query, severity: query.severity ?? CaseSeverity.LOW }, owner: newCase.attributes.owner, }); diff --git a/x-pack/plugins/cases/server/client/cases/mock.ts b/x-pack/plugins/cases/server/client/cases/mock.ts index 69a5f2d3a587b..4c0698b209bef 100644 --- a/x-pack/plugins/cases/server/client/cases/mock.ts +++ b/x-pack/plugins/cases/server/client/cases/mock.ts @@ -240,6 +240,7 @@ export const userActions: CaseUserActionsResponse = [ }, settings: { syncAlerts: true }, status: 'open', + severity: 'low', owner: SECURITY_SOLUTION_OWNER, }, action_id: 'fd830c60-6646-11eb-a291-51bf6b175a53', diff --git a/x-pack/plugins/cases/server/client/utils.test.ts b/x-pack/plugins/cases/server/client/utils.test.ts index 24e1135020a88..88140658c2b2b 100644 --- a/x-pack/plugins/cases/server/client/utils.test.ts +++ b/x-pack/plugins/cases/server/client/utils.test.ts @@ -5,9 +5,6 @@ * 2.0. */ -import { CaseConnector, ConnectorTypes } from '../../common/api'; -import { newCase } from '../routes/api/__mocks__/request_responses'; -import { transformNewCase } from '../common/utils'; import { buildRangeFilter, sortToSnake } from './utils'; import { toElasticsearchQuery } from '@kbn/es-query'; @@ -38,74 +35,6 @@ describe('utils', () => { }); }); - describe('transformNewCase', () => { - beforeAll(() => { - jest.useFakeTimers('modern'); - jest.setSystemTime(new Date('2020-04-09T09:43:51.778Z')); - }); - - afterAll(() => { - jest.useRealTimers(); - }); - - const connector: CaseConnector = { - id: '123', - name: 'My connector', - type: ConnectorTypes.jira, - fields: { issueType: 'Task', priority: 'High', parent: null }, - }; - it('transform correctly', () => { - const myCase = { - newCase: { ...newCase, connector }, - user: { - email: 'elastic@elastic.co', - full_name: 'Elastic', - username: 'elastic', - }, - }; - - const res = transformNewCase(myCase); - - expect(res).toMatchInlineSnapshot(` - Object { - "closed_at": null, - "closed_by": null, - "connector": Object { - "fields": Object { - "issueType": "Task", - "parent": null, - "priority": "High", - }, - "id": "123", - "name": "My connector", - "type": ".jira", - }, - "created_at": "2020-04-09T09:43:51.778Z", - "created_by": Object { - "email": "elastic@elastic.co", - "full_name": "Elastic", - "username": "elastic", - }, - "description": "A description", - "duration": null, - "external_service": null, - "owner": "securitySolution", - "settings": Object { - "syncAlerts": true, - }, - "status": "open", - "tags": Array [ - "new", - "case", - ], - "title": "My new case", - "updated_at": null, - "updated_by": null, - } - `); - }); - }); - describe('buildRangeFilter', () => { it('returns undefined if both the from and or are undefined', () => { const node = buildRangeFilter({}); diff --git a/x-pack/plugins/cases/server/common/utils.test.ts b/x-pack/plugins/cases/server/common/utils.test.ts index 974c36bd0d8a6..918a48863cac0 100644 --- a/x-pack/plugins/cases/server/common/utils.test.ts +++ b/x-pack/plugins/cases/server/common/utils.test.ts @@ -9,11 +9,14 @@ import { SavedObject, SavedObjectsFindResponse } from '@kbn/core/server'; import { makeLensEmbeddableFactory } from '@kbn/lens-plugin/server/embeddable/make_lens_embeddable_factory'; import { SECURITY_SOLUTION_OWNER } from '../../common/constants'; import { + CaseConnector, CaseResponse, + CaseSeverity, CommentAttributes, CommentRequest, CommentRequestUserType, CommentType, + ConnectorTypes, } from '../../common/api'; import { mockCaseComments, mockCases } from '../routes/api/__fixtures__/mock_saved_objects'; import { @@ -29,7 +32,9 @@ import { extractLensReferencesFromCommentString, getOrUpdateLensReferences, asArray, + transformNewCase, } from './utils'; +import { newCase } from '../routes/api/__mocks__/request_responses'; interface CommentReference { ids: string[]; @@ -67,6 +72,128 @@ function createCommentFindResponse( } describe('common utils', () => { + describe('transformNewCase', () => { + beforeAll(() => { + jest.useFakeTimers('modern'); + jest.setSystemTime(new Date('2020-04-09T09:43:51.778Z')); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + const connector: CaseConnector = { + id: '123', + name: 'My connector', + type: ConnectorTypes.jira, + fields: { issueType: 'Task', priority: 'High', parent: null }, + }; + + it('transform correctly', () => { + const myCase = { + newCase: { ...newCase, connector }, + user: { + email: 'elastic@elastic.co', + full_name: 'Elastic', + username: 'elastic', + }, + }; + + const res = transformNewCase(myCase); + + expect(res).toMatchInlineSnapshot(` + Object { + "closed_at": null, + "closed_by": null, + "connector": Object { + "fields": Object { + "issueType": "Task", + "parent": null, + "priority": "High", + }, + "id": "123", + "name": "My connector", + "type": ".jira", + }, + "created_at": "2020-04-09T09:43:51.778Z", + "created_by": Object { + "email": "elastic@elastic.co", + "full_name": "Elastic", + "username": "elastic", + }, + "description": "A description", + "duration": null, + "external_service": null, + "owner": "securitySolution", + "settings": Object { + "syncAlerts": true, + }, + "severity": "low", + "status": "open", + "tags": Array [ + "new", + "case", + ], + "title": "My new case", + "updated_at": null, + "updated_by": null, + } + `); + }); + + it('transform correctly with severity provided', () => { + const myCase = { + newCase: { ...newCase, connector, severity: CaseSeverity.MEDIUM }, + user: { + email: 'elastic@elastic.co', + full_name: 'Elastic', + username: 'elastic', + }, + }; + + const res = transformNewCase(myCase); + + expect(res).toMatchInlineSnapshot(` + Object { + "closed_at": null, + "closed_by": null, + "connector": Object { + "fields": Object { + "issueType": "Task", + "parent": null, + "priority": "High", + }, + "id": "123", + "name": "My connector", + "type": ".jira", + }, + "created_at": "2020-04-09T09:43:51.778Z", + "created_by": Object { + "email": "elastic@elastic.co", + "full_name": "Elastic", + "username": "elastic", + }, + "description": "A description", + "duration": null, + "external_service": null, + "owner": "securitySolution", + "settings": Object { + "syncAlerts": true, + }, + "severity": "medium", + "status": "open", + "tags": Array [ + "new", + "case", + ], + "title": "My new case", + "updated_at": null, + "updated_by": null, + } + `); + }); + }); + describe('transformCases', () => { it('transforms correctly', () => { const casesMap = new Map<string, CaseResponse>( @@ -110,6 +237,7 @@ describe('common utils', () => { "settings": Object { "syncAlerts": true, }, + "severity": "low", "status": "open", "tags": Array [ "defacement", @@ -149,6 +277,7 @@ describe('common utils', () => { "settings": Object { "syncAlerts": true, }, + "severity": "low", "status": "open", "tags": Array [ "Data Destruction", @@ -192,6 +321,7 @@ describe('common utils', () => { "settings": Object { "syncAlerts": true, }, + "severity": "low", "status": "open", "tags": Array [ "LOLBins", @@ -239,6 +369,7 @@ describe('common utils', () => { "settings": Object { "syncAlerts": true, }, + "severity": "low", "status": "closed", "tags": Array [ "LOLBins", @@ -303,6 +434,7 @@ describe('common utils', () => { "settings": Object { "syncAlerts": true, }, + "severity": "low", "status": "open", "tags": Array [ "LOLBins", @@ -358,6 +490,7 @@ describe('common utils', () => { "settings": Object { "syncAlerts": true, }, + "severity": "low", "status": "open", "tags": Array [ "LOLBins", @@ -436,6 +569,7 @@ describe('common utils', () => { "settings": Object { "syncAlerts": true, }, + "severity": "low", "status": "open", "tags": Array [ "LOLBins", @@ -489,6 +623,7 @@ describe('common utils', () => { "settings": Object { "syncAlerts": true, }, + "severity": "low", "status": "open", "tags": Array [ "defacement", diff --git a/x-pack/plugins/cases/server/common/utils.ts b/x-pack/plugins/cases/server/common/utils.ts index 11e77c5eb4579..bc8dbf8a6e842 100644 --- a/x-pack/plugins/cases/server/common/utils.ts +++ b/x-pack/plugins/cases/server/common/utils.ts @@ -19,6 +19,7 @@ import { CaseAttributes, CasePostRequest, CaseResponse, + CaseSeverity, CasesFindResponse, CaseStatuses, CommentAttributes, @@ -56,6 +57,7 @@ export const transformNewCase = ({ }): CaseAttributes => ({ ...newCase, duration: null, + severity: newCase.severity ?? CaseSeverity.LOW, closed_at: null, closed_by: null, created_at: new Date().toISOString(), diff --git a/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts b/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts index cc45ef0e2d069..77e1a64012c6d 100644 --- a/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts +++ b/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts @@ -8,6 +8,7 @@ import { SavedObject } from '@kbn/core/server'; import { CaseAttributes, + CaseSeverity, CaseStatuses, CommentAttributes, CommentType, @@ -34,6 +35,7 @@ export const mockCases: Array<SavedObject<CaseAttributes>> = [ email: 'testemail@elastic.co', username: 'elastic', }, + severity: CaseSeverity.LOW, duration: null, description: 'This is a brand new case of a bad meanie defacing data', external_service: null, @@ -73,6 +75,7 @@ export const mockCases: Array<SavedObject<CaseAttributes>> = [ email: 'testemail@elastic.co', username: 'elastic', }, + severity: CaseSeverity.LOW, duration: null, description: 'Oh no, a bad meanie destroying data!', external_service: null, @@ -112,6 +115,7 @@ export const mockCases: Array<SavedObject<CaseAttributes>> = [ email: 'testemail@elastic.co', username: 'elastic', }, + severity: CaseSeverity.LOW, duration: null, description: 'Oh no, a bad meanie going LOLBins all over the place!', external_service: null, @@ -155,6 +159,7 @@ export const mockCases: Array<SavedObject<CaseAttributes>> = [ email: 'testemail@elastic.co', username: 'elastic', }, + severity: CaseSeverity.LOW, duration: null, description: 'Oh no, a bad meanie going LOLBins all over the place!', external_service: null, diff --git a/x-pack/plugins/cases/server/saved_object_types/cases.ts b/x-pack/plugins/cases/server/saved_object_types/cases.ts index ea68fc24f60ca..9b2ea975c4dcd 100644 --- a/x-pack/plugins/cases/server/saved_object_types/cases.ts +++ b/x-pack/plugins/cases/server/saved_object_types/cases.ts @@ -152,6 +152,9 @@ export const createCaseSavedObjectType = ( }, }, }, + severity: { + type: 'keyword', + }, }, }, migrations: caseMigrations, diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/cases.test.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/cases.test.ts index 70e0e91caa57f..b4d3421643a41 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/cases.test.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/cases.test.ts @@ -9,13 +9,14 @@ import { SavedObjectSanitizedDoc } from '@kbn/core/server'; import { CaseAttributes, CaseFullExternalService, + CaseSeverity, ConnectorTypes, NONE_CONNECTOR_ID, } from '../../../common/api'; import { CASE_SAVED_OBJECT } from '../../../common/constants'; import { getNoneCaseConnector } from '../../common/utils'; import { createExternalService, ESCaseConnectorWithId } from '../../services/test_utils'; -import { addDuration, caseConnectorIdMigration, removeCaseType } from './cases'; +import { addDuration, addSeverity, caseConnectorIdMigration, removeCaseType } from './cases'; // eslint-disable-next-line @typescript-eslint/naming-convention const create_7_14_0_case = ({ @@ -496,4 +497,45 @@ describe('case migrations', () => { }); }); }); + + describe('add severity', () => { + it('adds the severity correctly when none is present', () => { + const doc = { + id: '123', + attributes: { + created_at: '2021-11-23T19:00:00Z', + closed_at: '2021-11-23T19:02:00Z', + }, + type: 'abc', + references: [], + } as unknown as SavedObjectSanitizedDoc<CaseAttributes>; + expect(addSeverity(doc)).toEqual({ + ...doc, + attributes: { + ...doc.attributes, + severity: CaseSeverity.LOW, + }, + }); + }); + + it('keeps the existing value if the field already exists', () => { + const doc = { + id: '123', + attributes: { + severity: CaseSeverity.CRITICAL, + created_at: '2021-11-23T19:00:00Z', + closed_at: '2021-11-23T19:02:00Z', + }, + type: 'abc', + references: [], + } as unknown as SavedObjectSanitizedDoc<CaseAttributes>; + expect(addSeverity(doc)).toEqual({ + ...doc, + attributes: { + ...doc.attributes, + severity: CaseSeverity.CRITICAL, + }, + }); + }); + }); }); diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/cases.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/cases.ts index 91a462c5c8053..c4961f742abc7 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/cases.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/cases.ts @@ -11,7 +11,7 @@ import { cloneDeep, unset } from 'lodash'; import { SavedObjectUnsanitizedDoc, SavedObjectSanitizedDoc } from '@kbn/core/server'; import { addOwnerToSO, SanitizedCaseOwner } from '.'; import { ESConnectorFields } from '../../services'; -import { CaseAttributes, ConnectorTypes } from '../../../common/api'; +import { CaseAttributes, CaseSeverity, ConnectorTypes } from '../../../common/api'; import { CONNECTOR_ID_REFERENCE_NAME, PUSH_CONNECTOR_ID_REFERENCE_NAME, @@ -21,6 +21,7 @@ import { transformPushConnectorIdToReference, } from './user_actions/connector_id'; import { CASE_TYPE_INDIVIDUAL } from './constants'; +import { pipeMigrations } from './utils'; interface UnsanitizedCaseConnector { connector_id: string; @@ -114,6 +115,13 @@ export const addDuration = ( return { ...doc, attributes: { ...doc.attributes, duration }, references: doc.references ?? [] }; }; +export const addSeverity = ( + doc: SavedObjectUnsanitizedDoc<CaseAttributes> +): SavedObjectSanitizedDoc<CaseAttributes> => { + const severity = doc.attributes.severity ?? CaseSeverity.LOW; + return { ...doc, attributes: { ...doc.attributes, severity }, references: doc.references ?? [] }; +}; + export const caseMigrations = { '7.10.0': ( doc: SavedObjectUnsanitizedDoc<UnsanitizedCaseConnector> @@ -175,5 +183,5 @@ export const caseMigrations = { }, '7.15.0': caseConnectorIdMigration, '8.1.0': removeCaseType, - '8.3.0': addDuration, + '8.3.0': pipeMigrations(addDuration, addSeverity), }; diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts index 65c1d42271845..8996f89155949 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { LogMeta, SavedObjectMigrationContext } from '@kbn/core/server'; +import { LogMeta, SavedObjectMigrationContext, SavedObjectUnsanitizedDoc } from '@kbn/core/server'; interface MigrationLogMeta extends LogMeta { migrations: { @@ -39,3 +39,10 @@ export function logError({ } ); } + +type CaseMigration<T> = (doc: SavedObjectUnsanitizedDoc<T>) => SavedObjectUnsanitizedDoc<T>; + +export function pipeMigrations<T>(...migrations: Array<CaseMigration<T>>): CaseMigration<T> { + return (doc: SavedObjectUnsanitizedDoc<T>) => + migrations.reduce((migratedDoc, nextMigration) => nextMigration(migratedDoc), doc); +} diff --git a/x-pack/plugins/cases/server/services/cases/index.test.ts b/x-pack/plugins/cases/server/services/cases/index.test.ts index 84c580c8800e3..826a8d06e97f2 100644 --- a/x-pack/plugins/cases/server/services/cases/index.test.ts +++ b/x-pack/plugins/cases/server/services/cases/index.test.ts @@ -166,6 +166,7 @@ describe('CasesService', () => { "settings": Object { "syncAlerts": true, }, + "severity": "low", "status": "open", "tags": Array [ "defacement", @@ -519,6 +520,7 @@ describe('CasesService', () => { "settings": Object { "syncAlerts": true, }, + "severity": "low", "status": "open", "tags": Array [ "defacement", diff --git a/x-pack/plugins/cases/server/services/test_utils.ts b/x-pack/plugins/cases/server/services/test_utils.ts index 617dedd368ab3..ff86783ae8e9c 100644 --- a/x-pack/plugins/cases/server/services/test_utils.ts +++ b/x-pack/plugins/cases/server/services/test_utils.ts @@ -10,15 +10,18 @@ import { ACTION_SAVED_OBJECT_TYPE } from '@kbn/actions-plugin/server'; import { ESConnectorFields } from '.'; import { CONNECTOR_ID_REFERENCE_NAME, PUSH_CONNECTOR_ID_REFERENCE_NAME } from '../common/constants'; import { + CaseAttributes, CaseConnector, CaseExternalServiceBasic, CaseFullExternalService, + CaseSeverity, CaseStatuses, ConnectorTypes, NONE_CONNECTOR_ID, } from '../../common/api'; import { CASE_SAVED_OBJECT, SECURITY_SOLUTION_OWNER } from '../../common/constants'; import { ESCaseAttributes, ExternalServicesWithoutConnectorId } from './cases/types'; +import { getNoneCaseConnector } from '../common/utils'; /** * This is only a utility interface to help with constructing test cases. After the migration, the ES format will no longer @@ -96,7 +99,7 @@ export const createExternalService = ( ...overrides, }); -export const basicCaseFields = { +export const basicCaseFields: CaseAttributes = { closed_at: null, closed_by: null, created_at: '2019-11-25T21:54:48.952Z', @@ -105,6 +108,7 @@ export const basicCaseFields = { email: 'testemail@elastic.co', username: 'elastic', }, + severity: CaseSeverity.LOW, duration: null, description: 'This is a brand new case of a bad meanie defacing data', title: 'Super Bad Security Issue', @@ -116,6 +120,8 @@ export const basicCaseFields = { email: 'testemail@elastic.co', username: 'elastic', }, + connector: getNoneCaseConnector(), + external_service: null, settings: { syncAlerts: true, }, diff --git a/x-pack/plugins/cases/server/services/user_actions/builder_factory.test.ts b/x-pack/plugins/cases/server/services/user_actions/builder_factory.test.ts index 2e2a9e905bb7e..ab349d690edef 100644 --- a/x-pack/plugins/cases/server/services/user_actions/builder_factory.test.ts +++ b/x-pack/plugins/cases/server/services/user_actions/builder_factory.test.ts @@ -9,6 +9,7 @@ import { SECURITY_SOLUTION_OWNER } from '../../../common'; import { Actions, ActionTypes, + CaseSeverity, CaseStatuses, CommentType, ConnectorTypes, @@ -340,6 +341,40 @@ describe('UserActionBuilder', () => { `); }); + it('builds a severity user action correctly', () => { + const builder = builderFactory.getBuilder(ActionTypes.severity)!; + const userAction = builder.build({ + payload: { severity: CaseSeverity.LOW }, + ...commonArgs, + }); + + expect(userAction).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "action": "update", + "created_at": "2022-01-09T22:00:00.000Z", + "created_by": Object { + "email": "elastic@elastic.co", + "full_name": "Elastic User", + "username": "elastic", + }, + "owner": "securitySolution", + "payload": Object { + "severity": "low", + }, + "type": "severity", + }, + "references": Array [ + Object { + "id": "123", + "name": "associated-cases", + "type": "cases", + }, + ], + } + `); + }); + it('builds a settings user action correctly', () => { const builder = builderFactory.getBuilder(ActionTypes.settings)!; const userAction = builder.build({ @@ -413,6 +448,7 @@ describe('UserActionBuilder', () => { "settings": Object { "syncAlerts": true, }, + "severity": "low", "status": "open", "tags": Array [ "sir", diff --git a/x-pack/plugins/cases/server/services/user_actions/builder_factory.ts b/x-pack/plugins/cases/server/services/user_actions/builder_factory.ts index 5d5f33c2ae4f5..510b6d12b1fa1 100644 --- a/x-pack/plugins/cases/server/services/user_actions/builder_factory.ts +++ b/x-pack/plugins/cases/server/services/user_actions/builder_factory.ts @@ -17,6 +17,7 @@ import { TagsUserActionBuilder } from './builders/tags'; import { SettingsUserActionBuilder } from './builders/settings'; import { DeleteCaseUserActionBuilder } from './builders/delete_case'; import { UserActionBuilder } from './abstract_builder'; +import { SeverityUserActionBuilder } from './builders/severity'; const builderMap = { title: TitleUserActionBuilder, @@ -27,6 +28,7 @@ const builderMap = { pushed: PushedUserActionBuilder, tags: TagsUserActionBuilder, status: StatusUserActionBuilder, + severity: SeverityUserActionBuilder, settings: SettingsUserActionBuilder, delete_case: DeleteCaseUserActionBuilder, }; diff --git a/x-pack/plugins/cases/server/services/user_actions/builders/severity.ts b/x-pack/plugins/cases/server/services/user_actions/builders/severity.ts new file mode 100644 index 0000000000000..4abd5856972b4 --- /dev/null +++ b/x-pack/plugins/cases/server/services/user_actions/builders/severity.ts @@ -0,0 +1,22 @@ +/* + * 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 { Actions, ActionTypes } from '../../../../common/api'; +import { UserActionBuilder } from '../abstract_builder'; +import { UserActionParameters, BuilderReturnValue } from '../types'; + +export class SeverityUserActionBuilder extends UserActionBuilder { + build(args: UserActionParameters<'severity'>): BuilderReturnValue { + return this.buildCommonUserAction({ + ...args, + action: Actions.update, + valueKey: 'severity', + value: args.payload.severity, + type: ActionTypes.severity, + }); + } +} diff --git a/x-pack/plugins/cases/server/services/user_actions/index.test.ts b/x-pack/plugins/cases/server/services/user_actions/index.test.ts index eb1b57622d24d..44e91bcae09d3 100644 --- a/x-pack/plugins/cases/server/services/user_actions/index.test.ts +++ b/x-pack/plugins/cases/server/services/user_actions/index.test.ts @@ -13,6 +13,7 @@ import { ACTION_SAVED_OBJECT_TYPE } from '@kbn/actions-plugin/server'; import { Actions, ActionTypes, + CaseSeverity, CaseStatuses, CaseUserActionAttributes, ConnectorUserAction, @@ -107,6 +108,7 @@ const createCaseUserAction = (): SavedObject<CaseUserActionAttributes> => { description: 'a desc', settings: { syncAlerts: false }, status: CaseStatuses.open, + severity: CaseSeverity.LOW, tags: [], owner: SECURITY_SOLUTION_OWNER, }, @@ -447,6 +449,7 @@ describe('CaseUserActionService', () => { payload: casePayload, type: ActionTypes.create_case, }); + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledWith( 'cases-user-actions', { @@ -477,6 +480,7 @@ describe('CaseUserActionService', () => { owner: 'securitySolution', settings: { syncAlerts: true }, status: 'open', + severity: 'low', tags: ['sir'], title: 'Case SIR', }, @@ -517,6 +521,33 @@ describe('CaseUserActionService', () => { }); }); + describe('severity', () => { + it('creates an update severity user action', async () => { + await service.createUserAction({ + ...commonArgs, + payload: { severity: CaseSeverity.MEDIUM }, + type: ActionTypes.severity, + }); + + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledWith( + 'cases-user-actions', + { + action: Actions.update, + created_at: '2022-01-09T22:00:00.000Z', + created_by: { + email: 'elastic@elastic.co', + full_name: 'Elastic User', + username: 'elastic', + }, + type: 'severity', + owner: 'securitySolution', + payload: { severity: 'medium' }, + }, + { references: [{ id: '123', name: 'associated-cases', type: 'cases' }] } + ); + }); + }); + describe('push', () => { it('creates a push user action', async () => { await service.createUserAction({ @@ -801,6 +832,30 @@ describe('CaseUserActionService', () => { references: [{ id: '2', name: 'associated-cases', type: 'cases' }], type: 'cases-user-actions', }, + { + attributes: { + action: 'update', + created_at: '2022-01-09T22:00:00.000Z', + created_by: { + email: 'elastic@elastic.co', + full_name: 'Elastic User', + username: 'elastic', + }, + owner: 'securitySolution', + payload: { + severity: 'critical', + }, + type: 'severity', + }, + references: [ + { + id: '2', + name: 'associated-cases', + type: 'cases', + }, + ], + type: 'cases-user-actions', + }, ]); }); }); diff --git a/x-pack/plugins/cases/server/services/user_actions/mocks.ts b/x-pack/plugins/cases/server/services/user_actions/mocks.ts index c745c040ac2ce..bc35f98bf926e 100644 --- a/x-pack/plugins/cases/server/services/user_actions/mocks.ts +++ b/x-pack/plugins/cases/server/services/user_actions/mocks.ts @@ -7,7 +7,7 @@ import { CASE_SAVED_OBJECT } from '../../../common/constants'; import { SECURITY_SOLUTION_OWNER } from '../../../common'; -import { CaseStatuses, CommentType, ConnectorTypes } from '../../../common/api'; +import { CaseSeverity, CaseStatuses, CommentType, ConnectorTypes } from '../../../common/api'; import { createCaseSavedObjectResponse } from '../test_utils'; import { transformSavedObjectToExternalModel } from '../cases/transform'; @@ -30,6 +30,7 @@ export const casePayload = { }, }, settings: { syncAlerts: true }, + severity: CaseSeverity.LOW, owner: SECURITY_SOLUTION_OWNER, }; @@ -69,6 +70,7 @@ export const updatedCases = [ description: 'updated desc', tags: ['one', 'two'], settings: { syncAlerts: false }, + severity: CaseSeverity.CRITICAL, }, references: [], }, diff --git a/x-pack/plugins/cases/server/services/user_actions/types.ts b/x-pack/plugins/cases/server/services/user_actions/types.ts index f681a9186181c..a60dee552a6be 100644 --- a/x-pack/plugins/cases/server/services/user_actions/types.ts +++ b/x-pack/plugins/cases/server/services/user_actions/types.ts @@ -9,6 +9,7 @@ import { SavedObjectReference } from '@kbn/core/server'; import { CasePostRequest, CaseSettings, + CaseSeverity, CaseStatuses, CommentUserAction, ConnectorUserAction, @@ -28,6 +29,9 @@ export interface BuilderParameters { status: { parameters: { payload: { status: CaseStatuses } }; }; + severity: { + parameters: { payload: { severity: CaseSeverity } }; + }; tags: { parameters: { payload: { tags: string[] } }; }; diff --git a/x-pack/test/cases_api_integration/common/lib/mock.ts b/x-pack/test/cases_api_integration/common/lib/mock.ts index 34f1d4a9273d2..08f30f8df024e 100644 --- a/x-pack/test/cases_api_integration/common/lib/mock.ts +++ b/x-pack/test/cases_api_integration/common/lib/mock.ts @@ -17,6 +17,7 @@ import { CaseStatuses, CommentRequest, CommentRequestActionsType, + CaseSeverity, } from '@kbn/cases-plugin/common/api'; export const defaultUser = { email: null, full_name: null, username: 'elastic' }; @@ -86,6 +87,7 @@ export const postCaseResp = ( ...(id != null ? { id } : {}), comments: [], duration: null, + severity: req.severity ?? CaseSeverity.LOW, totalAlerts: 0, totalComment: 0, closed_by: null, diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/import_export.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/import_export.ts index 44da07a845ff7..2ce441c37e687 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/import_export.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/import_export.ts @@ -27,6 +27,7 @@ import { CommentUserAction, CreateCaseUserAction, CaseStatuses, + CaseSeverity, } from '@kbn/cases-plugin/common/api'; import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; import { @@ -204,6 +205,10 @@ const expectExportToHaveCaseSavedObject = ( expect(createdCaseSO.attributes.connector.name).to.eql(caseRequest.connector.name); expect(createdCaseSO.attributes.connector.fields).to.eql([]); expect(createdCaseSO.attributes.settings).to.eql(caseRequest.settings); + expect(createdCaseSO.attributes.status).to.eql(CaseStatuses.open); + expect(createdCaseSO.attributes.severity).to.eql(CaseSeverity.LOW); + expect(createdCaseSO.attributes.duration).to.eql(null); + expect(createdCaseSO.attributes.tags).to.eql(caseRequest.tags); }; const expectExportToHaveUserActions = (objects: SavedObject[], caseRequest: CasePostRequest) => { @@ -239,6 +244,7 @@ const expectCaseCreateUserAction = ( expect(restParsedCreateCase).to.eql({ ...restCreateCase, status: CaseStatuses.open, + severity: CaseSeverity.LOW, }); expect(restParsedConnector).to.eql(restConnector); }; diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/migrations.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/migrations.ts index 3c43ac1932986..4d4b9d45b6717 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/migrations.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/migrations.ts @@ -369,7 +369,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { }); }); - describe('8.3.0 adding duration', () => { + describe('8.3.0', () => { before(async () => { await kibanaServer.importExport.load( 'x-pack/test/functional/fixtures/kbn_archiver/cases/8.2.0/cases_duration.json' @@ -383,34 +383,48 @@ export default function createGetTests({ getService }: FtrProviderContext) { await deleteAllCaseItems(es); }); - it('calculates the correct duration for closed cases', async () => { - const caseInfo = await getCase({ - supertest, - caseId: '4537b380-a512-11ec-b92f-859b9e89e434', + describe('adding duration', () => { + it('calculates the correct duration for closed cases', async () => { + const caseInfo = await getCase({ + supertest, + caseId: '4537b380-a512-11ec-b92f-859b9e89e434', + }); + + expect(caseInfo).to.have.property('duration'); + expect(caseInfo.duration).to.be(120); }); - expect(caseInfo).to.have.property('duration'); - expect(caseInfo.duration).to.be(120); - }); + it('sets the duration to null to open cases', async () => { + const caseInfo = await getCase({ + supertest, + caseId: '7537b580-a512-11ec-b94f-85979e89e434', + }); - it('sets the duration to null to open cases', async () => { - const caseInfo = await getCase({ - supertest, - caseId: '7537b580-a512-11ec-b94f-85979e89e434', + expect(caseInfo).to.have.property('duration'); + expect(caseInfo.duration).to.be(null); }); - expect(caseInfo).to.have.property('duration'); - expect(caseInfo.duration).to.be(null); - }); + it('sets the duration to null to in-progress cases', async () => { + const caseInfo = await getCase({ + supertest, + caseId: '1537b580-a512-11ec-b94f-85979e89e434', + }); - it('sets the duration to null to in-progress cases', async () => { - const caseInfo = await getCase({ - supertest, - caseId: '1537b580-a512-11ec-b94f-85979e89e434', + expect(caseInfo).to.have.property('duration'); + expect(caseInfo.duration).to.be(null); }); + }); - expect(caseInfo).to.have.property('duration'); - expect(caseInfo.duration).to.be(null); + describe('add severity', () => { + it('adds the severity field for existing documents', async () => { + const caseInfo = await getCase({ + supertest, + caseId: '4537b380-a512-11ec-b92f-859b9e89e434', + }); + + expect(caseInfo).to.have.property('severity'); + expect(caseInfo.severity).to.be('low'); + }); }); }); }); diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts index 9ef1c3d5655e4..80dffef7cd3ee 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts @@ -10,6 +10,7 @@ import { ALERT_WORKFLOW_STATUS } from '@kbn/rule-data-utils'; import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '@kbn/security-solution-plugin/common/constants'; import { + CaseSeverity, CasesResponse, CaseStatuses, CommentType, @@ -170,6 +171,34 @@ export default ({ getService }: FtrProviderContext): void => { }); }); + it('should patch the severity of a case correctly', async () => { + const postedCase = await createCase(supertest, postCaseReq); + + // the default severity + expect(postedCase.severity).equal(CaseSeverity.LOW); + + const patchedCases = await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + severity: CaseSeverity.MEDIUM, + }, + ], + }, + }); + + const data = removeServerGeneratedPropertiesFromCase(patchedCases[0]); + + expect(data).to.eql({ + ...postCaseResp(), + severity: CaseSeverity.MEDIUM, + updated_by: defaultUser, + }); + }); + it('should patch a case with new connector', async () => { const postedCase = await createCase(supertest, postCaseReq); const patchedCases = await updateCase({ @@ -297,6 +326,22 @@ export default ({ getService }: FtrProviderContext): void => { }); }); + it('400s when a wrong severity value is passed', async () => { + await updateCase({ + supertest, + params: { + cases: [ + { + version: 'version', + // @ts-expect-error + severity: 'wont-do', + }, + ], + }, + expectedHttpCode: 400, + }); + }); + it('400s when id is missing', async () => { await updateCase({ supertest, diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/post_case.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/post_case.ts index 9cd986a032b24..d4b52ff6f3394 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/post_case.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/post_case.ts @@ -12,6 +12,7 @@ import { ConnectorTypes, ConnectorJiraTypeFields, CaseStatuses, + CaseSeverity, } from '@kbn/cases-plugin/common/api'; import { getPostCaseRequest, postCaseResp, defaultUser } from '../../../../common/lib/mock'; import { @@ -102,6 +103,32 @@ export default ({ getService }: FtrProviderContext): void => { ); }); + it('should post a case without severity', async () => { + const postedCase = await createCase(supertest, getPostCaseRequest()); + const data = removeServerGeneratedPropertiesFromCase(postedCase); + + expect(data).to.eql(postCaseResp(null, getPostCaseRequest())); + }); + + it('should post a case with severity', async () => { + const postedCase = await createCase( + supertest, + getPostCaseRequest({ + severity: CaseSeverity.HIGH, + }) + ); + const data = removeServerGeneratedPropertiesFromCase(postedCase); + + expect(data).to.eql( + postCaseResp( + null, + getPostCaseRequest({ + severity: CaseSeverity.HIGH, + }) + ) + ); + }); + it('should create a user action when creating a case', async () => { const postedCase = await createCase(supertest, getPostCaseRequest()); const userActions = await getCaseUserActions({ supertest, caseID: postedCase.id }); @@ -122,6 +149,7 @@ export default ({ getService }: FtrProviderContext): void => { settings: postedCase.settings, owner: postedCase.owner, status: CaseStatuses.open, + severity: CaseSeverity.LOW, }, }); }); @@ -207,6 +235,11 @@ export default ({ getService }: FtrProviderContext): void => { await supertest.post(CASES_URL).set('kbn-xsrf', 'true').send(caseWithoutTags).expect(400); }); + it('400s when passing a wrong severity value', async () => { + // @ts-expect-error + await createCase(supertest, { ...getPostCaseRequest(), severity: 'very-severe' }, 400); + }); + it('400s if you passing status for a new case', async () => { const req = getPostCaseRequest(); diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts index 283e6b0c5301b..aacb5f6c8ae17 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts @@ -9,6 +9,7 @@ import expect from '@kbn/expect'; import { CaseResponse, + CaseSeverity, CaseStatuses, CommentType, ConnectorTypes, @@ -106,6 +107,30 @@ export default ({ getService }: FtrProviderContext): void => { expect(statusUserAction.payload).to.eql({ status: 'closed' }); }); + it('creates a severity update user action when changing the severity', async () => { + const theCase = await createCase(supertest, postCaseReq); + await updateCase({ + supertest, + params: { + cases: [ + { + id: theCase.id, + version: theCase.version, + severity: CaseSeverity.HIGH, + }, + ], + }, + }); + + const userActions = await getCaseUserActions({ supertest, caseID: theCase.id }); + const statusUserAction = userActions[1]; + + expect(userActions.length).to.eql(2); + expect(statusUserAction.type).to.eql('severity'); + expect(statusUserAction.action).to.eql('update'); + expect(statusUserAction.payload).to.eql({ severity: 'high' }); + }); + it('creates a connector update user action', async () => { const newConnector = { id: '123',