diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/action/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/action/v1.ts index d4de4ad549da6..cbff90a3b79f6 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/action/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/action/v1.ts @@ -26,6 +26,7 @@ export const UserActionTypes = { delete_case: 'delete_case', category: 'category', customFields: 'customFields', + observables: 'observables', } as const; type UserActionActionTypeKeys = keyof typeof UserActionTypes; diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/observables/latest.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/observables/latest.ts new file mode 100644 index 0000000000000..25300c97a6d2e --- /dev/null +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/observables/latest.ts @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export * from './v1'; diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/observables/v1.test.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/observables/v1.test.ts new file mode 100644 index 0000000000000..8986e498db8fd --- /dev/null +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/observables/v1.test.ts @@ -0,0 +1,90 @@ +/* + * 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 { UserActionTypes } from '../action/v1'; +import { ObservablesUserActionPayloadRt, ObservablesUserActionRt } from './v1'; + +describe('Observables', () => { + describe('ObservablesUserActionPayloadRt', () => { + const defaultRequest = { + observables: { + count: 1, + actionType: 'add', + }, + }; + + it('has expected attributes in request', () => { + const query = ObservablesUserActionPayloadRt.decode(defaultRequest); + + expect(query).toStrictEqual({ + _tag: 'Right', + right: defaultRequest, + }); + }); + + it('removes foo:bar attributes from request', () => { + const query = ObservablesUserActionPayloadRt.decode({ ...defaultRequest, foo: 'bar' }); + + expect(query).toStrictEqual({ + _tag: 'Right', + right: defaultRequest, + }); + }); + + it('removes foo:bar attributes from observables', () => { + const query = ObservablesUserActionPayloadRt.decode({ + observables: { ...defaultRequest.observables, foo: 'bar' }, + }); + + expect(query).toStrictEqual({ + _tag: 'Right', + right: defaultRequest, + }); + }); + }); + describe('ObservablesUserActionRt', () => { + const defaultRequest = { + type: UserActionTypes.observables, + payload: { + observables: { + count: 1, + actionType: 'add', + }, + }, + }; + + it('has expected attributes in request', () => { + const query = ObservablesUserActionRt.decode(defaultRequest); + + expect(query).toStrictEqual({ + _tag: 'Right', + right: defaultRequest, + }); + }); + + it('removes foo:bar attributes from request', () => { + const query = ObservablesUserActionRt.decode({ ...defaultRequest, foo: 'bar' }); + + expect(query).toStrictEqual({ + _tag: 'Right', + right: defaultRequest, + }); + }); + + it('removes foo:bar attributes from payload', () => { + const query = ObservablesUserActionRt.decode({ + ...defaultRequest, + payload: { ...defaultRequest.payload, foo: 'bar' }, + }); + + expect(query).toStrictEqual({ + _tag: 'Right', + right: defaultRequest, + }); + }); + }); +}); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/observables/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/observables/v1.ts new file mode 100644 index 0000000000000..5c3c77dd99922 --- /dev/null +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/observables/v1.ts @@ -0,0 +1,29 @@ +/* + * 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 { UserActionTypes } from '../action/v1'; + +const ObservablesActionTypeRt = rt.union([ + rt.literal('add'), + rt.literal('delete'), + rt.literal('update'), +]); + +export const ObservablePayloadRt = rt.strict({ + count: rt.number, + actionType: ObservablesActionTypeRt, +}); + +export const ObservablesUserActionPayloadRt = rt.strict({ observables: ObservablePayloadRt }); + +export const ObservablesUserActionRt = rt.strict({ + type: rt.literal(UserActionTypes.observables), + payload: ObservablesUserActionPayloadRt, +}); + +export type ObservablesActionType = rt.TypeOf; diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/v1.ts index fdef2a9530e54..9a8a388f03f2b 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/v1.ts @@ -23,7 +23,7 @@ import { StatusUserActionRt } from './status/v1'; import { TagsUserActionRt } from './tags/v1'; import { TitleUserActionRt } from './title/v1'; import { CustomFieldsUserActionRt } from './custom_fields/v1'; - +import { ObservablesUserActionRt } from './observables/v1'; export { UserActionTypes, UserActionActions } from './action/v1'; export { StatusUserActionRt } from './status/v1'; @@ -61,6 +61,7 @@ const BasicUserActionsRt = rt.union([ DeleteCaseUserActionRt, CategoryUserActionRt, CustomFieldsUserActionRt, + ObservablesUserActionRt, ]); const CommonUserActionsWithIdsRt = rt.union([BasicUserActionsRt, CommentUserActionRt]); @@ -154,3 +155,4 @@ export type CreateCaseUserActionWithoutConnectorId = UserActionWithAttributes< rt.TypeOf >; export type CustomFieldsUserAction = UserAction>; +export type ObservablesUserAction = UserAction>; diff --git a/x-pack/platform/plugins/shared/cases/public/components/case_view/translations.ts b/x-pack/platform/plugins/shared/cases/public/components/case_view/translations.ts index 06bab42be059a..e6fbbde0d71d7 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/case_view/translations.ts +++ b/x-pack/platform/plugins/shared/cases/public/components/case_view/translations.ts @@ -247,3 +247,24 @@ export const TOTAL_USERS_ASSIGNED = (total: number) => defaultMessage: '{total} assigned', values: { total }, }); + +export const ADDED_OBSERVABLES = (totalObservables: number): string => + i18n.translate('xpack.cases.caseView.observables.addedObservables', { + values: { totalObservables }, + defaultMessage: + 'added {totalObservables, plural, =1 {an} other {{totalObservables}}} {totalObservables, plural, =1 {observable} other {observables}}', + }); + +export const DELETED_OBSERVABLES = (totalObservables: number): string => + i18n.translate('xpack.cases.caseView.observables.deletedObservables', { + values: { totalObservables }, + defaultMessage: + 'deleted {totalObservables, plural, =1 {an} other {{totalObservables}}} {totalObservables, plural, =1 {observable} other {observables}}', + }); + +export const UPDATED_OBSERVABLES = (totalObservables: number): string => + i18n.translate('xpack.cases.caseView.observables.updatedObservables', { + values: { totalObservables }, + defaultMessage: + 'updated {totalObservables, plural, =1 {an} other {{totalObservables}}} {totalObservables, plural, =1 {observable} other {observables}}', + }); diff --git a/x-pack/platform/plugins/shared/cases/public/components/user_actions/builder.tsx b/x-pack/platform/plugins/shared/cases/public/components/user_actions/builder.tsx index b0b3a9f7a7de9..197f0f52e315a 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/user_actions/builder.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/user_actions/builder.tsx @@ -19,6 +19,7 @@ import { createCaseUserActionBuilder } from './create_case'; import type { UserActionBuilderMap } from './types'; import { createCategoryUserActionBuilder } from './category'; import { createCustomFieldsUserActionBuilder } from './custom_fields/custom_fields'; +import { createObservablesUserActionBuilder } from './observables'; export const builderMap: UserActionBuilderMap = { create_case: createCaseUserActionBuilder, @@ -34,4 +35,5 @@ export const builderMap: UserActionBuilderMap = { assignees: createAssigneesUserActionBuilder, category: createCategoryUserActionBuilder, customFields: createCustomFieldsUserActionBuilder, + observables: createObservablesUserActionBuilder, }; diff --git a/x-pack/platform/plugins/shared/cases/public/components/user_actions/observables.test.tsx b/x-pack/platform/plugins/shared/cases/public/components/user_actions/observables.test.tsx new file mode 100644 index 0000000000000..3bef4352db63d --- /dev/null +++ b/x-pack/platform/plugins/shared/cases/public/components/user_actions/observables.test.tsx @@ -0,0 +1,53 @@ +/* + * 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 React from 'react'; +import { EuiCommentList } from '@elastic/eui'; +import { screen } from '@testing-library/react'; +import { UserActionActions } from '../../../common/types/domain'; + +import { renderWithTestingProviders } from '../../common/mock'; +import { getUserAction } from '../../containers/mock'; +import { getMockBuilderArgs } from './mock'; +import { createObservablesUserActionBuilder } from './observables'; +import type { ObservablesActionType } from '../../../common/types/domain/user_action/observables/v1'; + +jest.mock('../../common/lib/kibana'); +jest.mock('../../common/navigation/hooks'); + +describe('createObservablesUserActionBuilder ', () => { + const builderArgs = getMockBuilderArgs(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + const tests: [number, ObservablesActionType, string][] = [ + [1, 'add', 'added an observable'], + [1, 'delete', 'deleted an observable'], + [1, 'update', 'updated an observable'], + [10, 'add', 'added 10 observables'], + ]; + + it.each(tests)( + 'renders correctly when changed observables to %s', + async (count, actionType, label) => { + const userAction = getUserAction('observables', UserActionActions.update, { + payload: { observables: { count, actionType } }, + }); + const builder = createObservablesUserActionBuilder({ + ...builderArgs, + userAction, + }); + + const createdUserAction = builder.build(); + renderWithTestingProviders(); + + expect(screen.getByTestId(`observables-${actionType}-action`)).toHaveTextContent(label); + } + ); +}); diff --git a/x-pack/platform/plugins/shared/cases/public/components/user_actions/observables.tsx b/x-pack/platform/plugins/shared/cases/public/components/user_actions/observables.tsx new file mode 100644 index 0000000000000..7e5ae01eb7f33 --- /dev/null +++ b/x-pack/platform/plugins/shared/cases/public/components/user_actions/observables.tsx @@ -0,0 +1,63 @@ +/* + * 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 React, { type ReactNode } from 'react'; +import { EuiText } from '@elastic/eui'; +import type { SnakeToCamelCase } from '../../../common/types'; +import type { ObservablesUserAction } from '../../../common/types/domain'; +import type { UserActionBuilder } from './types'; + +import { createCommonUpdateUserActionBuilder } from './common'; +import { ADDED_OBSERVABLES, DELETED_OBSERVABLES, UPDATED_OBSERVABLES } from './translations'; +import type { ObservablesActionType } from '../../../common/types/domain/user_action/observables/v1'; + +const getLabel: (actionType: ObservablesActionType, count: number) => ReactNode = ( + actionType, + count +) => { + let label = ''; + switch (actionType) { + case 'add': + label = ADDED_OBSERVABLES(count); + break; + case 'delete': + label = DELETED_OBSERVABLES(count); + break; + case 'update': + label = UPDATED_OBSERVABLES(count); + break; + } + return ( + + {label} + + ); +}; +export const createObservablesUserActionBuilder: UserActionBuilder = ({ + userAction, + userProfiles, + handleOutlineComment, +}) => ({ + build: () => { + const action = userAction as SnakeToCamelCase; + const { count, actionType } = action?.payload?.observables; + const label = getLabel(actionType, count); + + if (count > 0) { + const commonBuilder = createCommonUpdateUserActionBuilder({ + userProfiles, + userAction, + handleOutlineComment, + label, + icon: 'dot', + }); + + return commonBuilder.build(); + } + return []; + }, +}); diff --git a/x-pack/platform/plugins/shared/cases/public/components/user_actions/settings.test.tsx b/x-pack/platform/plugins/shared/cases/public/components/user_actions/settings.test.tsx index 04139943d7d85..80943b13cc402 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/user_actions/settings.test.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/user_actions/settings.test.tsx @@ -18,7 +18,7 @@ import { createSettingsUserActionBuilder } from './settings'; jest.mock('../../common/lib/kibana'); jest.mock('../../common/navigation/hooks'); -describe('createStatusUserActionBuilder ', () => { +describe('createSettingsUserActionBuilder ', () => { const builderArgs = getMockBuilderArgs(); beforeEach(() => { diff --git a/x-pack/platform/plugins/shared/cases/public/components/user_actions/translations.ts b/x-pack/platform/plugins/shared/cases/public/components/user_actions/translations.ts index e57bd98ca6a42..e4e45d531b262 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/user_actions/translations.ts +++ b/x-pack/platform/plugins/shared/cases/public/components/user_actions/translations.ts @@ -151,6 +151,10 @@ export const CUSTOM_FIELDS = i18n.translate('xpack.cases.caseView.userActions.cu defaultMessage: 'Custom Fields', }); +export const OBSERVABLES = i18n.translate('xpack.cases.caseView.userActions.observables', { + defaultMessage: 'Observables', +}); + export const USER_ACTION_EDITED = (type: string) => i18n.translate('xpack.cases.caseView.userActions.edited', { values: { type }, diff --git a/x-pack/platform/plugins/shared/cases/public/components/user_actions/user_actions_aria_labels.tsx b/x-pack/platform/plugins/shared/cases/public/components/user_actions/user_actions_aria_labels.tsx index 84a0f2b41fcdd..c889b76b31d38 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/user_actions/user_actions_aria_labels.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/user_actions/user_actions_aria_labels.tsx @@ -24,6 +24,7 @@ export const getUserActionAriaLabel = (type: keyof typeof UserActionTypes) => { delete_case: i18n.CASE_DELETED, category: i18n.CATEGORY, customFields: i18n.CUSTOM_FIELDS, + observables: i18n.OBSERVABLES, }; switch (type) { diff --git a/x-pack/platform/plugins/shared/cases/server/client/cases/observables.test.ts b/x-pack/platform/plugins/shared/cases/server/client/cases/observables.test.ts index 9fd03b05deeaf..d0e74aff0fe1c 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/cases/observables.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/cases/observables.test.ts @@ -20,6 +20,7 @@ import { MAX_OBSERVABLES_PER_CASE, } from '../../../common/constants'; import type { ObservablePost } from '../../../common/types/api'; +import { UserActionTypes } from '../../../common/types/domain/user_action/v1'; const caseSO = mockCases[0]; @@ -28,6 +29,7 @@ const mockClientArgs = createCasesClientMockArgs(); const mockLicensingService = mockClientArgs.services.licensingService; const mockCaseService = mockClientArgs.services.caseService; +const mockUserActionService = mockClientArgs.services.userActionService; const mockObservablePost = { value: '127.0.0.1', @@ -150,6 +152,26 @@ describe('addObservable', () => { ) ).rejects.toThrow(); }); + + it('should create a user action with the correct payload', async () => { + mockLicensingService.isAtLeastPlatinum.mockResolvedValue(true); + await addObservable( + caseSO.id, + { observable: { typeKey: OBSERVABLE_TYPE_IPV4.key, value: '127.0.0.1', description: '' } }, + mockClientArgs, + mockCasesClient + ); + + expect(mockUserActionService.creator.createUserAction).toHaveBeenCalledWith({ + userAction: { + type: UserActionTypes.observables, + caseId: caseSO.id, + owner: 'securitySolution', + user: mockClientArgs.user, + payload: { observables: { count: 1, actionType: 'add' } }, + }, + }); + }); }); describe('updateObservable', () => { @@ -239,6 +261,27 @@ describe('updateObservable', () => { ) ).rejects.toThrow(); }); + + it('should create a user action with the correct payload', async () => { + mockLicensingService.isAtLeastPlatinum.mockResolvedValue(true); + await updateObservable( + caseSO.id, + mockObservable.id, + { observable: { value: '192.168.0.1', description: 'Updated description' } }, + mockClientArgs, + mockCasesClient + ); + + expect(mockUserActionService.creator.createUserAction).toHaveBeenCalledWith({ + userAction: { + type: UserActionTypes.observables, + caseId: caseSO.id, + owner: 'securitySolution', + user: mockClientArgs.user, + payload: { observables: { count: 1, actionType: 'update' } }, + }, + }); + }); }); describe('deleteObservable', () => { @@ -277,6 +320,21 @@ describe('deleteObservable', () => { deleteObservable('case-id', 'observable-id', mockClientArgs, mockCasesClient) ).rejects.toThrow(); }); + + it('should create a user action with the correct payload', async () => { + mockLicensingService.isAtLeastPlatinum.mockResolvedValue(true); + await deleteObservable(caseSO.id, mockObservable.id, mockClientArgs, mockCasesClient); + + expect(mockUserActionService.creator.createUserAction).toHaveBeenCalledWith({ + userAction: { + type: UserActionTypes.observables, + caseId: caseSO.id, + owner: 'securitySolution', + user: mockClientArgs.user, + payload: { observables: { count: 1, actionType: 'delete' } }, + }, + }); + }); }); describe('bulkAddObservables', () => { @@ -380,4 +438,29 @@ describe('bulkAddObservables', () => { }) ); }); + + it('should create a user action with the correct payload', async () => { + mockLicensingService.isAtLeastPlatinum.mockResolvedValue(true); + await bulkAddObservables( + { + caseId: caseSO.id, + observables: [ + { ...mockObservablePost, value: 'ip2' }, + { ...mockObservablePost, value: 'ip3' }, + ], + }, + mockClientArgs, + mockCasesClient + ); + + expect(mockUserActionService.creator.createUserAction).toHaveBeenCalledWith({ + userAction: { + type: UserActionTypes.observables, + caseId: caseSO.id, + owner: 'securitySolution', + user: mockClientArgs.user, + payload: { observables: { count: 2, actionType: 'add' } }, + }, + }); + }); }); diff --git a/x-pack/platform/plugins/shared/cases/server/client/cases/observables.ts b/x-pack/platform/plugins/shared/cases/server/client/cases/observables.ts index 2ecd22d5abb9a..697057ed1ba32 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/cases/observables.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/cases/observables.ts @@ -11,7 +11,7 @@ import Boom from '@hapi/boom'; import { MAX_OBSERVABLES_PER_CASE } from '../../../common/constants'; import type { Observable } from '../../../common/types/domain'; -import { CaseRt } from '../../../common/types/domain'; +import { CaseRt, UserActionTypes } from '../../../common/types/domain'; import { AddObservableRequestRt, type AddObservableRequest, @@ -58,8 +58,9 @@ export const addObservable = async ( casesClient: CasesClient ) => { const { - services: { caseService, licensingService }, + services: { caseService, licensingService, userActionService }, authorization, + user, } = clientArgs; const hasPlatinumLicenseOrGreater = await licensingService.isAtLeastPlatinum(); @@ -112,6 +113,18 @@ export const addObservable = async ( }, }); + await userActionService.creator.createUserAction({ + userAction: { + type: UserActionTypes.observables, + caseId: retrievedCase.id, + owner: retrievedCase.attributes.owner, + user, + payload: { + observables: { count: 1, actionType: 'add' }, + }, + }, + }); + const res = flattenCaseSavedObject({ savedObject: { ...retrievedCase, @@ -135,8 +148,9 @@ export const updateObservable = async ( casesClient: CasesClient ) => { const { - services: { caseService, licensingService }, + services: { caseService, licensingService, userActionService }, authorization, + user, } = clientArgs; const hasPlatinumLicenseOrGreater = await licensingService.isAtLeastPlatinum(); @@ -188,6 +202,18 @@ export const updateObservable = async ( }, }); + await userActionService.creator.createUserAction({ + userAction: { + type: UserActionTypes.observables, + caseId: retrievedCase.id, + owner: retrievedCase.attributes.owner, + user, + payload: { + observables: { count: 1, actionType: 'update' }, + }, + }, + }); + const res = flattenCaseSavedObject({ savedObject: { ...retrievedCase, @@ -210,8 +236,9 @@ export const deleteObservable = async ( casesClient: CasesClient ) => { const { - services: { caseService, licensingService }, + services: { caseService, licensingService, userActionService }, authorization, + user, } = clientArgs; const hasPlatinumLicenseOrGreater = await licensingService.isAtLeastPlatinum(); @@ -242,6 +269,17 @@ export const deleteObservable = async ( originalCase: retrievedCase, updatedAttributes: { observables: updatedObservables }, }); + await userActionService.creator.createUserAction({ + userAction: { + type: UserActionTypes.observables, + caseId: retrievedCase.id, + owner: retrievedCase.attributes.owner, + user, + payload: { + observables: { count: 1, actionType: 'delete' }, + }, + }, + }); } catch (error) { throw Boom.badRequest(`Failed to delete observable id: ${observableId}: ${error}`); } @@ -253,8 +291,9 @@ export const bulkAddObservables = async ( casesClient: CasesClient ) => { const { - services: { caseService, licensingService }, + services: { caseService, licensingService, userActionService }, authorization, + user, } = clientArgs; const hasPlatinumLicenseOrGreater = await licensingService.isAtLeastPlatinum(); @@ -304,6 +343,20 @@ export const bulkAddObservables = async ( }, }); + const newObservablesCount = finalObservables.length - currentObservables.length; + + await userActionService.creator.createUserAction({ + userAction: { + type: UserActionTypes.observables, + caseId: retrievedCase.id, + owner: retrievedCase.attributes.owner, + user, + payload: { + observables: { count: newObservablesCount, actionType: 'add' }, + }, + }, + }); + const res = flattenCaseSavedObject({ savedObject: { ...retrievedCase, diff --git a/x-pack/platform/plugins/shared/cases/server/services/user_actions/builder_factory.test.ts b/x-pack/platform/plugins/shared/cases/server/services/user_actions/builder_factory.test.ts index 9473370fe7e36..260c0c058925a 100644 --- a/x-pack/platform/plugins/shared/cases/server/services/user_actions/builder_factory.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/services/user_actions/builder_factory.test.ts @@ -777,6 +777,43 @@ describe('UserActionBuilder', () => { } `); }); + + it('builds an add observables user action correctly', () => { + const builder = builderFactory.getBuilder(UserActionTypes.observables)!; + const userAction = builder.build({ + payload: { observables: { count: 1, actionType: 'add' } }, + ...commonArgs, + }); + + expect(userAction!.parameters).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "action": "create", + "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 { + "observables": Object { + "actionType": "add", + "count": 1, + }, + }, + "type": "observables", + }, + "references": Array [ + Object { + "id": "123", + "name": "associated-cases", + "type": "cases", + }, + ], + } + `); + }); }); describe('eventDetails', () => { @@ -1239,5 +1276,26 @@ describe('UserActionBuilder', () => { `"User updated the category for case id: 123 - user action id: 123"` ); }); + + it('builds an update observables user action correctly', () => { + const builder = builderFactory.getBuilder(UserActionTypes.observables)!; + const userAction = builder.build({ + payload: { observables: { count: 1, actionType: 'update' } }, + ...commonArgs, + }); + + expect(userAction!.eventDetails).toMatchInlineSnapshot(` + Object { + "action": "create", + "descriptiveAction": "case_user_action_observables", + "getMessage": [Function], + "savedObjectId": "123", + "savedObjectType": "cases", + } + `); + expect(userAction!.eventDetails.getMessage('123')).toMatchInlineSnapshot( + `"User added observables to case id: 123 - user action id: 123"` + ); + }); }); }); diff --git a/x-pack/platform/plugins/shared/cases/server/services/user_actions/builder_factory.ts b/x-pack/platform/plugins/shared/cases/server/services/user_actions/builder_factory.ts index 53a19dccd11bd..fd9645244dfff 100644 --- a/x-pack/platform/plugins/shared/cases/server/services/user_actions/builder_factory.ts +++ b/x-pack/platform/plugins/shared/cases/server/services/user_actions/builder_factory.ts @@ -23,6 +23,7 @@ import { AssigneesUserActionBuilder } from './builders/assignees'; import { NoopUserActionBuilder } from './builders/noop'; import { CategoryUserActionBuilder } from './builders/category'; import { CustomFieldsUserActionBuilder } from './builders/custom_fields'; +import { ObservablesUserActionBuilder } from './builders/observables'; const builderMap = { assignees: AssigneesUserActionBuilder, @@ -39,6 +40,7 @@ const builderMap = { settings: SettingsUserActionBuilder, delete_case: NoopUserActionBuilder, customFields: CustomFieldsUserActionBuilder, + observables: ObservablesUserActionBuilder, }; export class BuilderFactory { diff --git a/x-pack/platform/plugins/shared/cases/server/services/user_actions/builders/observables.ts b/x-pack/platform/plugins/shared/cases/server/services/user_actions/builders/observables.ts new file mode 100644 index 0000000000000..7bf743e62a475 --- /dev/null +++ b/x-pack/platform/plugins/shared/cases/server/services/user_actions/builders/observables.ts @@ -0,0 +1,42 @@ +/* + * 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 { UserActionActions, UserActionTypes } from '../../../../common/types/domain'; +import { CASE_SAVED_OBJECT } from '../../../../common/constants'; +import { UserActionBuilder } from '../abstract_builder'; +import type { EventDetails, UserActionParameters, UserActionEvent } from '../types'; + +export class ObservablesUserActionBuilder extends UserActionBuilder { + build(args: UserActionParameters<'observables'>): UserActionEvent { + const { caseId } = args; + const action = UserActionActions.create; + + const parameters = this.buildCommonUserAction({ + ...args, + action, + valueKey: 'observables', + value: args.payload.observables, + type: UserActionTypes.observables, + }); + + const getMessage = (id?: string) => + `User added observables to case id: ${caseId} - user action id: ${id}`; + + const eventDetails: EventDetails = { + getMessage, + action, + descriptiveAction: 'case_user_action_observables', + savedObjectId: caseId, + savedObjectType: CASE_SAVED_OBJECT, + }; + + return { + parameters, + eventDetails, + }; + } +} diff --git a/x-pack/platform/plugins/shared/cases/server/services/user_actions/types.ts b/x-pack/platform/plugins/shared/cases/server/services/user_actions/types.ts index 2861361d005f6..05109261db9de 100644 --- a/x-pack/platform/plugins/shared/cases/server/services/user_actions/types.ts +++ b/x-pack/platform/plugins/shared/cases/server/services/user_actions/types.ts @@ -40,6 +40,7 @@ import type { CasePostRequest, UserActionFindRequest, } from '../../../common/types/api'; +import type { ObservablesActionType } from '../../../common/types/domain/user_action/observables/v1'; export interface BuilderParameters { title: { @@ -96,6 +97,13 @@ export interface BuilderParameters { customFields: { parameters: { payload: { customFields: CaseCustomFields } }; }; + observables: { + parameters: { + payload: { + observables: { actionType: ObservablesActionType; count: number }; + }; + }; + }; } export interface CreateUserAction { diff --git a/x-pack/solutions/security/plugins/security_solution/public/cases/index.ts b/x-pack/solutions/security/plugins/security_solution/public/cases/index.ts index d0769ec5ea452..3e7a8c102a6e6 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/cases/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/cases/index.ts @@ -11,7 +11,7 @@ import { routes } from './routes'; export const CASES_FEATURES = { observables: { enabled: true, - autoExtract: false, + autoExtract: true, }, } as const; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/index.tsx index e260a918d39bd..59ce1d0e0f193 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -144,7 +144,7 @@ const casesConfiguration = { featureId: CASES_FEATURE_ID, owner: [APP_ID], syncAlerts: true, - extractObservables: false, + extractObservables: true, }; const emptyInputFilters: Filter[] = [];