[Cases] User actions enhancements (#120342)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Christos Nasikas 2021-12-22 10:27:57 +02:00 committed by GitHub
parent 47abd52f03
commit 61dd51ae01
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
105 changed files with 6463 additions and 4554 deletions

View file

@ -48,7 +48,7 @@ export const caseTypeField = 'type';
const CaseTypeRt = rt.union([rt.literal(CaseType.collection), rt.literal(CaseType.individual)]);
const SettingsRt = rt.type({
export const SettingsRt = rt.type({
syncAlerts: rt.boolean,
});
@ -102,7 +102,7 @@ export const CaseUserActionExternalServiceRt = rt.type({
export const CaseExternalServiceBasicRt = rt.intersection([
rt.type({
connector_id: rt.union([rt.string, rt.null]),
connector_id: rt.string,
}),
CaseUserActionExternalServiceRt,
]);
@ -339,6 +339,7 @@ export type CasesPatchRequest = rt.TypeOf<typeof CasesPatchRequestRt>;
export type CaseFullExternalService = rt.TypeOf<typeof CaseFullExternalServiceRt>;
export type CaseSettings = rt.TypeOf<typeof SettingsRt>;
export type ExternalServiceResponse = rt.TypeOf<typeof ExternalServiceResponseRt>;
export type CaseExternalServiceBasic = rt.TypeOf<typeof CaseExternalServiceBasicRt>;
export type AllTagsFindRequest = rt.TypeOf<typeof AllTagsFindRequestRt>;
export type AllReportersFindRequest = AllTagsFindRequest;

View file

@ -1,69 +0,0 @@
/*
* 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 { OWNER_FIELD } from './constants';
import { UserRT } from '../user';
/* To the next developer, if you add/removed fields here
* make sure to check this file (x-pack/plugins/cases/server/services/user_actions/helpers.ts) too
*/
const UserActionFieldTypeRt = rt.union([
rt.literal('comment'),
rt.literal('connector'),
rt.literal('description'),
rt.literal('pushed'),
rt.literal('tags'),
rt.literal('title'),
rt.literal('status'),
rt.literal('settings'),
rt.literal('sub_case'),
rt.literal(OWNER_FIELD),
]);
const UserActionFieldRt = rt.array(UserActionFieldTypeRt);
const UserActionRt = rt.union([
rt.literal('add'),
rt.literal('create'),
rt.literal('delete'),
rt.literal('update'),
rt.literal('push-to-service'),
]);
const CaseUserActionBasicRT = rt.type({
action_field: UserActionFieldRt,
action: UserActionRt,
action_at: rt.string,
action_by: UserRT,
new_value: rt.union([rt.string, rt.null]),
old_value: rt.union([rt.string, rt.null]),
owner: rt.string,
});
const CaseUserActionResponseRT = rt.intersection([
CaseUserActionBasicRT,
rt.type({
action_id: rt.string,
case_id: rt.string,
comment_id: rt.union([rt.string, rt.null]),
new_val_connector_id: rt.union([rt.string, rt.null]),
old_val_connector_id: rt.union([rt.string, rt.null]),
}),
rt.partial({ sub_case_id: rt.string }),
]);
export const CaseUserActionAttributesRt = CaseUserActionBasicRT;
export const CaseUserActionsResponseRt = rt.array(CaseUserActionResponseRT);
export type CaseUserActionAttributes = rt.TypeOf<typeof CaseUserActionAttributesRt>;
export type CaseUserActionsResponse = rt.TypeOf<typeof CaseUserActionsResponseRt>;
export type CaseUserActionResponse = rt.TypeOf<typeof CaseUserActionResponseRT>;
export type UserAction = rt.TypeOf<typeof UserActionRt>;
export type UserActionField = rt.TypeOf<typeof UserActionFieldRt>;
export type UserActionFieldType = rt.TypeOf<typeof UserActionFieldTypeRt>;

View file

@ -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 { CommentRequestRt } from '../comment';
import { ActionTypes, UserActionWithAttributes } from './common';
export const CommentUserActionPayloadRt = rt.type({ comment: CommentRequestRt });
export const CommentUserActionRt = rt.type({
type: rt.literal(ActionTypes.comment),
payload: CommentUserActionPayloadRt,
});
export type CommentUserAction = UserActionWithAttributes<rt.TypeOf<typeof CommentUserActionRt>>;

View file

@ -0,0 +1,55 @@
/*
* 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 { UserRT } from '../../user';
export const ActionTypes = {
comment: 'comment',
connector: 'connector',
description: 'description',
pushed: 'pushed',
tags: 'tags',
title: 'title',
status: 'status',
settings: 'settings',
create_case: 'create_case',
delete_case: 'delete_case',
} as const;
export const Actions = {
add: 'add',
create: 'create',
delete: 'delete',
update: 'update',
push_to_service: 'push_to_service',
} as const;
/* To the next developer, if you add/removed fields here
* make sure to check this file (x-pack/plugins/cases/server/services/user_actions/helpers.ts) too
*/
export const ActionTypesRt = rt.keyof(ActionTypes);
export const ActionsRt = rt.keyof(Actions);
export const UserActionCommonAttributesRt = rt.type({
created_at: rt.string,
created_by: UserRT,
owner: rt.string,
action: ActionsRt,
});
export const CaseUserActionSavedObjectIdsRt = rt.intersection([
rt.type({
action_id: rt.string,
case_id: rt.string,
comment_id: rt.union([rt.string, rt.null]),
}),
rt.partial({ sub_case_id: rt.string }),
]);
export type UserActionWithAttributes<T> = T & rt.TypeOf<typeof UserActionCommonAttributesRt>;
export type UserActionWithResponse<T> = T & rt.TypeOf<typeof CaseUserActionSavedObjectIdsRt>;

View file

@ -0,0 +1,33 @@
/*
* 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 { CaseUserActionConnectorRt, CaseConnectorRt } from '../../connectors';
import { ActionTypes, UserActionWithAttributes } from './common';
export const ConnectorUserActionPayloadWithoutConnectorIdRt = rt.type({
connector: CaseUserActionConnectorRt,
});
export const ConnectorUserActionPayloadRt = rt.type({
connector: CaseConnectorRt,
});
export const ConnectorUserActionWithoutConnectorIdRt = rt.type({
type: rt.literal(ActionTypes.connector),
payload: ConnectorUserActionPayloadWithoutConnectorIdRt,
});
export const ConnectorUserActionRt = rt.type({
type: rt.literal(ActionTypes.connector),
payload: ConnectorUserActionPayloadRt,
});
export type ConnectorUserAction = UserActionWithAttributes<rt.TypeOf<typeof ConnectorUserActionRt>>;
export type ConnectorUserActionWithoutConnectorId = UserActionWithAttributes<
rt.TypeOf<typeof ConnectorUserActionWithoutConnectorIdRt>
>;

View file

@ -0,0 +1,54 @@
/*
* 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 { ActionTypes, UserActionWithAttributes } from './common';
import {
ConnectorUserActionPayloadRt,
ConnectorUserActionPayloadWithoutConnectorIdRt,
} from './connector';
import { DescriptionUserActionPayloadRt } from './description';
import { SettingsUserActionPayloadRt } from './settings';
import { TagsUserActionPayloadRt } from './tags';
import { TitleUserActionPayloadRt } from './title';
export const CommonFieldsRt = rt.type({
type: rt.literal(ActionTypes.create_case),
});
const CommonPayloadAttributesRt = rt.type({
description: DescriptionUserActionPayloadRt.props.description,
status: rt.string,
tags: TagsUserActionPayloadRt.props.tags,
title: TitleUserActionPayloadRt.props.title,
settings: SettingsUserActionPayloadRt.props.settings,
owner: rt.string,
});
export const CreateCaseUserActionRt = rt.intersection([
CommonFieldsRt,
rt.type({
payload: rt.intersection([ConnectorUserActionPayloadRt, CommonPayloadAttributesRt]),
}),
]);
export const CreateCaseUserActionWithoutConnectorIdRt = rt.intersection([
CommonFieldsRt,
rt.type({
payload: rt.intersection([
ConnectorUserActionPayloadWithoutConnectorIdRt,
CommonPayloadAttributesRt,
]),
}),
]);
export type CreateCaseUserAction = UserActionWithAttributes<
rt.TypeOf<typeof CreateCaseUserActionRt>
>;
export type CreateCaseUserActionWithoutConnectorId = UserActionWithAttributes<
rt.TypeOf<typeof CreateCaseUserActionWithoutConnectorIdRt>
>;

View file

@ -0,0 +1,18 @@
/*
* 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 { ActionTypes, UserActionWithAttributes } from './common';
export const DeleteCaseUserActionRt = rt.type({
type: rt.literal(ActionTypes.delete_case),
payload: rt.type({}),
});
export type DeleteCaseUserAction = UserActionWithAttributes<
rt.TypeOf<typeof DeleteCaseUserActionRt>
>;

View file

@ -0,0 +1,20 @@
/*
* 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 { ActionTypes, UserActionWithAttributes } from './common';
export const DescriptionUserActionPayloadRt = rt.type({ description: rt.string });
export const DescriptionUserActionRt = rt.type({
type: rt.literal(ActionTypes.description),
payload: DescriptionUserActionPayloadRt,
});
export type DescriptionUserAction = UserActionWithAttributes<
rt.TypeOf<typeof DescriptionUserActionRt>
>;

View file

@ -0,0 +1,91 @@
/*
* 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 {
ActionsRt,
UserActionCommonAttributesRt,
CaseUserActionSavedObjectIdsRt,
ActionTypesRt,
} from './common';
import { CreateCaseUserActionRt } from './create_case';
import { DescriptionUserActionRt } from './description';
import { CommentUserActionRt } from './comment';
import { ConnectorUserActionRt } from './connector';
import { PushedUserActionRt } from './pushed';
import { TagsUserActionRt } from './tags';
import { TitleUserActionRt } from './title';
import { SettingsUserActionRt } from './settings';
import { StatusUserActionRt } from './status';
import { DeleteCaseUserActionRt } from './delete_case';
export * from './common';
export * from './comment';
export * from './connector';
export * from './create_case';
export * from './delete_case';
export * from './description';
export * from './pushed';
export * from './settings';
export * from './status';
export * from './tags';
export * from './title';
const CommonUserActionsRt = rt.union([
DescriptionUserActionRt,
CommentUserActionRt,
TagsUserActionRt,
TitleUserActionRt,
SettingsUserActionRt,
StatusUserActionRt,
]);
export const UserActionsRt = rt.union([
CommonUserActionsRt,
CreateCaseUserActionRt,
ConnectorUserActionRt,
PushedUserActionRt,
DeleteCaseUserActionRt,
]);
export const UserActionsWithoutConnectorIdRt = rt.union([
CommonUserActionsRt,
CreateCaseUserActionRt,
ConnectorUserActionRt,
PushedUserActionRt,
DeleteCaseUserActionRt,
]);
const CaseUserActionBasicRt = rt.intersection([UserActionsRt, UserActionCommonAttributesRt]);
const CaseUserActionBasicWithoutConnectorIdRt = rt.intersection([
UserActionsWithoutConnectorIdRt,
UserActionCommonAttributesRt,
]);
const CaseUserActionResponseRt = rt.intersection([
CaseUserActionBasicRt,
CaseUserActionSavedObjectIdsRt,
]);
export const CaseUserActionAttributesRt = CaseUserActionBasicRt;
export const CaseUserActionsResponseRt = rt.array(CaseUserActionResponseRt);
export type CaseUserActionAttributes = rt.TypeOf<typeof CaseUserActionAttributesRt>;
export type CaseUserActionAttributesWithoutConnectorId = rt.TypeOf<
typeof CaseUserActionAttributesRt
>;
export type CaseUserActionsResponse = rt.TypeOf<typeof CaseUserActionsResponseRt>;
export type CaseUserActionResponse = rt.TypeOf<typeof CaseUserActionResponseRt>;
export type UserAction = rt.TypeOf<typeof ActionsRt>;
export type UserActionTypes = rt.TypeOf<typeof ActionTypesRt>;
export type CaseUserAction = rt.TypeOf<typeof CaseUserActionBasicRt>;
export type CaseUserActionWithoutConnectorId = rt.TypeOf<
typeof CaseUserActionBasicWithoutConnectorIdRt
>;

View file

@ -0,0 +1,33 @@
/*
* 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 { CaseUserActionExternalServiceRt, CaseExternalServiceBasicRt } from '../case';
import { ActionTypes, UserActionWithAttributes } from './common';
export const PushedUserActionPayloadWithoutConnectorIdRt = rt.type({
externalService: CaseUserActionExternalServiceRt,
});
export const PushedUserActionPayloadRt = rt.type({
externalService: CaseExternalServiceBasicRt,
});
export const PushedUserActionWithoutConnectorIdRt = rt.type({
type: rt.literal(ActionTypes.pushed),
payload: PushedUserActionPayloadWithoutConnectorIdRt,
});
export const PushedUserActionRt = rt.type({
type: rt.literal(ActionTypes.pushed),
payload: PushedUserActionPayloadRt,
});
export type PushedUserAction = UserActionWithAttributes<rt.TypeOf<typeof PushedUserActionRt>>;
export type PushedUserActionWithoutConnectorId = UserActionWithAttributes<
rt.TypeOf<typeof PushedUserActionWithoutConnectorIdRt>
>;

View file

@ -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 { ActionTypes, UserActionWithAttributes } from './common';
import { SettingsRt } from '../case';
export const SettingsUserActionPayloadRt = rt.type({ settings: SettingsRt });
export const SettingsUserActionRt = rt.type({
type: rt.literal(ActionTypes.settings),
payload: SettingsUserActionPayloadRt,
});
export type SettingsUserAction = UserActionWithAttributes<rt.TypeOf<typeof SettingsUserActionRt>>;

View file

@ -0,0 +1,18 @@
/*
* 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 { ActionTypes, UserActionWithAttributes } from './common';
export const StatusUserActionPayloadRt = rt.type({ status: rt.string });
export const StatusUserActionRt = rt.type({
type: rt.literal(ActionTypes.status),
payload: StatusUserActionPayloadRt,
});
export type StatusUserAction = UserActionWithAttributes<rt.TypeOf<typeof StatusUserActionRt>>;

View file

@ -0,0 +1,18 @@
/*
* 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 { ActionTypes, UserActionWithAttributes } from './common';
export const TagsUserActionPayloadRt = rt.type({ tags: rt.array(rt.string) });
export const TagsUserActionRt = rt.type({
type: rt.literal(ActionTypes.tags),
payload: TagsUserActionPayloadRt,
});
export type TagsUserAction = UserActionWithAttributes<rt.TypeOf<typeof TagsUserActionRt>>;

View file

@ -0,0 +1,18 @@
/*
* 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 { ActionTypes, UserActionWithAttributes } from './common';
export const TitleUserActionPayloadRt = rt.type({ title: rt.string });
export const TitleUserActionRt = rt.type({
type: rt.literal(ActionTypes.title),
payload: TitleUserActionPayloadRt,
});
export type TitleUserAction = UserActionWithAttributes<rt.TypeOf<typeof TitleUserActionRt>>;

View file

@ -73,7 +73,7 @@ const ConnectorNoneTypeFieldsRt = rt.type({
fields: rt.null,
});
export const noneConnectorId: string = 'none';
export const NONE_CONNECTOR_ID: string = 'none';
export const ConnectorTypeFieldsRt = rt.union([
ConnectorJiraTypeFieldsRt,
@ -87,9 +87,13 @@ export const ConnectorTypeFieldsRt = rt.union([
/**
* This type represents the connector's format when it is encoded within a user action.
*/
export const CaseUserActionConnectorRt = rt.intersection([
rt.type({ name: rt.string }),
ConnectorTypeFieldsRt,
export const CaseUserActionConnectorRt = rt.union([
rt.intersection([ConnectorJiraTypeFieldsRt, rt.type({ name: rt.string })]),
rt.intersection([ConnectorNoneTypeFieldsRt, rt.type({ name: rt.string })]),
rt.intersection([ConnectorResilientTypeFieldsRt, rt.type({ name: rt.string })]),
rt.intersection([ConnectorServiceNowITSMTypeFieldsRt, rt.type({ name: rt.string })]),
rt.intersection([ConnectorServiceNowSIRTypeFieldsRt, rt.type({ name: rt.string })]),
rt.intersection([ConnectorSwimlaneTypeFieldsRt, rt.type({ name: rt.string })]),
]);
export const CaseConnectorRt = rt.intersection([

View file

@ -0,0 +1,16 @@
/*
* 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.
*/
type SnakeToCamelCaseString<S extends string> = S extends `${infer T}_${infer U}`
? `${T}${Capitalize<SnakeToCamelCaseString<U>>}`
: S;
export type SnakeToCamelCase<T> = T extends Record<string, unknown>
? {
[K in keyof T as SnakeToCamelCaseString<K & string>]: SnakeToCamelCase<T[K]>;
}
: T;

View file

@ -14,11 +14,12 @@ import {
CaseType,
CommentRequest,
User,
UserAction,
UserActionField,
ActionConnector,
CaseExternalServiceBasic,
CaseUserActionResponse,
CaseMetricsResponse,
} from '../api';
import { SnakeToCamelCase } from '../types';
export interface CasesContextFeatures {
alerts: { sync: boolean };
@ -72,29 +73,9 @@ export type Comment = CommentRequest & {
updatedBy: ElasticUser | null;
version: string;
};
export interface CaseUserActions {
actionId: string;
actionField: UserActionField;
action: UserAction;
actionAt: string;
actionBy: ElasticUser;
caseId: string;
commentId: string | null;
newValue: string | null;
newValConnectorId: string | null;
oldValue: string | null;
oldValConnectorId: string | null;
}
export interface CaseExternalService {
pushedAt: string;
pushedBy: ElasticUser;
connectorId: string;
connectorName: string;
externalId: string;
externalTitle: string;
externalUrl: string;
}
export type CaseUserActions = SnakeToCamelCase<CaseUserActionResponse>;
export type CaseExternalService = SnakeToCamelCase<CaseExternalServiceBasic>;
interface BasicCase {
id: string;

View file

@ -0,0 +1,110 @@
/*
* 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 { omit } from 'lodash';
import { ActionTypes } from '../api';
import {
isConnectorUserAction,
isTitleUserAction,
isStatusUserAction,
isTagsUserAction,
isCommentUserAction,
isDescriptionUserAction,
isPushedUserAction,
isCreateCaseUserAction,
isUserActionType,
} from './user_actions';
describe('user action utils', () => {
const predicateMap = {
[ActionTypes.connector]: isConnectorUserAction,
[ActionTypes.title]: isTitleUserAction,
[ActionTypes.status]: isStatusUserAction,
[ActionTypes.tags]: isTagsUserAction,
[ActionTypes.comment]: isCommentUserAction,
[ActionTypes.description]: isDescriptionUserAction,
};
const tests = (Object.keys(predicateMap) as Array<keyof typeof predicateMap>).map((key) => [key]);
describe.each(tests)('%s', (type) => {
it('returns true if the user action is %s', () => {
const predicate = predicateMap[type];
expect(predicate({ type, payload: { [type]: {} } })).toBe(true);
});
it('returns false if the type is wrong', () => {
const predicate = predicateMap[type];
expect(predicate({ type: 'not-exist', payload: { connector: {} } })).toBe(false);
});
it('returns false if the payload is wrong', () => {
const predicate = predicateMap[type];
expect(predicate({ type: 'not-exist', payload: {} })).toBe(false);
});
});
describe('isPushedUserAction', () => {
it('returns true if the user action is pushed', () => {
expect(
isPushedUserAction({ type: ActionTypes.pushed, payload: { externalService: {} } })
).toBe(true);
});
it('returns false if the type is wrong', () => {
expect(isPushedUserAction({ type: 'not-exist', payload: { connector: {} } })).toBe(false);
});
it('returns false if the payload is wrong', () => {
expect(isPushedUserAction({ type: 'not-exist', payload: {} })).toBe(false);
});
});
describe('isCreateCaseUserAction', () => {
const payloadTests = [...Object.keys(predicateMap), ['settings'], ['owner']];
const payload = {
connector: {},
title: '',
description: '',
tags: [],
settings: {},
status: '',
owner: '',
};
it('returns true if the user action is create_case', () => {
expect(
isCreateCaseUserAction({
type: ActionTypes.create_case,
payload,
})
).toBe(true);
});
it('returns false if the type is wrong', () => {
expect(isCreateCaseUserAction({ type: 'not-exist' })).toBe(false);
});
it.each(payloadTests)('returns false if the payload is missing %s', (field) => {
const wrongPayload = omit(payload, field);
expect(isPushedUserAction({ type: 'not-exist', payload: wrongPayload })).toBe(false);
});
});
describe('isUserActionType', () => {
const actionTypesTests = Object.keys(predicateMap).map((key) => [key]);
it.each(actionTypesTests)('returns true if it is a user action type is %s', (type) => {
expect(isUserActionType(type)).toBe(true);
});
it('returns false if the type is not a user action type', () => {
expect(isCreateCaseUserAction('not-exist')).toBe(false);
});
});
});

View file

@ -5,14 +5,69 @@
* 2.0.
*/
export function isCreateConnector(action?: string, actionFields?: string[]): boolean {
return action === 'create' && actionFields != null && actionFields.includes('connector');
}
import {
ActionTypes,
CommentUserAction,
ConnectorUserAction,
CreateCaseUserAction,
DescriptionUserAction,
PushedUserAction,
StatusUserAction,
TagsUserAction,
TitleUserAction,
UserActionTypes,
} from '../api';
import { SnakeToCamelCase } from '../types';
export function isUpdateConnector(action?: string, actionFields?: string[]): boolean {
return action === 'update' && actionFields != null && actionFields.includes('connector');
}
type SnakeCaseOrCamelCaseUserAction<
T extends 'snakeCase' | 'camelCase',
S,
C
> = T extends 'snakeCase' ? S : C;
export function isPush(action?: string, actionFields?: string[]): boolean {
return action === 'push-to-service' && actionFields != null && actionFields.includes('pushed');
}
export const isConnectorUserAction = (userAction: unknown): userAction is ConnectorUserAction =>
(userAction as ConnectorUserAction)?.type === ActionTypes.connector &&
(userAction as ConnectorUserAction)?.payload?.connector != null;
export const isPushedUserAction = <T extends 'snakeCase' | 'camelCase' = 'snakeCase'>(
userAction: unknown
): userAction is SnakeCaseOrCamelCaseUserAction<
T,
PushedUserAction,
SnakeToCamelCase<PushedUserAction>
> =>
(userAction as PushedUserAction)?.type === ActionTypes.pushed &&
(userAction as PushedUserAction)?.payload?.externalService != null;
export const isTitleUserAction = (userAction: unknown): userAction is TitleUserAction =>
(userAction as TitleUserAction)?.type === ActionTypes.title &&
(userAction as TitleUserAction)?.payload?.title != null;
export const isStatusUserAction = (userAction: unknown): userAction is StatusUserAction =>
(userAction as StatusUserAction)?.type === ActionTypes.status &&
(userAction as StatusUserAction)?.payload?.status != null;
export const isTagsUserAction = (userAction: unknown): userAction is TagsUserAction =>
(userAction as TagsUserAction)?.type === ActionTypes.tags &&
(userAction as TagsUserAction)?.payload?.tags != null;
export const isCommentUserAction = (userAction: unknown): userAction is CommentUserAction =>
(userAction as CommentUserAction)?.type === ActionTypes.comment &&
(userAction as CommentUserAction)?.payload?.comment != null;
export const isDescriptionUserAction = (userAction: unknown): userAction is DescriptionUserAction =>
(userAction as DescriptionUserAction)?.type === ActionTypes.description &&
(userAction as DescriptionUserAction)?.payload?.description != null;
export const isCreateCaseUserAction = (userAction: unknown): userAction is CreateCaseUserAction =>
(userAction as CreateCaseUserAction)?.type === ActionTypes.create_case &&
/**
* Connector is needed in various places across the application where
* the isCreateCaseUserAction is being used.
* Migrations should add the connector payload if it is
* missing.
*/
(userAction as CreateCaseUserAction)?.payload?.connector != null;
export const isUserActionType = (field: string): field is UserActionTypes =>
ActionTypes[field as UserActionTypes] != null;

View file

@ -5,8 +5,18 @@
* 2.0.
*/
import { ReactWrapper } from 'enzyme';
import { act } from 'react-dom/test-utils';
/**
* Convenience utility to remove text appended to links by EUI
*/
export const removeExternalLinkText = (str: string) =>
str.replace(/\(opens in a new tab or window\)/g, '');
export async function waitForComponentToPaint<P = {}>(wrapper: ReactWrapper<P>, amount = 0) {
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, amount));
wrapper.update();
});
}

View file

@ -1,8 +0,0 @@
/*
* 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 './parsers';

View file

@ -1,86 +0,0 @@
/*
* 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 { ConnectorTypes, noneConnectorId } from '../../../common/api';
import { parseStringAsConnector, parseStringAsExternalService } from './parsers';
describe('user actions utility functions', () => {
describe('parseStringAsConnector', () => {
it('return null if the data is null', () => {
expect(parseStringAsConnector('', null)).toBeNull();
});
it('return null if the data is not a json object', () => {
expect(parseStringAsConnector('', 'blah')).toBeNull();
});
it('return null if the data is not a valid connector', () => {
expect(parseStringAsConnector('', JSON.stringify({ a: '1' }))).toBeNull();
});
it('return null if id is null but the data is a connector other than none', () => {
expect(
parseStringAsConnector(
null,
JSON.stringify({ type: ConnectorTypes.jira, name: '', fields: null })
)
).toBeNull();
});
it('return the id as the none connector if the data is the none connector', () => {
expect(
parseStringAsConnector(
null,
JSON.stringify({ type: ConnectorTypes.none, name: '', fields: null })
)
).toEqual({ id: noneConnectorId, type: ConnectorTypes.none, name: '', fields: null });
});
it('returns a decoded connector with the specified id', () => {
expect(
parseStringAsConnector(
'a',
JSON.stringify({ type: ConnectorTypes.jira, name: 'hi', fields: null })
)
).toEqual({ id: 'a', type: ConnectorTypes.jira, name: 'hi', fields: null });
});
});
describe('parseStringAsExternalService', () => {
it('returns null when the data is null', () => {
expect(parseStringAsExternalService('', null)).toBeNull();
});
it('returns null when the data is not valid json', () => {
expect(parseStringAsExternalService('', 'blah')).toBeNull();
});
it('returns null when the data is not a valid external service object', () => {
expect(parseStringAsExternalService('', JSON.stringify({ a: '1' }))).toBeNull();
});
it('returns the decoded external service with the connector_id field added', () => {
const externalServiceInfo = {
connector_name: 'name',
external_id: '1',
external_title: 'title',
external_url: 'abc',
pushed_at: '1',
pushed_by: {
username: 'a',
email: 'a@a.com',
full_name: 'a',
},
};
expect(parseStringAsExternalService('500', JSON.stringify(externalServiceInfo))).toEqual({
...externalServiceInfo,
connector_id: '500',
});
});
});
});

View file

@ -1,77 +0,0 @@
/*
* 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 {
CaseUserActionConnectorRt,
CaseConnector,
ConnectorTypes,
noneConnectorId,
CaseFullExternalService,
CaseUserActionExternalServiceRt,
} from '../../../common/api';
export const parseStringAsConnector = (
id: string | null,
encodedData: string | null
): CaseConnector | null => {
if (encodedData == null) {
return null;
}
const decodedConnector = parseString(encodedData);
if (!CaseUserActionConnectorRt.is(decodedConnector)) {
return null;
}
if (id == null && decodedConnector.type === ConnectorTypes.none) {
return {
...decodedConnector,
id: noneConnectorId,
};
} else if (id == null) {
return null;
} else {
// id does not equal null or undefined and the connector type does not equal none
// so return the connector with its id
return {
...decodedConnector,
id,
};
}
};
const parseString = (params: string | null): unknown | null => {
if (params == null) {
return null;
}
try {
return JSON.parse(params);
} catch {
return null;
}
};
export const parseStringAsExternalService = (
id: string | null,
encodedData: string | null
): CaseFullExternalService => {
if (encodedData == null) {
return null;
}
const decodedExternalService = parseString(encodedData);
if (!CaseUserActionExternalServiceRt.is(decodedExternalService)) {
return null;
}
return {
...decodedExternalService,
connector_id: id,
};
};

View file

@ -9,6 +9,7 @@ import React from 'react';
import { mount } from 'enzyme';
import { act, render } from '@testing-library/react';
import { NONE_CONNECTOR_ID } from '../../../common/api';
import { useForm, Form, FormHook } from '../../common/shared_imports';
import { useGetTags } from '../../containers/use_get_tags';
import { useConnectors } from '../../containers/configure/use_connectors';
@ -35,7 +36,7 @@ const initialCaseValue: FormProps = {
description: '',
tags: [],
title: '',
connectorId: 'none',
connectorId: NONE_CONNECTOR_ID,
fields: null,
syncAlerts: true,
};

View file

@ -14,7 +14,7 @@ import { usePostPushToService } from '../../containers/use_post_push_to_service'
import { useConnectors } from '../../containers/configure/use_connectors';
import { Case } from '../../containers/types';
import { CaseType } from '../../../common/api';
import { CaseType, NONE_CONNECTOR_ID } from '../../../common/api';
import { UsePostComment, usePostComment } from '../../containers/use_post_comment';
import { useCasesContext } from '../cases_context/use_cases_context';
import { useCasesFeatures } from '../cases_context/use_cases_features';
@ -24,7 +24,7 @@ const initialCaseValue: FormProps = {
description: '',
tags: [],
title: '',
connectorId: 'none',
connectorId: NONE_CONNECTOR_ID,
fields: null,
syncAlerts: true,
selectedOwner: null,

View file

@ -14,6 +14,7 @@ import { OBSERVABILITY_OWNER } from '../../../common/constants';
import { useForm, Form, FormHook } from '../../common/shared_imports';
import { CreateCaseOwnerSelector } from './owner_selector';
import { schema, FormProps } from './schema';
import { waitForComponentToPaint } from '../../common/test_utils';
describe('Case Owner Selection', () => {
let globalForm: FormHook;
@ -35,26 +36,29 @@ describe('Case Owner Selection', () => {
jest.clearAllMocks();
});
it('renders', () => {
it('renders', async () => {
const wrapper = mount(
<MockHookWrapperComponent>
<CreateCaseOwnerSelector availableOwners={[SECURITY_SOLUTION_OWNER]} isLoading={false} />
</MockHookWrapperComponent>
);
await waitForComponentToPaint(wrapper);
expect(wrapper.find(`[data-test-subj="caseOwnerSelector"]`).exists()).toBeTruthy();
});
it.each([
[OBSERVABILITY_OWNER, SECURITY_SOLUTION_OWNER],
[SECURITY_SOLUTION_OWNER, OBSERVABILITY_OWNER],
])('disables %s button if user only has %j', (disabledButton, permission) => {
])('disables %s button if user only has %j', async (disabledButton, permission) => {
const wrapper = mount(
<MockHookWrapperComponent>
<CreateCaseOwnerSelector availableOwners={[permission]} isLoading={false} />
</MockHookWrapperComponent>
);
await waitForComponentToPaint(wrapper);
expect(
wrapper.find(`[data-test-subj="${disabledButton}RadioButton"] input`).first().props().disabled
).toBeTruthy();
@ -76,6 +80,8 @@ describe('Case Owner Selection', () => {
</MockHookWrapperComponent>
);
await waitForComponentToPaint(wrapper);
expect(
wrapper.find(`[data-test-subj="observabilityRadioButton"] input`).first().props().checked
).toBeFalsy();

View file

@ -5,71 +5,37 @@
* 2.0.
*/
import { CaseUserActionConnector, ConnectorTypes } from '../../../common/api';
import { Actions, ConnectorTypes, ConnectorUserAction } from '../../../common/api';
import { CaseUserActions } from '../../containers/types';
import { getConnectorFieldsFromUserActions } from './helpers';
const defaultJiraFields = {
issueType: '1',
parent: null,
priority: null,
};
describe('helpers', () => {
describe('getConnectorFieldsFromUserActions', () => {
it('returns null when it cannot find the connector id', () => {
expect(getConnectorFieldsFromUserActions('a', [])).toBeNull();
});
it('returns null when the value fields are not valid encoded fields', () => {
expect(
getConnectorFieldsFromUserActions('a', [createUserAction({ newValue: 'a', oldValue: 'a' })])
).toBeNull();
});
it('returns null when it cannot find the connector id in a non empty array', () => {
expect(
getConnectorFieldsFromUserActions('a', [
createUserAction({
newValue: JSON.stringify({ a: '1' }),
oldValue: JSON.stringify({ a: '1' }),
createConnectorUserAction({
// @ts-expect-error payload missing fields
payload: { a: '1' },
}),
])
).toBeNull();
});
it('returns the fields when it finds the connector id in the new value', () => {
expect(
getConnectorFieldsFromUserActions('a', [
createUserAction({
newValue: createEncodedJiraConnector(),
oldValue: JSON.stringify({ a: '1' }),
newValConnectorId: 'a',
}),
])
).toEqual(defaultJiraFields);
});
it('returns the fields when it finds the connector id in the new value and the old value is null', () => {
expect(
getConnectorFieldsFromUserActions('a', [
createUserAction({
newValue: createEncodedJiraConnector(),
newValConnectorId: 'a',
}),
])
).toEqual(defaultJiraFields);
});
it('returns the fields when it finds the connector id in the old value', () => {
const expectedFields = { ...defaultJiraFields, issueType: '5' };
expect(
getConnectorFieldsFromUserActions('id-to-find', [
createUserAction({
newValue: createEncodedJiraConnector(),
oldValue: createEncodedJiraConnector({
fields: expectedFields,
}),
newValConnectorId: 'b',
oldValConnectorId: 'id-to-find',
}),
])
).toEqual(expectedFields);
it('returns the fields when it finds the connector id', () => {
expect(getConnectorFieldsFromUserActions('a', [createConnectorUserAction()])).toEqual(
defaultJiraFields
);
});
it('returns the fields when it finds the connector id in the second user action', () => {
@ -77,76 +43,27 @@ describe('helpers', () => {
expect(
getConnectorFieldsFromUserActions('id-to-find', [
createUserAction({
newValue: createEncodedJiraConnector(),
oldValue: createEncodedJiraConnector(),
newValConnectorId: 'b',
oldValConnectorId: 'a',
}),
createUserAction({
newValue: createEncodedJiraConnector(),
oldValue: createEncodedJiraConnector({ fields: expectedFields }),
newValConnectorId: 'b',
oldValConnectorId: 'id-to-find',
createConnectorUserAction({}),
createConnectorUserAction({
payload: {
connector: {
id: 'id-to-find',
name: 'test',
fields: expectedFields,
type: ConnectorTypes.jira,
},
},
}),
])
).toEqual(expectedFields);
});
it('ignores a parse failure and finds the right user action', () => {
expect(
getConnectorFieldsFromUserActions('none', [
createUserAction({
newValue: 'b',
newValConnectorId: null,
}),
createUserAction({
newValue: createEncodedJiraConnector({
type: ConnectorTypes.none,
name: '',
fields: null,
}),
newValConnectorId: null,
}),
])
).toBeNull();
});
it('returns null when the id matches but the encoded value is null', () => {
expect(
getConnectorFieldsFromUserActions('b', [
createUserAction({
newValue: null,
newValConnectorId: 'b',
}),
])
).toBeNull();
});
it('returns null when the action fields is not of length 1', () => {
it('returns null when the action is not a connector', () => {
expect(
getConnectorFieldsFromUserActions('id-to-find', [
createUserAction({
newValue: JSON.stringify({ a: '1', fields: { hello: '1' } }),
oldValue: JSON.stringify({ a: '1', fields: { hi: '2' } }),
newValConnectorId: 'b',
oldValConnectorId: 'id-to-find',
actionField: ['connector', 'connector'],
}),
])
).toBeNull();
});
it('matches the none connector the searched for id is none', () => {
expect(
getConnectorFieldsFromUserActions('none', [
createUserAction({
newValue: createEncodedJiraConnector({
type: ConnectorTypes.none,
name: '',
fields: null,
}),
newValConnectorId: null,
createConnectorUserAction({
// @ts-expect-error
type: 'not-a-connector',
}),
])
).toBeNull();
@ -154,34 +71,18 @@ describe('helpers', () => {
});
});
function createUserAction(fields: Partial<CaseUserActions>): CaseUserActions {
function createConnectorUserAction(attributes: Partial<ConnectorUserAction> = {}): CaseUserActions {
return {
action: 'update',
actionAt: '',
actionBy: {},
actionField: ['connector'],
action: Actions.update,
createdBy: { username: 'user', fullName: null, email: null },
createdAt: '2021-12-08T11:28:32.623Z',
type: 'connector',
actionId: '',
caseId: '',
commentId: '',
newValConnectorId: null,
oldValConnectorId: null,
newValue: null,
oldValue: null,
...fields,
};
payload: {
connector: { id: 'a', name: 'test', fields: defaultJiraFields, type: ConnectorTypes.jira },
},
...attributes,
} as CaseUserActions;
}
function createEncodedJiraConnector(fields?: Partial<CaseUserActionConnector>): string {
return JSON.stringify({
type: ConnectorTypes.jira,
name: 'name',
fields: defaultJiraFields,
...fields,
});
}
const defaultJiraFields = {
issueType: '1',
parent: null,
priority: null,
};

View file

@ -5,39 +5,23 @@
* 2.0.
*/
import { isConnectorUserAction, isCreateCaseUserAction } from '../../../common/utils/user_actions';
import { ConnectorTypeFields } from '../../../common/api';
import { CaseUserActions } from '../../containers/types';
import { parseStringAsConnector } from '../../common/user_actions';
export const getConnectorFieldsFromUserActions = (
id: string,
userActions: CaseUserActions[]
): ConnectorTypeFields['fields'] => {
try {
for (const action of [...userActions].reverse()) {
if (action.actionField.length === 1 && action.actionField[0] === 'connector') {
const parsedNewConnector = parseStringAsConnector(
action.newValConnectorId,
action.newValue
);
for (const action of [...userActions].reverse()) {
if (isConnectorUserAction(action) || isCreateCaseUserAction(action)) {
const connector = action.payload.connector;
if (parsedNewConnector && id === parsedNewConnector.id) {
return parsedNewConnector.fields;
}
const parsedOldConnector = parseStringAsConnector(
action.oldValConnectorId,
action.oldValue
);
if (parsedOldConnector && id === parsedOldConnector.id) {
return parsedOldConnector.fields;
}
if (connector && id === connector.id) {
return connector.fields;
}
}
return null;
} catch {
return null;
}
return null;
};

View file

@ -8,65 +8,71 @@
import React from 'react';
import { mount } from 'enzyme';
import { CaseStatuses, ConnectorTypes } from '../../../common/api';
import { basicPush, getUserAction } from '../../containers/mock';
import {
getLabelTitle,
getPushedServiceLabelTitle,
getConnectorLabelTitle,
toStringArray,
} from './helpers';
Actions,
CaseStatuses,
CommentType,
ConnectorTypes,
ConnectorUserAction,
PushedUserAction,
TagsUserAction,
TitleUserAction,
} from '../../../common/api';
import { basicPush, getUserAction } from '../../containers/mock';
import { getLabelTitle, getPushedServiceLabelTitle, getConnectorLabelTitle } from './helpers';
import { connectorsMock } from '../../containers/configure/mock';
import * as i18n from './translations';
import { SnakeToCamelCase } from '../../../common/types';
import { SECURITY_SOLUTION_OWNER } from '../../../common/constants';
describe('User action tree helpers', () => {
const connectors = connectorsMock;
it('label title generated for update tags', () => {
const action = getUserAction(['tags'], 'update');
const action = getUserAction('tags', Actions.update, { payload: { tags: ['test'] } });
const result: string | JSX.Element = getLabelTitle({
action,
field: 'tags',
});
const tags = (action as unknown as TagsUserAction).payload.tags;
const wrapper = mount(<>{result}</>);
expect(wrapper.find(`[data-test-subj="ua-tags-label"]`).first().text()).toEqual(
` ${i18n.TAGS.toLowerCase()}`
);
expect(wrapper.find(`[data-test-subj="tag-${action.newValue}"]`).first().text()).toEqual(
action.newValue
);
expect(wrapper.find(`[data-test-subj="tag-${tags[0]}"]`).first().text()).toEqual(tags[0]);
});
it('label title generated for update title', () => {
const action = getUserAction(['title'], 'update');
const action = getUserAction('title', Actions.update, { payload: { title: 'test' } });
const result: string | JSX.Element = getLabelTitle({
action,
field: 'title',
});
const title = (action as unknown as TitleUserAction).payload.title;
expect(result).toEqual(
`${i18n.CHANGED_FIELD.toLowerCase()} ${i18n.CASE_NAME.toLowerCase()} ${i18n.TO} "${
action.newValue
}"`
`${i18n.CHANGED_FIELD.toLowerCase()} ${i18n.CASE_NAME.toLowerCase()} ${i18n.TO} "${title}"`
);
});
it('label title generated for update description', () => {
const action = getUserAction(['description'], 'update');
const action = getUserAction('description', Actions.update, {
payload: { description: 'test' },
});
const result: string | JSX.Element = getLabelTitle({
action,
field: 'description',
});
expect(result).toEqual(`${i18n.EDITED_FIELD} ${i18n.DESCRIPTION.toLowerCase()}`);
});
it('label title generated for update status to open', () => {
const action = { ...getUserAction(['status'], 'update'), newValue: CaseStatuses.open };
const action = {
...getUserAction('status', Actions.update, { payload: { status: CaseStatuses.open } }),
};
const result: string | JSX.Element = getLabelTitle({
action,
field: 'status',
});
const wrapper = mount(<>{result}</>);
@ -75,12 +81,12 @@ describe('User action tree helpers', () => {
it('label title generated for update status to in-progress', () => {
const action = {
...getUserAction(['status'], 'update'),
newValue: CaseStatuses['in-progress'],
...getUserAction('status', Actions.update, {
payload: { status: CaseStatuses['in-progress'] },
}),
};
const result: string | JSX.Element = getLabelTitle({
action,
field: 'status',
});
const wrapper = mount(<>{result}</>);
@ -90,10 +96,13 @@ describe('User action tree helpers', () => {
});
it('label title generated for update status to closed', () => {
const action = { ...getUserAction(['status'], 'update'), newValue: CaseStatuses.closed };
const action = {
...getUserAction('status', Actions.update, {
payload: { status: CaseStatuses.closed },
}),
};
const result: string | JSX.Element = getLabelTitle({
action,
field: 'status',
});
const wrapper = mount(<>{result}</>);
@ -101,64 +110,67 @@ describe('User action tree helpers', () => {
});
it('label title is empty when status is not valid', () => {
const action = { ...getUserAction(['status'], 'update'), newValue: CaseStatuses.closed };
const action = {
...getUserAction('status', Actions.update, {
payload: { status: '' },
}),
};
const result: string | JSX.Element = getLabelTitle({
action: { ...action, newValue: 'not-exist' },
field: 'status',
action,
});
expect(result).toEqual('');
});
it('label title generated for update comment', () => {
const action = getUserAction(['comment'], 'update');
const action = getUserAction('comment', Actions.update, {
payload: {
comment: { comment: 'a comment', type: CommentType.user, owner: SECURITY_SOLUTION_OWNER },
},
});
const result: string | JSX.Element = getLabelTitle({
action,
field: 'comment',
});
expect(result).toEqual(`${i18n.EDITED_FIELD} ${i18n.COMMENT.toLowerCase()}`);
});
it('label title generated for pushed incident', () => {
const action = getUserAction(['pushed'], 'push-to-service');
const action = getUserAction('pushed', 'push_to_service', {
payload: { externalService: basicPush },
}) as SnakeToCamelCase<PushedUserAction>;
const result: string | JSX.Element = getPushedServiceLabelTitle(action, true);
const externalService = (action as SnakeToCamelCase<PushedUserAction>).payload.externalService;
const wrapper = mount(<>{result}</>);
expect(wrapper.find(`[data-test-subj="pushed-label"]`).first().text()).toEqual(
`${i18n.PUSHED_NEW_INCIDENT} ${basicPush.connectorName}`
);
expect(wrapper.find(`[data-test-subj="pushed-value"]`).first().prop('href')).toEqual(
JSON.parse(action.newValue!).external_url
externalService.externalUrl
);
});
it('label title generated for needs update incident', () => {
const action = getUserAction(['pushed'], 'push-to-service');
const action = getUserAction('pushed', 'push_to_service') as SnakeToCamelCase<PushedUserAction>;
const result: string | JSX.Element = getPushedServiceLabelTitle(action, false);
const externalService = (action as SnakeToCamelCase<PushedUserAction>).payload.externalService;
const wrapper = mount(<>{result}</>);
expect(wrapper.find(`[data-test-subj="pushed-label"]`).first().text()).toEqual(
`${i18n.UPDATE_INCIDENT} ${basicPush.connectorName}`
);
expect(wrapper.find(`[data-test-subj="pushed-value"]`).first().prop('href')).toEqual(
JSON.parse(action.newValue!).external_url
externalService.externalUrl
);
});
describe('getConnectorLabelTitle', () => {
it('returns an empty string when the encoded old value is null', () => {
it('returns an empty string when the encoded value is null', () => {
const result = getConnectorLabelTitle({
action: getUserAction(['connector'], 'update', { oldValue: null }),
connectors,
});
expect(result).toEqual('');
});
it('returns an empty string when the encoded new value is null', () => {
const result = getConnectorLabelTitle({
action: getUserAction(['connector'], 'update', { newValue: null }),
// @ts-expect-error
action: getUserAction(['connector'], Actions.update, { payload: { connector: null } }),
connectors,
});
@ -167,16 +179,16 @@ describe('User action tree helpers', () => {
it('returns the change connector label', () => {
const result: string | JSX.Element = getConnectorLabelTitle({
action: getUserAction(['connector'], 'update', {
oldValue: JSON.stringify({
type: ConnectorTypes.serviceNowITSM,
name: 'a',
fields: null,
}),
oldValConnectorId: 'servicenow-1',
newValue: JSON.stringify({ type: ConnectorTypes.resilient, name: 'a', fields: null }),
newValConnectorId: 'resilient-2',
}),
action: getUserAction('connector', Actions.update, {
payload: {
connector: {
id: 'resilient-2',
type: ConnectorTypes.resilient,
name: 'a',
fields: null,
},
},
}) as unknown as ConnectorUserAction,
connectors,
});
@ -185,64 +197,15 @@ describe('User action tree helpers', () => {
it('returns the removed connector label', () => {
const result: string | JSX.Element = getConnectorLabelTitle({
action: getUserAction(['connector'], 'update', {
oldValue: JSON.stringify({ type: ConnectorTypes.serviceNowITSM, name: '', fields: null }),
oldValConnectorId: 'servicenow-1',
newValue: JSON.stringify({ type: ConnectorTypes.none, name: '', fields: null }),
newValConnectorId: 'none',
}),
action: getUserAction('connector', Actions.update, {
payload: {
connector: { id: 'none', type: ConnectorTypes.none, name: 'test', fields: null },
},
}) as unknown as ConnectorUserAction,
connectors,
});
expect(result).toEqual('removed external incident management system');
});
it('returns the connector fields changed label', () => {
const result: string | JSX.Element = getConnectorLabelTitle({
action: getUserAction(['connector'], 'update', {
oldValue: JSON.stringify({ type: ConnectorTypes.serviceNowITSM, name: '', fields: null }),
oldValConnectorId: 'servicenow-1',
newValue: JSON.stringify({ type: ConnectorTypes.serviceNowITSM, name: '', fields: null }),
newValConnectorId: 'servicenow-1',
}),
connectors,
});
expect(result).toEqual('changed connector field');
});
});
describe('toStringArray', () => {
const circularReference = { otherData: 123, circularReference: undefined };
// @ts-ignore testing catch on circular reference
circularReference.circularReference = circularReference;
it('handles all data types in an array', () => {
const value = [1, true, { a: 1 }, circularReference, 'yeah', 100n, null];
const res = toStringArray(value);
expect(res).toEqual(['1', 'true', '{"a":1}', 'Invalid Object', 'yeah', '100']);
});
it('handles null', () => {
const value = null;
const res = toStringArray(value);
expect(res).toEqual([]);
});
it('handles object', () => {
const value = { a: true };
const res = toStringArray(value);
expect(res).toEqual([JSON.stringify(value)]);
});
it('handles Invalid Object', () => {
const value = circularReference;
const res = toStringArray(value);
expect(res).toEqual(['Invalid Object']);
});
it('handles unexpected value', () => {
const value = 100n;
const res = toStringArray(value);
expect(res).toEqual(['100']);
});
});
});

View file

@ -16,18 +16,20 @@ import {
import React, { useContext } from 'react';
import classNames from 'classnames';
import { ThemeContext } from 'styled-components';
import { Comment } from '../../../common/ui/types';
import { CaseExternalService, Comment } from '../../../common/ui/types';
import {
CaseFullExternalService,
ActionConnector,
CaseStatuses,
CommentType,
CommentRequestActionsType,
noneConnectorId,
NONE_CONNECTOR_ID,
Actions,
ConnectorUserAction,
PushedUserAction,
TagsUserAction,
} from '../../../common/api';
import { CaseUserActions } from '../../containers/types';
import { CaseServices } from '../../containers/use_get_case_user_actions';
import { parseStringAsConnector, parseStringAsExternalService } from '../../common/user_actions';
import { Tags } from '../tag_list/tags';
import { UserActionUsernameWithAvatar } from './user_action_username_with_avatar';
import { UserActionTimestamp } from './user_action_timestamp';
@ -41,10 +43,17 @@ import { AlertCommentEvent } from './user_action_alert_comment_event';
import { CasesNavigation } from '../links';
import { HostIsolationCommentEvent } from './user_action_host_isolation_comment_event';
import { MarkdownRenderer } from '../markdown_editor';
import {
isCommentUserAction,
isDescriptionUserAction,
isStatusUserAction,
isTagsUserAction,
isTitleUserAction,
} from '../../../common/utils/user_actions';
import { SnakeToCamelCase } from '../../../common/types';
interface LabelTitle {
action: CaseUserActions;
field: string;
}
export type RuleDetailsNavigation = CasesNavigation<string | null | undefined, 'configurable'>;
@ -68,23 +77,23 @@ const getStatusTitle = (id: string, status: CaseStatuses) => (
const isStatusValid = (status: string): status is CaseStatuses =>
Object.prototype.hasOwnProperty.call(statuses, status);
export const getLabelTitle = ({ action, field }: LabelTitle) => {
if (field === 'tags') {
export const getLabelTitle = ({ action }: LabelTitle) => {
if (isTagsUserAction(action)) {
return getTagsLabelTitle(action);
} else if (field === 'title' && action.action === 'update') {
} else if (isTitleUserAction(action)) {
return `${i18n.CHANGED_FIELD.toLowerCase()} ${i18n.CASE_NAME.toLowerCase()} ${i18n.TO} "${
action.newValue
action.payload.title
}"`;
} else if (field === 'description' && action.action === 'update') {
} else if (isDescriptionUserAction(action) && action.action === Actions.update) {
return `${i18n.EDITED_FIELD} ${i18n.DESCRIPTION.toLowerCase()}`;
} else if (field === 'status' && action.action === 'update') {
const status = action.newValue ?? '';
} else if (isStatusUserAction(action)) {
const status = action.payload.status ?? '';
if (isStatusValid(status)) {
return getStatusTitle(action.actionId, status);
}
return '';
} else if (field === 'comment' && action.action === 'update') {
} else if (isCommentUserAction(action) && action.action === Actions.update) {
return `${i18n.EDITED_FIELD} ${i18n.COMMENT.toLowerCase()}`;
}
@ -95,25 +104,19 @@ export const getConnectorLabelTitle = ({
action,
connectors,
}: {
action: CaseUserActions;
action: ConnectorUserAction;
connectors: ActionConnector[];
}) => {
const oldConnector = parseStringAsConnector(action.oldValConnectorId, action.oldValue);
const newConnector = parseStringAsConnector(action.newValConnectorId, action.newValue);
const connector = action.payload.connector;
if (!oldConnector || !newConnector) {
if (connector == null) {
return '';
}
// if the ids are the same, assume we just changed the fields
if (oldConnector.id === newConnector.id) {
return i18n.CHANGED_CONNECTOR_FIELD;
}
// ids are not the same so check and see if the id is a valid connector and then return its name
// if the connector id is the none connector value then it must have been removed
const newConnectorActionInfo = connectors.find((c) => c.id === newConnector.id);
if (newConnector.id !== noneConnectorId && newConnectorActionInfo != null) {
const newConnectorActionInfo = connectors.find((c) => c.id === connector.id);
if (connector.id !== NONE_CONNECTOR_ID && newConnectorActionInfo != null) {
return i18n.SELECTED_THIRD_PARTY(newConnectorActionInfo.name);
}
@ -121,14 +124,14 @@ export const getConnectorLabelTitle = ({
return i18n.REMOVED_THIRD_PARTY;
};
const getTagsLabelTitle = (action: CaseUserActions) => {
const tags = action.newValue != null ? action.newValue.split(',') : [];
const getTagsLabelTitle = (action: TagsUserAction) => {
const tags = action.payload.tags ?? [];
return (
<EuiFlexGroup alignItems="baseline" gutterSize="xs" component="span" responsive={false}>
<EuiFlexItem data-test-subj="ua-tags-label" grow={false}>
{action.action === 'add' && i18n.ADDED_FIELD}
{action.action === 'delete' && i18n.REMOVED_FIELD} {i18n.TAGS.toLowerCase()}
{action.action === Actions.add && i18n.ADDED_FIELD}
{action.action === Actions.delete && i18n.REMOVED_FIELD} {i18n.TAGS.toLowerCase()}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<Tags tags={tags} gutterSize="xs" />
@ -137,8 +140,11 @@ const getTagsLabelTitle = (action: CaseUserActions) => {
);
};
export const getPushedServiceLabelTitle = (action: CaseUserActions, firstPush: boolean) => {
const externalService = parseStringAsExternalService(action.newValConnectorId, action.newValue);
export const getPushedServiceLabelTitle = (
action: SnakeToCamelCase<PushedUserAction>,
firstPush: boolean
) => {
const externalService = action.payload.externalService;
return (
<EuiFlexGroup
@ -149,12 +155,12 @@ export const getPushedServiceLabelTitle = (action: CaseUserActions, firstPush: b
>
<EuiFlexItem data-test-subj="pushed-label">
{`${firstPush ? i18n.PUSHED_NEW_INCIDENT : i18n.UPDATE_INCIDENT} ${
externalService?.connector_name
externalService?.connectorName
}`}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiLink data-test-subj="pushed-value" href={externalService?.external_url} target="_blank">
{externalService?.external_title}
<EuiLink data-test-subj="pushed-value" href={externalService?.externalUrl} target="_blank">
{externalService?.externalTitle}
</EuiLink>
</EuiFlexItem>
</EuiFlexGroup>
@ -163,25 +169,25 @@ export const getPushedServiceLabelTitle = (action: CaseUserActions, firstPush: b
export const getPushInfo = (
caseServices: CaseServices,
externalService: CaseFullExternalService | undefined,
externalService: CaseExternalService | undefined,
index: number
) =>
externalService != null && externalService.connector_id != null
externalService != null && externalService.connectorId !== NONE_CONNECTOR_ID
? {
firstPush: caseServices[externalService.connector_id]?.firstPushIndex === index,
parsedConnectorId: externalService.connector_id,
parsedConnectorName: externalService.connector_name,
firstPush: caseServices[externalService.connectorId]?.firstPushIndex === index,
parsedConnectorId: externalService.connectorId,
parsedConnectorName: externalService.connectorName,
}
: {
firstPush: false,
parsedConnectorId: noneConnectorId,
parsedConnectorName: noneConnectorId,
parsedConnectorId: NONE_CONNECTOR_ID,
parsedConnectorName: NONE_CONNECTOR_ID,
};
const getUpdateActionIcon = (actionField: string): string => {
if (actionField === 'tags') {
const getUpdateActionIcon = (fields: string): string => {
if (fields === 'tags') {
return 'tag';
} else if (actionField === 'status') {
} else if (fields === 'status') {
return 'folderClosed';
}
@ -199,21 +205,21 @@ export const getUpdateAction = ({
}): EuiCommentProps => ({
username: (
<UserActionUsernameWithAvatar
username={action.actionBy.username}
fullName={action.actionBy.fullName}
username={action.createdBy.username}
fullName={action.createdBy.fullName}
/>
),
type: 'update',
event: label,
'data-test-subj': `${action.actionField[0]}-${action.action}-action-${action.actionId}`,
timestamp: <UserActionTimestamp createdAt={action.actionAt} />,
timelineIcon: getUpdateActionIcon(action.actionField[0]),
'data-test-subj': `${action.type}-${action.action}-action-${action.actionId}`,
timestamp: <UserActionTimestamp createdAt={action.createdAt} />,
timelineIcon: getUpdateActionIcon(action.type),
actions: (
<EuiFlexGroup responsive={false}>
<EuiFlexItem grow={false}>
<UserActionCopyLink id={action.actionId} />
</EuiFlexItem>
{action.action === 'update' && action.commentId != null && (
{action.action === Actions.update && action.commentId != null && (
<EuiFlexItem grow={false}>
<UserActionMoveToReference id={action.commentId} outlineComment={handleOutlineComment} />
</EuiFlexItem>
@ -245,8 +251,8 @@ export const getAlertAttachment = ({
}): EuiCommentProps => ({
username: (
<UserActionUsernameWithAvatar
username={action.actionBy.username}
fullName={action.actionBy.fullName}
username={action.createdBy.username}
fullName={action.createdBy.fullName}
/>
),
className: 'comment-alert',
@ -262,8 +268,8 @@ export const getAlertAttachment = ({
commentType={CommentType.alert}
/>
),
'data-test-subj': `${action.actionField[0]}-${action.action}-action-${action.actionId}`,
timestamp: <UserActionTimestamp createdAt={action.actionAt} />,
'data-test-subj': `${action.type}-${action.action}-action-${action.actionId}`,
timestamp: <UserActionTimestamp createdAt={action.createdAt} />,
timelineIcon: 'bell',
actions: (
<EuiFlexGroup responsive={false}>
@ -282,41 +288,6 @@ export const getAlertAttachment = ({
),
});
export const toStringArray = (value: unknown): string[] => {
if (Array.isArray(value)) {
return value.reduce<string[]>((acc, v) => {
if (v != null) {
switch (typeof v) {
case 'number':
case 'boolean':
return [...acc, v.toString()];
case 'object':
try {
return [...acc, JSON.stringify(v)];
} catch {
return [...acc, 'Invalid Object'];
}
case 'string':
return [...acc, v];
default:
return [...acc, `${v}`];
}
}
return acc;
}, []);
} else if (value == null) {
return [];
} else if (typeof value === 'object') {
try {
return [JSON.stringify(value)];
} catch {
return ['Invalid Object'];
}
} else {
return [`${value}`];
}
};
export const getGeneratedAlertsAttachment = ({
action,
alertIds,
@ -348,8 +319,8 @@ export const getGeneratedAlertsAttachment = ({
commentType={CommentType.generatedAlert}
/>
),
'data-test-subj': `${action.actionField[0]}-${action.action}-action-${action.actionId}`,
timestamp: <UserActionTimestamp createdAt={action.actionAt} />,
'data-test-subj': `${action.type}-${action.action}-action-${action.actionId}`,
timestamp: <UserActionTimestamp createdAt={action.createdAt} />,
timelineIcon: 'bell',
actions: (
<EuiFlexGroup responsive={false}>
@ -412,7 +383,7 @@ export const getActionAttachment = ({
/>
),
'data-test-subj': 'endpoint-action',
timestamp: <UserActionTimestamp createdAt={action.actionAt} />,
timestamp: <UserActionTimestamp createdAt={action.createdAt} />,
timelineIcon: <ActionIcon actionType={comment.actions.type} />,
actions: <UserActionCopyLink id={comment.id} />,
children: comment.comment.trim().length > 0 && (

View file

@ -23,6 +23,7 @@ import {
import { UserActionTree } from '.';
import { TestProviders } from '../../common/mock';
import { Ecs } from '../../../common/ui/types';
import { Actions } from '../../../common/api';
const fetchUserActions = jest.fn();
const onUpdateField = jest.fn();
@ -94,8 +95,8 @@ describe(`UserActionTree`, () => {
it('Renders service now update line with top and bottom when push is required', async () => {
const ourActions = [
getUserAction(['pushed'], 'push-to-service'),
getUserAction(['comment'], 'update'),
getUserAction('pushed', 'push_to_service'),
getUserAction('comment', Actions.update),
];
const props = {
@ -123,7 +124,7 @@ describe(`UserActionTree`, () => {
});
it('Renders service now update line with top only when push is up to date', async () => {
const ourActions = [getUserAction(['pushed'], 'push-to-service')];
const ourActions = [getUserAction('pushed', 'push_to_service')];
const props = {
...defaultProps,
caseUserActions: ourActions,
@ -149,7 +150,10 @@ describe(`UserActionTree`, () => {
});
});
it('Outlines comment when update move to link is clicked', async () => {
const ourActions = [getUserAction(['comment'], 'create'), getUserAction(['comment'], 'update')];
const ourActions = [
getUserAction('comment', Actions.create),
getUserAction('comment', Actions.update),
];
const props = {
...defaultProps,
caseUserActions: ourActions,
@ -184,7 +188,7 @@ describe(`UserActionTree`, () => {
});
});
it('Switches to markdown when edit is clicked and back to panel when canceled', async () => {
const ourActions = [getUserAction(['comment'], 'create')];
const ourActions = [getUserAction('comment', Actions.create)];
const props = {
...defaultProps,
caseUserActions: ourActions,
@ -228,7 +232,7 @@ describe(`UserActionTree`, () => {
});
it('calls update comment when comment markdown is saved', async () => {
const ourActions = [getUserAction(['comment'], 'create')];
const ourActions = [getUserAction('comment', Actions.create)];
const props = {
...defaultProps,
caseUserActions: ourActions,
@ -361,7 +365,7 @@ describe(`UserActionTree`, () => {
const commentId = 'basic-comment-id';
jest.spyOn(routeData, 'useParams').mockReturnValue({ commentId });
const ourActions = [getUserAction(['comment'], 'create')];
const ourActions = [getUserAction('comment', Actions.create)];
const props = {
...defaultProps,
caseUserActions: ourActions,
@ -381,6 +385,7 @@ describe(`UserActionTree`, () => {
).toEqual(true);
});
});
describe('Host isolation action', () => {
it('renders in the cases details view', async () => {
const isolateAction = [getHostIsolationUserAction()];

View file

@ -28,14 +28,13 @@ import { AddComment, AddCommentRefObject } from '../add_comment';
import { Case, CaseUserActions, Ecs } from '../../../common/ui/types';
import {
ActionConnector,
Actions,
ActionsCommentRequestRt,
AlertCommentRequestRt,
CommentType,
ContextTypeUserRt,
} from '../../../common/api';
import { CaseServices } from '../../containers/use_get_case_user_actions';
import { parseStringAsExternalService } from '../../common/user_actions';
import type { OnUpdateFields } from '../case_view/types';
import {
getConnectorLabelTitle,
getLabelTitle,
@ -56,6 +55,8 @@ import { UserActionContentToolbar } from './user_action_content_toolbar';
import { getManualAlertIdsWithNoRuleId } from '../case_view/helpers';
import { useLensDraftComment } from '../markdown_editor/plugins/lens/use_lens_draft_comment';
import { useCaseViewParams } from '../../common/navigation';
import { isConnectorUserAction, isPushedUserAction } from '../../../common/utils/user_actions';
import type { OnUpdateFields } from '../case_view/types';
export interface UserActionTreeProps {
caseServices: CaseServices;
@ -341,7 +342,7 @@ export const UserActionTree = React.memo(
// eslint-disable-next-line complexity
(comments, action, index) => {
// Comment creation
if (action.commentId != null && action.action === 'create') {
if (action.commentId != null && action.action === Actions.create) {
const comment = caseData.comments.find((c) => c.id === action.commentId);
if (
comment != null &&
@ -501,7 +502,7 @@ export const UserActionTree = React.memo(
}
// Connectors
if (action.actionField.length === 1 && action.actionField[0] === 'connector') {
if (isConnectorUserAction(action)) {
const label = getConnectorLabelTitle({ action, connectors });
return [
...comments,
@ -514,11 +515,8 @@ export const UserActionTree = React.memo(
}
// Pushed information
if (action.actionField.length === 1 && action.actionField[0] === 'pushed') {
const parsedExternalService = parseStringAsExternalService(
action.newValConnectorId,
action.newValue
);
if (isPushedUserAction<'camelCase'>(action)) {
const parsedExternalService = action.payload.externalService;
const { firstPush, parsedConnectorId, parsedConnectorName } = getPushInfo(
caseServices,
@ -529,11 +527,11 @@ export const UserActionTree = React.memo(
const label = getPushedServiceLabelTitle(action, firstPush);
const showTopFooter =
action.action === 'push-to-service' &&
action.action === Actions.push_to_service &&
index === caseServices[parsedConnectorId]?.lastPushIndex;
const showBottomFooter =
action.action === 'push-to-service' &&
action.action === Actions.push_to_service &&
index === caseServices[parsedConnectorId]?.lastPushIndex &&
caseServices[parsedConnectorId].hasDataToPush;
@ -577,14 +575,9 @@ export const UserActionTree = React.memo(
}
// title, description, comment updates, tags
if (
action.actionField.length === 1 &&
['title', 'description', 'comment', 'tags', 'status'].includes(action.actionField[0])
) {
const myField = action.actionField[0];
if (['title', 'description', 'comment', 'tags', 'status'].includes(action.type)) {
const label: string | JSX.Element = getLabelTitle({
action,
field: myField,
});
return [

View file

@ -5,8 +5,6 @@
* 2.0.
*/
// TODO: removed dependencies on UrlGetSearch
import React from 'react';
import { mount, ReactWrapper } from 'enzyme';
import copy from 'copy-to-clipboard';

View file

@ -7,26 +7,31 @@
import { ActionLicense, AllCases, Case, CasesStatus, CaseUserActions, Comment } from './types';
import { isCreateConnector, isPush, isUpdateConnector } from '../../common/utils/user_actions';
import { CaseMetrics, CaseMetricsFeature, ResolvedCase } from '../../common/ui/types';
import type { ResolvedCase, CaseMetrics, CaseMetricsFeature } from '../../common/ui/types';
import {
Actions,
ActionTypes,
AssociationType,
CaseUserActionConnector,
CaseConnector,
CaseResponse,
CasesFindResponse,
CasesResponse,
CasesStatusResponse,
CaseStatuses,
CaseType,
CaseUserActionResponse,
CaseUserActionsResponse,
CommentResponse,
CommentType,
ConnectorTypes,
UserAction,
UserActionField,
UserActionTypes,
UserActionWithResponse,
CommentUserAction,
} from '../../common/api';
import { SECURITY_SOLUTION_OWNER } from '../../common/constants';
import { UseGetCasesState, DEFAULT_FILTER_OPTIONS, DEFAULT_QUERY_PARAMS } from './use_get_cases';
import { SnakeToCamelCase } from '../../common/types';
export { connectorsMock } from './configure/mock';
export const basicCaseId = 'basic-case-id';
@ -274,15 +279,13 @@ export const pushedCase: Case = {
};
const basicAction = {
actionAt: basicCreatedAt,
actionBy: elasticUser,
oldValConnectorId: null,
oldValue: null,
newValConnectorId: null,
newValue: 'what a cool value',
createdAt: basicCreatedAt,
createdBy: elasticUser,
caseId: basicCaseId,
commentId: null,
owner: SECURITY_SOLUTION_OWNER,
payload: { title: 'a title' },
type: 'title',
};
export const cases: Case[] = [
@ -363,6 +366,7 @@ export const casesStatusSnake: CasesStatusResponse = {
export const pushConnectorId = '123';
export const pushSnake = {
connector_id: pushConnectorId,
connector_name: 'connector name',
external_id: 'external_id',
external_title: 'external title',
@ -410,130 +414,114 @@ export const allCasesSnake: CasesFindResponse = {
};
const basicActionSnake = {
action_at: basicCreatedAt,
action_by: elasticUserSnake,
old_value: null,
new_value: 'what a cool value',
created_at: basicCreatedAt,
created_by: elasticUserSnake,
case_id: basicCaseId,
comment_id: null,
owner: SECURITY_SOLUTION_OWNER,
};
export const getUserActionSnake = (af: UserActionField, a: UserAction) => {
const isPushToService = a === 'push-to-service' && af[0] === 'pushed';
export const getUserActionSnake = (
type: UserActionTypes,
action: UserAction,
payload?: Record<string, unknown>
): CaseUserActionResponse => {
const isPushToService = type === ActionTypes.pushed;
return {
...basicActionSnake,
action_id: `${af[0]}-${a}`,
action_field: af,
action: a,
comment_id: af[0] === 'comment' ? basicCommentId : null,
new_value: isPushToService ? JSON.stringify(basicPushSnake) : basicAction.newValue,
new_val_connector_id: isPushToService ? pushConnectorId : null,
old_val_connector_id: null,
};
action_id: `${type}-${action}`,
type,
action,
comment_id: type === 'comment' ? basicCommentId : null,
payload: isPushToService ? { externalService: basicPushSnake } : payload ?? basicAction.payload,
} as unknown as CaseUserActionResponse;
};
export const caseUserActionsSnake: CaseUserActionsResponse = [
getUserActionSnake(['description'], 'create'),
getUserActionSnake(['comment'], 'create'),
getUserActionSnake(['description'], 'update'),
getUserActionSnake('description', Actions.create, { description: 'a desc' }),
getUserActionSnake('comment', Actions.create, {
comment: { comment: 'a comment', type: CommentType.user, owner: SECURITY_SOLUTION_OWNER },
}),
getUserActionSnake('description', Actions.update, { description: 'a desc updated' }),
];
// user actions
export const getUserAction = (
af: UserActionField,
a: UserAction,
overrides?: Partial<CaseUserActions>
type: UserActionTypes,
action: UserAction,
overrides?: Record<string, unknown>
): CaseUserActions => {
return {
...basicAction,
actionId: `${af[0]}-${a}`,
actionField: af,
action: a,
commentId: af[0] === 'comment' ? basicCommentId : null,
...getValues(a, af, overrides),
};
actionId: `${type}-${action}`,
type,
action,
commentId: type === 'comment' ? basicCommentId : null,
payload: type === 'pushed' ? { externalService: basicPush } : basicAction.payload,
...overrides,
} as CaseUserActions;
};
const getValues = (
userAction: UserAction,
actionFields: UserActionField,
overrides?: Partial<CaseUserActions>
): Partial<CaseUserActions> => {
if (isCreateConnector(userAction, actionFields)) {
return {
newValue:
overrides?.newValue === undefined ? JSON.stringify(basicCaseSnake) : overrides.newValue,
newValConnectorId: overrides?.newValConnectorId ?? null,
oldValue: null,
oldValConnectorId: null,
};
} else if (isUpdateConnector(userAction, actionFields)) {
return {
newValue:
overrides?.newValue === undefined
? JSON.stringify({ name: 'My Connector', type: ConnectorTypes.none, fields: null })
: overrides.newValue,
newValConnectorId: overrides?.newValConnectorId ?? null,
oldValue:
overrides?.oldValue === undefined
? JSON.stringify({ name: 'My Connector2', type: ConnectorTypes.none, fields: null })
: overrides.oldValue,
oldValConnectorId: overrides?.oldValConnectorId ?? null,
};
} else if (isPush(userAction, actionFields)) {
return {
newValue:
overrides?.newValue === undefined ? JSON.stringify(basicPushSnake) : overrides?.newValue,
newValConnectorId:
overrides?.newValConnectorId === undefined ? pushConnectorId : overrides.newValConnectorId,
oldValue: overrides?.oldValue ?? null,
oldValConnectorId: overrides?.oldValConnectorId ?? null,
};
} else {
return {
newValue: overrides?.newValue === undefined ? basicAction.newValue : overrides.newValue,
newValConnectorId: overrides?.newValConnectorId ?? null,
oldValue: overrides?.oldValue ?? null,
oldValConnectorId: overrides?.oldValConnectorId ?? null,
};
}
};
export const getJiraConnectorWithoutId = (overrides?: Partial<CaseUserActionConnector>) => {
return JSON.stringify({
export const getJiraConnector = (overrides?: Partial<CaseConnector>): CaseConnector => {
return {
id: '123',
name: 'jira1',
type: ConnectorTypes.jira,
...jiraFields,
...overrides,
});
type: ConnectorTypes.jira as const,
} as CaseConnector;
};
export const jiraFields = { fields: { issueType: '10006', priority: null, parent: null } };
export const getAlertUserAction = () => ({
export const getAlertUserAction = (): SnakeToCamelCase<
UserActionWithResponse<CommentUserAction>
> => ({
...basicAction,
actionId: 'alert-action-id',
actionField: ['comment'],
action: 'create',
action: Actions.create,
commentId: 'alert-comment-id',
newValue: '{"type":"alert","alertId":"alert-id-1","index":"index-id-1"}',
type: ActionTypes.comment,
payload: {
comment: {
type: CommentType.alert,
alertId: 'alert-id-1',
index: 'index-id-1',
owner: SECURITY_SOLUTION_OWNER,
rule: {
id: 'rule-id-1',
name: 'Awesome rule',
},
},
},
});
export const getHostIsolationUserAction = () => ({
export const getHostIsolationUserAction = (): SnakeToCamelCase<
UserActionWithResponse<CommentUserAction>
> => ({
...basicAction,
actionId: 'isolate-action-id',
actionField: ['comment'] as UserActionField,
action: 'create' as UserAction,
type: ActionTypes.comment,
action: Actions.create,
commentId: 'isolate-comment-id',
newValue: 'some value',
payload: {
comment: {
type: CommentType.actions,
comment: 'a comment',
actions: { targets: [], type: 'test' },
owner: SECURITY_SOLUTION_OWNER,
},
},
});
export const caseUserActions: CaseUserActions[] = [
getUserAction(['description'], 'create'),
getUserAction(['comment'], 'create'),
getUserAction(['description'], 'update'),
getUserAction('description', Actions.create, { payload: { description: 'a desc' } }),
getUserAction('comment', Actions.create, {
payload: {
comment: { comment: 'a comment', type: CommentType.user, owner: SECURITY_SOLUTION_OWNER },
},
}),
getUserAction('description', Actions.update, { payload: { description: 'a desc updated' } }),
];
// components tests

View file

@ -15,14 +15,14 @@ import {
import {
basicCase,
basicPush,
basicPushSnake,
caseUserActions,
elasticUser,
getJiraConnectorWithoutId,
getJiraConnector,
getUserAction,
jiraFields,
} from './mock';
import * as api from './api';
import { Actions } from '../../common/api';
jest.mock('./api');
jest.mock('../common/lib/kibana');
@ -72,7 +72,7 @@ describe('useGetCaseUserActions', () => {
await waitForNextUpdate();
expect(result.current).toEqual({
...initialData,
caseUserActions: caseUserActions.slice(1),
caseUserActions,
fetchCaseUserActions: result.current.fetchCaseUserActions,
hasDataToPush: true,
isError: false,
@ -118,7 +118,7 @@ describe('useGetCaseUserActions', () => {
describe('getPushedInfo', () => {
it('Correctly marks first/last index - hasDataToPush: false', () => {
const userActions = [...caseUserActions, getUserAction(['pushed'], 'push-to-service')];
const userActions = [...caseUserActions, getUserAction('pushed', Actions.push_to_service)];
const result = getPushedInfo(userActions, '123');
expect(result).toEqual({
hasDataToPush: false,
@ -137,8 +137,8 @@ describe('useGetCaseUserActions', () => {
it('Correctly marks first/last index and comment id - hasDataToPush: true', () => {
const userActions = [
...caseUserActions,
getUserAction(['pushed'], 'push-to-service'),
getUserAction(['comment'], 'create'),
getUserAction('pushed', Actions.push_to_service),
getUserAction('comment', Actions.create),
];
const result = getPushedInfo(userActions, '123');
expect(result).toEqual({
@ -158,9 +158,9 @@ describe('useGetCaseUserActions', () => {
it('Correctly marks first/last index and multiple comment ids, both needs push', () => {
const userActions = [
...caseUserActions,
getUserAction(['pushed'], 'push-to-service'),
getUserAction(['comment'], 'create'),
{ ...getUserAction(['comment'], 'create'), commentId: 'muahaha' },
getUserAction('pushed', Actions.push_to_service),
getUserAction('comment', Actions.create),
{ ...getUserAction('comment', Actions.create), commentId: 'muahaha' },
];
const result = getPushedInfo(userActions, '123');
expect(result).toEqual({
@ -183,10 +183,10 @@ describe('useGetCaseUserActions', () => {
it('Correctly marks first/last index and multiple comment ids, one needs push', () => {
const userActions = [
...caseUserActions,
getUserAction(['pushed'], 'push-to-service'),
getUserAction(['comment'], 'create'),
getUserAction(['pushed'], 'push-to-service'),
{ ...getUserAction(['comment'], 'create'), commentId: 'muahaha' },
getUserAction('pushed', Actions.push_to_service),
getUserAction('comment', Actions.create),
getUserAction('pushed', Actions.push_to_service),
{ ...getUserAction('comment', Actions.create), commentId: 'muahaha' },
];
const result = getPushedInfo(userActions, '123');
expect(result).toEqual({
@ -206,12 +206,12 @@ describe('useGetCaseUserActions', () => {
it('Correctly marks first/last index and multiple comment ids, one needs push and one needs update', () => {
const userActions = [
...caseUserActions,
getUserAction(['pushed'], 'push-to-service'),
getUserAction(['comment'], 'create'),
getUserAction(['pushed'], 'push-to-service'),
{ ...getUserAction(['comment'], 'create'), commentId: 'muahaha' },
getUserAction(['comment'], 'update'),
getUserAction(['comment'], 'update'),
getUserAction('pushed', Actions.push_to_service),
getUserAction('comment', Actions.create),
getUserAction('pushed', Actions.push_to_service),
{ ...getUserAction('comment', Actions.create), commentId: 'muahaha' },
getUserAction('comment', Actions.update),
getUserAction('comment', Actions.update),
];
const result = getPushedInfo(userActions, '123');
expect(result).toEqual({
@ -234,8 +234,8 @@ describe('useGetCaseUserActions', () => {
it('Does not count connector update as a reason to push', () => {
const userActions = [
...caseUserActions,
getUserAction(['pushed'], 'push-to-service'),
getUserAction(['connector'], 'update'),
getUserAction('pushed', Actions.push_to_service),
getUserAction('connector', Actions.update),
];
const result = getPushedInfo(userActions, '123');
expect(result).toEqual({
@ -255,9 +255,9 @@ describe('useGetCaseUserActions', () => {
it('Correctly handles multiple push actions', () => {
const userActions = [
...caseUserActions,
getUserAction(['pushed'], 'push-to-service'),
getUserAction(['comment'], 'create'),
getUserAction(['pushed'], 'push-to-service'),
getUserAction('pushed', Actions.push_to_service),
getUserAction('comment', Actions.create),
getUserAction('pushed', Actions.push_to_service),
];
const result = getPushedInfo(userActions, '123');
expect(result).toEqual({
@ -277,10 +277,10 @@ describe('useGetCaseUserActions', () => {
it('Correctly handles comment update with multiple push actions', () => {
const userActions = [
...caseUserActions,
getUserAction(['pushed'], 'push-to-service'),
getUserAction(['comment'], 'create'),
getUserAction(['pushed'], 'push-to-service'),
getUserAction(['comment'], 'update'),
getUserAction('pushed', Actions.push_to_service),
getUserAction('comment', Actions.create),
getUserAction('pushed', Actions.push_to_service),
getUserAction('comment', Actions.update),
];
const result = getPushedInfo(userActions, '123');
expect(result).toEqual({
@ -298,22 +298,22 @@ describe('useGetCaseUserActions', () => {
});
it('Multiple connector tracking - hasDataToPush: true', () => {
const pushAction123 = getUserAction(['pushed'], 'push-to-service');
const pushAction123 = getUserAction('pushed', Actions.push_to_service);
const push456 = {
...basicPushSnake,
connector_name: 'other connector name',
external_id: 'other_external_id',
...basicPush,
connectorId: '456',
connectorName: 'other connector name',
externalId: 'other_external_id',
};
const pushAction456 = getUserAction(['pushed'], 'push-to-service', {
newValue: JSON.stringify(push456),
newValConnectorId: '456',
const pushAction456 = getUserAction('pushed', Actions.push_to_service, {
payload: { externalService: push456 },
});
const userActions = [
...caseUserActions,
pushAction123,
getUserAction(['comment'], 'create'),
getUserAction('comment', Actions.create),
pushAction456,
];
@ -344,22 +344,22 @@ describe('useGetCaseUserActions', () => {
});
it('Multiple connector tracking - hasDataToPush: false', () => {
const pushAction123 = getUserAction(['pushed'], 'push-to-service');
const pushAction123 = getUserAction('pushed', Actions.push_to_service);
const push456 = {
...basicPushSnake,
connector_name: 'other connector name',
external_id: 'other_external_id',
...basicPush,
connectorId: '456',
connectorName: 'other connector name',
externalId: 'other_external_id',
};
const pushAction456 = getUserAction(['pushed'], 'push-to-service', {
newValue: JSON.stringify(push456),
newValConnectorId: '456',
const pushAction456 = getUserAction('pushed', Actions.push_to_service, {
payload: { externalService: push456 },
});
const userActions = [
...caseUserActions,
pushAction123,
getUserAction(['comment'], 'create'),
getUserAction('comment', Actions.create),
pushAction456,
];
@ -391,8 +391,9 @@ describe('useGetCaseUserActions', () => {
it('Change fields of current connector - hasDataToPush: true', () => {
const userActions = [
...caseUserActions,
getUserAction(['pushed'], 'push-to-service'),
createUpdateConnectorFields123HighPriorityUserAction(),
createUpdate123HighPriorityConnector(),
getUserAction('pushed', Actions.push_to_service),
createUpdate123LowPriorityConnector(),
];
const result = getPushedInfo(userActions, '123');
@ -401,8 +402,8 @@ describe('useGetCaseUserActions', () => {
caseServices: {
'123': {
...basicPush,
firstPushIndex: 3,
lastPushIndex: 3,
firstPushIndex: 4,
lastPushIndex: 4,
commentsToUpdate: [],
hasDataToPush: true,
},
@ -413,8 +414,8 @@ describe('useGetCaseUserActions', () => {
it('Change current connector - hasDataToPush: true', () => {
const userActions = [
...caseUserActions,
getUserAction(['pushed'], 'push-to-service'),
createChangeConnector123To456UserAction(),
getUserAction('pushed', Actions.push_to_service),
createUpdate456HighPriorityConnector(),
];
const result = getPushedInfo(userActions, '123');
@ -435,9 +436,9 @@ describe('useGetCaseUserActions', () => {
it('Change connector and back - hasDataToPush: true', () => {
const userActions = [
...caseUserActions,
getUserAction(['pushed'], 'push-to-service'),
createChangeConnector123To456UserAction(),
createChangeConnector456To123UserAction(),
getUserAction('pushed', Actions.push_to_service),
createUpdate456HighPriorityConnector(),
createUpdate123HighPriorityConnector(),
];
const result = getPushedInfo(userActions, '123');
@ -458,10 +459,10 @@ describe('useGetCaseUserActions', () => {
it('Change fields and connector after push - hasDataToPush: true', () => {
const userActions = [
...caseUserActions,
createUpdateConnectorFields123HighPriorityUserAction(),
getUserAction(['pushed'], 'push-to-service'),
createChangeConnector123HighPriorityTo456UserAction(),
createChangeConnector456To123PriorityLowUserAction(),
createUpdate123HighPriorityConnector(),
getUserAction('pushed', Actions.push_to_service),
createUpdate456HighPriorityConnector(),
createUpdate123LowPriorityConnector(),
];
const result = getPushedInfo(userActions, '123');
@ -482,10 +483,10 @@ describe('useGetCaseUserActions', () => {
it('Change only connector after push - hasDataToPush: false', () => {
const userActions = [
...caseUserActions,
createUpdateConnectorFields123HighPriorityUserAction(),
getUserAction(['pushed'], 'push-to-service'),
createChangeConnector123HighPriorityTo456UserAction(),
createChangeConnector456To123HighPriorityUserAction(),
createUpdate123HighPriorityConnector(),
getUserAction('pushed', Actions.push_to_service),
createUpdate456HighPriorityConnector(),
createUpdate123HighPriorityConnector(),
];
const result = getPushedInfo(userActions, '123');
@ -504,27 +505,27 @@ describe('useGetCaseUserActions', () => {
});
it('Change connectors and fields - multiple pushes', () => {
const pushAction123 = getUserAction(['pushed'], 'push-to-service');
const pushAction123 = getUserAction('pushed', Actions.push_to_service);
const push456 = {
...basicPushSnake,
connector_name: 'other connector name',
external_id: 'other_external_id',
...basicPush,
connectorId: '456',
connectorName: 'other connector name',
externalId: 'other_external_id',
};
const pushAction456 = getUserAction(['pushed'], 'push-to-service', {
newValue: JSON.stringify(push456),
newValConnectorId: '456',
const pushAction456 = getUserAction('pushed', Actions.push_to_service, {
payload: { externalService: push456 },
});
const userActions = [
...caseUserActions,
createUpdateConnectorFields123HighPriorityUserAction(),
createUpdate123HighPriorityConnector(),
pushAction123,
createChangeConnector123HighPriorityTo456UserAction(),
createUpdate456HighPriorityConnector(),
pushAction456,
createChangeConnector456To123PriorityLowUserAction(),
createChangeConnector123LowPriorityTo456UserAction(),
createChangeConnector456To123PriorityLowUserAction(),
createUpdate123LowPriorityConnector(),
createUpdate456HighPriorityConnector(),
createUpdate123LowPriorityConnector(),
];
const result = getPushedInfo(userActions, '123');
@ -553,25 +554,25 @@ describe('useGetCaseUserActions', () => {
});
it('pushing other connectors does not count as an update', () => {
const pushAction123 = getUserAction(['pushed'], 'push-to-service');
const pushAction123 = getUserAction('pushed', Actions.push_to_service);
const push456 = {
...basicPushSnake,
connector_name: 'other connector name',
external_id: 'other_external_id',
...basicPush,
connectorId: '456',
connectorName: 'other connector name',
externalId: 'other_external_id',
};
const pushAction456 = getUserAction(['pushed'], 'push-to-service', {
newValConnectorId: '456',
newValue: JSON.stringify(push456),
const pushAction456 = getUserAction('pushed', Actions.push_to_service, {
payload: { externalService: push456 },
});
const userActions = [
...caseUserActions,
createUpdateConnectorFields123HighPriorityUserAction(),
createUpdate123HighPriorityConnector(),
pushAction123,
createChangeConnector123HighPriorityTo456UserAction(),
createUpdate456HighPriorityConnector(),
pushAction456,
createChangeConnector456To123HighPriorityUserAction(),
createUpdate123HighPriorityConnector(),
];
const result = getPushedInfo(userActions, '123');
@ -602,10 +603,10 @@ describe('useGetCaseUserActions', () => {
it('Changing other connectors fields does not count as an update', () => {
const userActions = [
...caseUserActions,
createUpdateConnectorFields123HighPriorityUserAction(),
getUserAction(['pushed'], 'push-to-service'),
createChangeConnector123HighPriorityTo456UserAction(),
createUpdateConnectorFields456HighPriorityUserAction(),
createUpdate123HighPriorityConnector(),
getUserAction('pushed', Actions.push_to_service),
createUpdate456HighPriorityConnector(),
createUpdate456HighPriorityConnector(),
];
const result = getPushedInfo(userActions, '123');
@ -638,69 +639,21 @@ const jira456Fields = {
};
const jira456HighPriorityFields = {
id: '456',
fields: { ...jira456Fields.fields, priority: 'High' },
};
const createUpdateConnectorFields123HighPriorityUserAction = () =>
getUserAction(['connector'], 'update', {
oldValue: getJiraConnectorWithoutId(),
newValue: getJiraConnectorWithoutId(jira123HighPriorityFields),
oldValConnectorId: '123',
newValConnectorId: '123',
const createUpdate123HighPriorityConnector = () =>
getUserAction('connector', Actions.update, {
payload: { connector: getJiraConnector(jira123HighPriorityFields) },
});
const createUpdateConnectorFields456HighPriorityUserAction = () =>
getUserAction(['connector'], 'update', {
oldValue: getJiraConnectorWithoutId(jira456Fields),
newValue: getJiraConnectorWithoutId(jira456HighPriorityFields),
oldValConnectorId: '456',
newValConnectorId: '456',
const createUpdate123LowPriorityConnector = () =>
getUserAction('connector', Actions.update, {
payload: { connector: getJiraConnector(jira123LowPriorityFields) },
});
const createChangeConnector123HighPriorityTo456UserAction = () =>
getUserAction(['connector'], 'update', {
oldValue: getJiraConnectorWithoutId(jira123HighPriorityFields),
oldValConnectorId: '123',
newValue: getJiraConnectorWithoutId(jira456Fields),
newValConnectorId: '456',
});
const createChangeConnector123To456UserAction = () =>
getUserAction(['connector'], 'update', {
oldValue: getJiraConnectorWithoutId(),
oldValConnectorId: '123',
newValue: getJiraConnectorWithoutId(jira456Fields),
newValConnectorId: '456',
});
const createChangeConnector123LowPriorityTo456UserAction = () =>
getUserAction(['connector'], 'update', {
oldValue: getJiraConnectorWithoutId(jira123LowPriorityFields),
oldValConnectorId: '123',
newValue: getJiraConnectorWithoutId(jira456Fields),
newValConnectorId: '456',
});
const createChangeConnector456To123UserAction = () =>
getUserAction(['connector'], 'update', {
oldValue: getJiraConnectorWithoutId(jira456Fields),
oldValConnectorId: '456',
newValue: getJiraConnectorWithoutId(),
newValConnectorId: '123',
});
const createChangeConnector456To123HighPriorityUserAction = () =>
getUserAction(['connector'], 'update', {
oldValue: getJiraConnectorWithoutId(jira456Fields),
oldValConnectorId: '456',
newValue: getJiraConnectorWithoutId(jira123HighPriorityFields),
newValConnectorId: '123',
});
const createChangeConnector456To123PriorityLowUserAction = () =>
getUserAction(['connector'], 'update', {
oldValue: getJiraConnectorWithoutId(jira456Fields),
oldValConnectorId: '456',
newValue: getJiraConnectorWithoutId(jira123LowPriorityFields),
newValConnectorId: '123',
const createUpdate456HighPriorityConnector = () =>
getUserAction('connector', Actions.update, {
payload: { connector: getJiraConnector(jira456HighPriorityFields) },
});

View file

@ -10,12 +10,15 @@ import { useCallback, useEffect, useState, useRef } from 'react';
import deepEqual from 'fast-deep-equal';
import { ElasticUser, CaseUserActions, CaseExternalService } from '../../common/ui/types';
import { CaseFullExternalService, CaseConnector } from '../../common/api';
import { ActionTypes, CaseConnector, NONE_CONNECTOR_ID } from '../../common/api';
import { getCaseUserActions, getSubCaseUserActions } from './api';
import * as i18n from './translations';
import { convertToCamelCase } from './utils';
import { parseStringAsConnector, parseStringAsExternalService } from '../common/user_actions';
import { useToasts } from '../common/lib/kibana';
import {
isPushedUserAction,
isConnectorUserAction,
isCreateCaseUserAction,
} from '../../common/utils/user_actions';
export interface CaseService extends CaseExternalService {
firstPushIndex: number;
@ -54,55 +57,23 @@ export interface UseGetCaseUserActions extends CaseUserActionsState {
) => Promise<void>;
}
const unknownExternalServiceConnectorId = 'unknown';
const getExternalService = (
connectorId: string | null,
encodedValue: string | null
): CaseExternalService | null => {
const decodedValue = parseStringAsExternalService(connectorId, encodedValue);
if (decodedValue == null) {
return null;
}
return {
...convertToCamelCase<CaseFullExternalService, CaseExternalService>(decodedValue),
// if in the rare case that the connector id is null we'll set it to unknown if we need to reference it in the UI
// anywhere. The id would only ever be null if a migration failed or some logic error within the backend occurred
connectorId: connectorId ?? unknownExternalServiceConnectorId,
};
};
const groupConnectorFields = (
userActions: CaseUserActions[]
): Record<string, Array<CaseConnector['fields']>> =>
userActions.reduce((acc, mua) => {
if (mua.actionField[0] !== 'connector') {
return acc;
if (
(isConnectorUserAction(mua) || isCreateCaseUserAction(mua)) &&
mua.payload?.connector?.id !== NONE_CONNECTOR_ID
) {
const connector = mua.payload.connector;
return {
...acc,
[connector.id]: [...(acc[connector.id] || []), connector.fields],
};
}
const oldConnector = parseStringAsConnector(mua.oldValConnectorId, mua.oldValue);
const newConnector = parseStringAsConnector(mua.newValConnectorId, mua.newValue);
if (!oldConnector || !newConnector) {
return acc;
}
return {
...acc,
[oldConnector.id]: [
...(acc[oldConnector.id] || []),
...(oldConnector.id === newConnector.id
? [oldConnector.fields, newConnector.fields]
: [oldConnector.fields]),
],
[newConnector.id]: [
...(acc[newConnector.id] || []),
...(oldConnector.id === newConnector.id
? [oldConnector.fields, newConnector.fields]
: [newConnector.fields]),
],
};
return acc;
}, {} as Record<string, Array<CaseConnector['fields']>>);
const connectorHasChangedFields = ({
@ -153,7 +124,9 @@ export const getPushedInfo = (
const hasDataToPushForConnector = (connectorId: string): boolean => {
const caseUserActionsReversed = [...caseUserActions].reverse();
const lastPushOfConnectorReversedIndex = caseUserActionsReversed.findIndex(
(mua) => mua.action === 'push-to-service' && mua.newValConnectorId === connectorId
(mua) =>
isPushedUserAction<'camelCase'>(mua) &&
mua.payload.externalService.connectorId === connectorId
);
if (lastPushOfConnectorReversedIndex === -1) {
@ -180,14 +153,14 @@ export const getPushedInfo = (
return (
actionsAfterPush.some(
(mua) => mua.actionField[0] !== 'connector' && mua.action !== 'push-to-service'
(mua) => mua.type !== ActionTypes.connector && mua.type !== ActionTypes.pushed
) || connectorHasChanged
);
};
const commentsAndIndex = caseUserActions.reduce<CommentsAndIndex[]>(
(bacc, mua, index) =>
mua.actionField[0] === 'comment' && mua.commentId != null
mua.type === ActionTypes.comment && mua.commentId != null
? [
...bacc,
{
@ -200,11 +173,11 @@ export const getPushedInfo = (
);
let caseServices = caseUserActions.reduce<CaseServices>((acc, cua, i) => {
if (cua.action !== 'push-to-service') {
if (!isPushedUserAction<'camelCase'>(cua)) {
return acc;
}
const externalService = getExternalService(cua.newValConnectorId, cua.newValue);
const externalService = cua.payload.externalService;
if (externalService === null) {
return acc;
}
@ -290,14 +263,10 @@ export const useGetCaseUserActions = (
// We are removing the first item because it will always be the creation of the case
// and we do not want it to simplify our life
const participants = !isEmpty(response)
? uniqBy('actionBy.username', response).map((cau) => cau.actionBy)
? uniqBy('createdBy.username', response).map((cau) => cau.createdBy)
: [];
const caseUserActions = !isEmpty(response)
? thisSubCaseId
? response
: response.slice(1)
: [];
const caseUserActions = !isEmpty(response) ? response : [];
setCaseUserActionsState({
caseUserActions,

View file

@ -16,7 +16,6 @@ export interface UseMessagesStorage {
hasMessage: (plugin: string, id: string) => boolean;
}
// TODO: Removed const { storage } = useKibana().services; in favor of using the util directly
export const useMessagesStorage = (): UseMessagesStorage => {
const storage = useMemo(() => new Storage(localStorage), []);

View file

@ -20,6 +20,8 @@ import {
import { LensServerPluginSetup } from '../../../../lens/server';
import {
Actions,
ActionTypes,
AlertCommentRequestRt,
CaseResponse,
CaseStatuses,
@ -36,10 +38,6 @@ import {
ENABLE_CASE_CONNECTOR,
MAX_GENERATED_ALERTS_PER_SUB_CASE,
} from '../../../common/constants';
import {
buildCaseUserActionItem,
buildCommentUserActionItem,
} from '../../services/user_actions/helpers';
import { AttachmentService, CasesService, CaseUserActionService } from '../../services';
import { CommentableCase } from '../../common/models';
@ -95,21 +93,7 @@ async function getSubCase({
caseId,
createdBy: user,
});
await userActionService.bulkCreate({
unsecuredSavedObjectsClient,
actions: [
buildCaseUserActionItem({
action: 'create',
actionAt: createdAt,
actionBy: user,
caseId,
subCaseId: newSubCase.id,
fields: ['status', 'sub_case'],
newValue: { status: newSubCase.attributes.status },
owner: newSubCase.attributes.owner,
}),
],
});
return newSubCase;
}
@ -127,6 +111,7 @@ const addGeneratedAlerts = async (
lensEmbeddableFactory,
authorization,
alertsService,
user,
} = clientArgs;
const query = pipe(
@ -207,21 +192,18 @@ const addGeneratedAlerts = async (
await alertsService.updateAlertsStatus(alertsToUpdate);
}
await userActionService.bulkCreate({
await userActionService.createUserAction({
type: ActionTypes.comment,
action: Actions.create,
unsecuredSavedObjectsClient,
actions: [
buildCommentUserActionItem({
action: 'create',
actionAt: createdDate,
actionBy: { ...userDetails },
caseId: updatedCase.caseId,
subCaseId: updatedCase.subCaseId,
commentId: newComment.id,
fields: ['comment'],
newValue: query,
owner: newComment.attributes.owner,
}),
],
caseId: updatedCase.caseId,
subCaseId: updatedCase.subCaseId,
payload: {
attachment: query,
},
attachmentId: newComment.id,
user,
owner: newComment.attributes.owner,
});
return updatedCase.encode();
@ -394,21 +376,18 @@ export const addComment = async (
await alertsService.updateAlertsStatus(alertsToUpdate);
}
await userActionService.bulkCreate({
await userActionService.createUserAction({
type: ActionTypes.comment,
action: Actions.create,
unsecuredSavedObjectsClient,
actions: [
buildCommentUserActionItem({
action: 'create',
actionAt: createdDate,
actionBy: { username, full_name, email },
caseId: updatedCase.caseId,
subCaseId: updatedCase.subCaseId,
commentId: newComment.id,
fields: ['comment'],
newValue: query,
owner: newComment.attributes.owner,
}),
],
caseId,
subCaseId: updatedCase.subCaseId,
attachmentId: newComment.id,
payload: {
attachment: query,
},
user,
owner: newComment.attributes.owner,
});
return updatedCase.encode();

View file

@ -9,14 +9,13 @@ import Boom from '@hapi/boom';
import pMap from 'p-map';
import { SavedObject } from 'kibana/public';
import { AssociationType, CommentAttributes } from '../../../common/api';
import { Actions, ActionTypes, AssociationType, CommentAttributes } from '../../../common/api';
import {
CASE_SAVED_OBJECT,
MAX_CONCURRENT_SEARCHES,
SUB_CASE_SAVED_OBJECT,
} from '../../../common/constants';
import { CasesClientArgs } from '../types';
import { buildCommentUserActionItem } from '../../services/user_actions/helpers';
import { createCaseError } from '../../common/error';
import { checkEnabledCaseConnectorOrThrow } from '../../common/utils';
import { Operations } from '../../authorization';
@ -105,22 +104,16 @@ export async function deleteAll(
concurrency: MAX_CONCURRENT_SEARCHES,
});
const deleteDate = new Date().toISOString();
await userActionService.bulkCreate({
await userActionService.bulkCreateAttachmentDeletion({
unsecuredSavedObjectsClient,
actions: comments.saved_objects.map((comment) =>
buildCommentUserActionItem({
action: 'delete',
actionAt: deleteDate,
actionBy: user,
caseId: caseID,
subCaseId: subCaseID,
commentId: comment.id,
fields: ['comment'],
owner: comment.attributes.owner,
})
),
caseId: caseID,
subCaseId: subCaseID,
attachments: comments.saved_objects.map((comment) => ({
id: comment.id,
owner: comment.attributes.owner,
attachment: comment.attributes,
})),
user,
});
} catch (error) {
throw createCaseError({
@ -152,8 +145,6 @@ export async function deleteComment(
try {
checkEnabledCaseConnectorOrThrow(subCaseID);
const deleteDate = new Date().toISOString();
const myComment = await attachmentService.get({
unsecuredSavedObjectsClient,
attachmentId: attachmentID,
@ -181,20 +172,16 @@ export async function deleteComment(
attachmentId: attachmentID,
});
await userActionService.bulkCreate({
await userActionService.createUserAction({
type: ActionTypes.comment,
action: Actions.delete,
unsecuredSavedObjectsClient,
actions: [
buildCommentUserActionItem({
action: 'delete',
actionAt: deleteDate,
actionBy: user,
caseId: id,
subCaseId: subCaseID,
commentId: attachmentID,
fields: ['comment'],
owner: myComment.attributes.owner,
}),
],
caseId: id,
subCaseId: subCaseID,
attachmentId: attachmentID,
payload: { attachment: { ...myComment.attributes } },
user,
owner: myComment.attributes.owner,
});
} catch (error) {
throw createCaseError({

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import { pick } from 'lodash/fp';
import Boom from '@hapi/boom';
import { SavedObjectsClientContract, Logger } from 'kibana/server';
@ -13,8 +12,7 @@ import { LensServerPluginSetup } from '../../../../lens/server';
import { CommentableCase } from '../../common/models';
import { createCaseError } from '../../common/error';
import { checkEnabledCaseConnectorOrThrow } from '../../common/utils';
import { buildCommentUserActionItem } from '../../services/user_actions/helpers';
import { CaseResponse, CommentPatchRequest, CommentRequest } from '../../../common/api';
import { Actions, ActionTypes, CaseResponse, CommentPatchRequest } from '../../../common/api';
import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../../common/constants';
import { AttachmentService, CasesService } from '../../services';
import { CasesClientArgs } from '..';
@ -180,26 +178,16 @@ export async function update(
user,
});
await userActionService.bulkCreate({
await userActionService.createUserAction({
type: ActionTypes.comment,
action: Actions.update,
unsecuredSavedObjectsClient,
actions: [
buildCommentUserActionItem({
action: 'update',
actionAt: updatedDate,
actionBy: user,
caseId: caseID,
subCaseId: subCaseID,
commentId: updatedComment.id,
fields: ['comment'],
// casting because typescript is complaining that it's not a Record<string, unknown> even though it is
newValue: queryRestAttributes as CommentRequest,
oldValue:
// We are interested only in ContextBasicRt attributes
// myComment.attribute contains also CommentAttributesBasicRt attributes
pick(Object.keys(queryRestAttributes), myComment.attributes),
owner: myComment.attributes.owner,
}),
],
caseId: caseID,
subCaseId: subCaseID,
attachmentId: updatedComment.id,
payload: { attachment: queryRestAttributes },
user,
owner: myComment.attributes.owner,
});
return await updatedCase.encode();

View file

@ -20,10 +20,9 @@ import {
CasesClientPostRequestRt,
CasePostRequest,
CaseType,
OWNER_FIELD,
ActionTypes,
} from '../../../common/api';
import { ENABLE_CASE_CONNECTOR, MAX_TITLE_LENGTH } from '../../../common/constants';
import { buildCaseUserActionItem } from '../../services/user_actions/helpers';
import { Operations } from '../../authorization';
import { createCaseError } from '../../common/error';
@ -80,36 +79,22 @@ export const create = async (
entities: [{ owner: query.owner, id: savedObjectID }],
});
// eslint-disable-next-line @typescript-eslint/naming-convention
const { username, full_name, email } = user;
const createdDate = new Date().toISOString();
const newCase = await caseService.postNewCase({
unsecuredSavedObjectsClient,
attributes: transformNewCase({
createdDate,
user,
newCase: query,
username,
full_name,
email,
connector: query.connector,
}),
id: savedObjectID,
});
await userActionService.bulkCreate({
await userActionService.createUserAction({
type: ActionTypes.create_case,
unsecuredSavedObjectsClient,
actions: [
buildCaseUserActionItem({
action: 'create',
actionAt: createdDate,
actionBy: { username, full_name, email },
caseId: newCase.id,
fields: ['description', 'status', 'tags', 'title', 'connector', 'settings', OWNER_FIELD],
newValue: query,
owner: newCase.attributes.owner,
}),
],
caseId: newCase.id,
user,
payload: query,
owner: newCase.attributes.owner,
});
return CaseResponseRt.encode(

View file

@ -8,12 +8,11 @@
import pMap from 'p-map';
import { Boom } from '@hapi/boom';
import { SavedObject, SavedObjectsClientContract, SavedObjectsFindResponse } from 'kibana/server';
import { CommentAttributes, SubCaseAttributes, OWNER_FIELD } from '../../../common/api';
import { CommentAttributes, SubCaseAttributes } from '../../../common/api';
import { ENABLE_CASE_CONNECTOR, MAX_CONCURRENT_SEARCHES } from '../../../common/constants';
import { CasesClientArgs } from '..';
import { createCaseError } from '../../common/error';
import { AttachmentService, CasesService } from '../../services';
import { buildCaseUserActionItem } from '../../services/user_actions/helpers';
import { Operations, OwnerEntity } from '../../authorization';
async function deleteSubCases({
@ -144,30 +143,14 @@ export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): P
});
}
const deleteDate = new Date().toISOString();
await userActionService.bulkCreate({
await userActionService.bulkCreateCaseDeletion({
unsecuredSavedObjectsClient,
actions: cases.saved_objects.map((caseInfo) =>
buildCaseUserActionItem({
action: 'delete',
actionAt: deleteDate,
actionBy: user,
caseId: caseInfo.id,
fields: [
'description',
'status',
'tags',
'title',
'connector',
'settings',
OWNER_FIELD,
'comment',
...(ENABLE_CASE_CONNECTOR ? ['sub_case' as const] : []),
],
owner: caseInfo.attributes.owner,
})
),
cases: cases.saved_objects.map((caseInfo) => ({
id: caseInfo.id,
owner: caseInfo.attributes.owner,
connectorId: caseInfo.attributes.connector.id,
})),
user,
});
} catch (error) {
throw createCaseError({

View file

@ -12,6 +12,8 @@ import {
CaseUserActionsResponse,
AssociationType,
CommentResponseAlertsType,
ConnectorTypes,
Actions,
} from '../../../common/api';
import { SECURITY_SOLUTION_OWNER } from '../../../common/constants';
@ -222,111 +224,151 @@ export const mappings: ConnectorMappingsAttributes[] = [
export const userActions: CaseUserActionsResponse = [
{
action_field: ['description', 'status', 'tags', 'title', 'connector', 'settings'],
action: 'create',
action_at: '2021-02-03T17:41:03.771Z',
action_by: {
action: Actions.create,
type: 'create_case',
created_at: '2021-02-03T17:41:03.771Z',
created_by: {
email: 'elastic@elastic.co',
full_name: 'Elastic',
username: 'elastic',
},
new_value:
'{"title":"Case SIR","tags":["sir"],"description":"testing sir","connector":{"name":"ServiceNow SN","type":".servicenow-sir","fields":{"category":"Denial of Service","destIp":true,"malwareHash":true,"malwareUrl":true,"priority":"2","sourceIp":true,"subcategory":"45"}},"settings":{"syncAlerts":true}}',
new_val_connector_id: '456',
old_value: null,
old_val_connector_id: null,
payload: {
title: 'Case SIR',
tags: ['sir'],
description: 'testing sir',
connector: {
id: '456',
name: 'ServiceNow SN',
type: ConnectorTypes.serviceNowSIR,
fields: {
category: 'Denial of Service',
destIp: true,
malwareHash: true,
malwareUrl: true,
priority: '2',
sourceIp: true,
subcategory: '45',
},
},
settings: { syncAlerts: true },
status: 'open',
owner: SECURITY_SOLUTION_OWNER,
},
action_id: 'fd830c60-6646-11eb-a291-51bf6b175a53',
case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53',
comment_id: null,
owner: SECURITY_SOLUTION_OWNER,
},
{
action_field: ['pushed'],
action: 'push-to-service',
action_at: '2021-02-03T17:41:26.108Z',
action_by: {
type: 'pushed',
action: Actions.push_to_service,
created_at: '2021-02-03T17:41:26.108Z',
created_by: {
email: 'elastic@elastic.co',
full_name: 'Elastic',
username: 'elastic',
},
new_value:
'{"pushed_at":"2021-02-03T17:41:26.108Z","pushed_by":{"username":"elastic","full_name":"Elastic","email":"elastic@elastic.co"},"connector_name":"ServiceNow SN","external_id":"external-id","external_title":"SIR0010037","external_url":"https://dev92273.service-now.com/nav_to.do?uri=sn_si_incident.do?sys_id=external-id"}',
new_val_connector_id: '456',
old_val_connector_id: null,
old_value: null,
payload: {
externalService: {
pushed_at: '2021-02-03T17:41:26.108Z',
pushed_by: { username: 'elastic', full_name: 'Elastic', email: 'elastic@elastic.co' },
connector_id: '456',
connector_name: 'ServiceNow SN',
external_id: 'external-id',
external_title: 'SIR0010037',
external_url:
'https://dev92273.service-now.com/nav_to.do?uri=sn_si_incident.do?sys_id=external-id',
},
},
action_id: '0a801750-6647-11eb-a291-51bf6b175a53',
case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53',
comment_id: null,
owner: SECURITY_SOLUTION_OWNER,
},
{
action_field: ['comment'],
action: 'create',
action_at: '2021-02-03T17:44:21.067Z',
action_by: {
type: 'comment',
action: Actions.create,
created_at: '2021-02-03T17:44:21.067Z',
created_by: {
email: 'elastic@elastic.co',
full_name: 'Elastic',
username: 'elastic',
},
new_value: '{"type":"alert","alertId":"alert-id-1","index":".siem-signals-default-000008"}',
new_val_connector_id: null,
old_val_connector_id: null,
old_value: null,
payload: {
comment: {
type: CommentType.alert,
alertId: 'alert-id-1',
index: '.siem-signals-default-000008',
rule: { id: '123', name: 'rule name' },
owner: SECURITY_SOLUTION_OWNER,
},
},
action_id: '7373eb60-6647-11eb-a291-51bf6b175a53',
case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53',
comment_id: 'comment-alert-1',
owner: SECURITY_SOLUTION_OWNER,
},
{
action_field: ['comment'],
action: 'create',
action_at: '2021-02-03T17:44:33.078Z',
action_by: {
type: 'comment',
action: Actions.create,
created_at: '2021-02-03T17:44:33.078Z',
created_by: {
email: 'elastic@elastic.co',
full_name: 'Elastic',
username: 'elastic',
},
new_value: '{"type":"alert","alertId":"alert-id-2","index":".siem-signals-default-000008"}',
old_value: null,
new_val_connector_id: null,
old_val_connector_id: null,
payload: {
comment: {
type: CommentType.alert,
alertId: 'alert-id-2',
index: '.siem-signals-default-000008',
rule: { id: '123', name: 'rule name' },
owner: SECURITY_SOLUTION_OWNER,
},
},
action_id: '7abc6410-6647-11eb-a291-51bf6b175a53',
case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53',
comment_id: 'comment-alert-2',
owner: SECURITY_SOLUTION_OWNER,
},
{
action_field: ['pushed'],
action: 'push-to-service',
action_at: '2021-02-03T17:45:29.400Z',
action_by: {
type: 'pushed',
action: Actions.push_to_service,
created_at: '2021-02-03T17:45:29.400Z',
created_by: {
email: 'elastic@elastic.co',
full_name: 'Elastic',
username: 'elastic',
},
new_value:
'{"pushed_at":"2021-02-03T17:45:29.400Z","pushed_by":{"username":"elastic","full_name":"Elastic","email":"elastic@elastic.co"},"connector_name":"ServiceNow SN","external_id":"external-id","external_title":"SIR0010037","external_url":"https://dev92273.service-now.com/nav_to.do?uri=sn_si_incident.do?sys_id=external-id"}',
new_val_connector_id: '456',
old_value: null,
old_val_connector_id: null,
payload: {
externalService: {
pushed_at: '2021-02-03T17:45:29.400Z',
pushed_by: { username: 'elastic', full_name: 'Elastic', email: 'elastic@elastic.co' },
connector_id: '456',
connector_name: 'ServiceNow SN',
external_id: 'external-id',
external_title: 'SIR0010037',
external_url:
'https://dev92273.service-now.com/nav_to.do?uri=sn_si_incident.do?sys_id=external-id',
},
},
action_id: '9b91d8f0-6647-11eb-a291-51bf6b175a53',
case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53',
comment_id: null,
owner: SECURITY_SOLUTION_OWNER,
},
{
action_field: ['comment'],
action: 'create',
action_at: '2021-02-03T17:48:30.616Z',
action_by: {
type: 'comment',
action: Actions.create,
created_at: '2021-02-03T17:48:30.616Z',
created_by: {
email: 'elastic@elastic.co',
full_name: 'Elastic',
username: 'elastic',
},
new_value: '{"comment":"a comment!","type":"user"}',
old_value: null,
new_val_connector_id: null,
old_val_connector_id: null,
payload: {
comment: { comment: 'a comment!', type: CommentType.user, owner: SECURITY_SOLUTION_OWNER },
},
action_id: '0818e5e0-6648-11eb-a291-51bf6b175a53',
case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53',
comment_id: 'comment-user-1',

View file

@ -17,9 +17,9 @@ import {
CaseType,
CasesConfigureAttributes,
CaseAttributes,
ActionTypes,
} from '../../../common/api';
import { ENABLE_CASE_CONNECTOR } from '../../../common/constants';
import { buildCaseUserActionItem } from '../../services/user_actions/helpers';
import { createIncident, getCommentContextFromAttributes } from './utils';
import { createCaseError } from '../../common/error';
@ -217,37 +217,28 @@ export const push = async (
version: comment.version,
})),
}),
userActionService.bulkCreate({
unsecuredSavedObjectsClient,
actions: [
...(shouldMarkAsClosed
? [
buildCaseUserActionItem({
action: 'update',
actionAt: pushedDate,
actionBy: { username, full_name, email },
caseId,
fields: ['status'],
newValue: CaseStatuses.closed,
oldValue: myCase.attributes.status,
owner: myCase.attributes.owner,
}),
]
: []),
buildCaseUserActionItem({
action: 'push-to-service',
actionAt: pushedDate,
actionBy: { username, full_name, email },
caseId,
fields: ['pushed'],
newValue: externalService,
owner: myCase.attributes.owner,
}),
],
}),
]);
if (shouldMarkAsClosed) {
await userActionService.createUserAction({
type: ActionTypes.status,
unsecuredSavedObjectsClient,
payload: { status: CaseStatuses.closed },
user,
caseId,
owner: myCase.attributes.owner,
});
}
await userActionService.createUserAction({
type: ActionTypes.pushed,
unsecuredSavedObjectsClient,
payload: { externalService },
user,
caseId,
owner: myCase.attributes.owner,
});
/* End of update case with push information */
return CaseResponseRt.encode(

View file

@ -43,7 +43,7 @@ import {
SUB_CASE_SAVED_OBJECT,
MAX_TITLE_LENGTH,
} from '../../../common/constants';
import { buildCaseUserActions } from '../../services/user_actions/helpers';
import { getCaseToUpdate } from '../utils';
import { AlertService, CasesService } from '../../services';
@ -589,14 +589,11 @@ export const update = async (
});
});
await userActionService.bulkCreate({
await userActionService.bulkCreateUpdateCase({
unsecuredSavedObjectsClient,
actions: buildCaseUserActions({
originalCases: myCases.saved_objects,
updatedCases: updatedCases.saved_objects,
actionDate: updatedDt,
actionBy: { email, full_name, username },
}),
originalCases: myCases.saved_objects,
updatedCases: updatedCases.saved_objects,
user,
});
return CasesResponseRt.encode(returnUpdatedCase);

View file

@ -31,6 +31,7 @@ import {
transformers,
transformFields,
} from './utils';
import { Actions } from '../../../common/api';
import { flattenCaseSavedObject } from '../../common/utils';
import { SECURITY_SOLUTION_OWNER } from '../../../common/constants';
import { casesConnectors } from '../../connectors';
@ -790,20 +791,30 @@ describe('utils', () => {
const res = getLatestPushInfo('456', [
...userActions.slice(0, 3),
{
action_field: ['pushed'],
action: 'push-to-service',
action_at: '2021-02-03T17:45:29.400Z',
action_by: {
type: 'pushed',
action: Actions.push_to_service,
created_at: '2021-02-03T17:45:29.400Z',
created_by: {
email: 'elastic@elastic.co',
full_name: 'Elastic',
username: 'elastic',
},
new_value:
// The connector id is 123
'{"pushed_at":"2021-02-03T17:45:29.400Z","pushed_by":{"username":"elastic","full_name":"Elastic","email":"elastic@elastic.co"},"connector_name":"ServiceNow SN","external_id":"external-id","external_title":"SIR0010037","external_url":"https://dev92273.service-now.com/nav_to.do?uri=sn_si_incident.do?sys_id=external-id"}',
new_val_connector_id: '123',
old_val_connector_id: null,
old_value: null,
payload: {
externalService: {
pushed_at: '2021-02-03T17:45:29.400Z',
pushed_by: {
username: 'elastic',
full_name: 'Elastic',
email: 'elastic@elastic.co',
},
connector_id: '123',
connector_name: 'ServiceNow SN',
external_id: 'external-id',
external_title: 'SIR0010037',
external_url:
'https://dev92273.service-now.com/nav_to.do?uri=sn_si_incident.do?sys_id=external-id',
},
},
action_id: '9b91d8f0-6647-11eb-a291-51bf6b175a53',
case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53',
comment_id: null,

View file

@ -7,7 +7,7 @@
import { i18n } from '@kbn/i18n';
import { flow } from 'lodash';
import { isPush } from '../../../common/utils/user_actions';
import { isPushedUserAction } from '../../../common/utils/user_actions';
import {
ActionConnector,
CaseFullExternalService,
@ -21,7 +21,7 @@ import {
CommentRequestUserType,
CommentRequestAlertType,
CommentRequestActionsType,
CaseUserActionResponse,
ActionTypes,
} from '../../../common/api';
import { ActionsClient } from '../../../../actions/server';
import { CasesClientGetAlertsResponse } from '../../client/alerts/types';
@ -57,18 +57,14 @@ export const getLatestPushInfo = (
userActions: CaseUserActionsResponse
): { index: number; pushedInfo: CaseFullExternalService } | null => {
for (const [index, action] of [...userActions].reverse().entries()) {
if (
isPush(action.action, action.action_field) &&
isValidNewValue(action) &&
connectorId === action.new_val_connector_id
) {
if (isPushedUserAction(action) && connectorId === action.payload.externalService.connector_id) {
try {
const pushedInfo = JSON.parse(action.new_value);
const pushedInfo = action.payload.externalService;
// We returned the index of the element in the userActions array.
// As we traverse the userActions in reverse we need to calculate the index of a normal traversal
return {
index: userActions.length - index - 1,
pushedInfo: { ...pushedInfo, connector_id: connectorId },
pushedInfo,
};
} catch (e) {
// ignore parse failures and check the next user action
@ -79,14 +75,6 @@ export const getLatestPushInfo = (
return null;
};
type NonNullNewValueAction = Omit<CaseUserActionResponse, 'new_value' | 'new_val_connector_id'> & {
new_value: string;
new_val_connector_id: string;
};
const isValidNewValue = (userAction: CaseUserActionResponse): userAction is NonNullNewValueAction =>
userAction.new_val_connector_id != null && userAction.new_value != null;
const getCommentContent = (comment: CommentResponse): string => {
if (comment.type === CommentType.user) {
return comment.comment;
@ -220,9 +208,7 @@ export const createIncident = async ({
const commentsIdsToBeUpdated = new Set(
userActions
.slice(latestPushInfo?.index ?? 0)
.filter(
(action) => Array.isArray(action.action_field) && action.action_field[0] === 'comment'
)
.filter((action) => action.type === ActionTypes.comment)
.map((action) => action.comment_id)
);

View file

@ -19,11 +19,10 @@ import {
SubCasesFindResponseRt,
SubCasesPatchRequest,
} from '../../../common/api';
import { CASE_SAVED_OBJECT, MAX_CONCURRENT_SEARCHES } from '../../../common/constants';
import { MAX_CONCURRENT_SEARCHES } from '../../../common/constants';
import { CasesClientArgs } from '..';
import { createCaseError } from '../../common/error';
import { countAlertsForID, flattenSubCaseSavedObject, transformSubCases } from '../../common/utils';
import { buildCaseUserActionItem } from '../../services/user_actions/helpers';
import { constructQueryOptions } from '../utils';
import { defaultPage, defaultPerPage } from '../../routes/api';
import { update } from './update';
@ -91,8 +90,7 @@ export function createSubCasesClient(clientArgs: CasesClientArgs): SubCasesClien
async function deleteSubCase(ids: string[], clientArgs: CasesClientArgs): Promise<void> {
try {
const { unsecuredSavedObjectsClient, user, userActionService, caseService, attachmentService } =
clientArgs;
const { unsecuredSavedObjectsClient, caseService, attachmentService } = clientArgs;
const [comments, subCases] = await Promise.all([
caseService.getAllSubCaseComments({ unsecuredSavedObjectsClient, id: ids }),
@ -109,12 +107,6 @@ async function deleteSubCase(ids: string[], clientArgs: CasesClientArgs): Promis
);
}
const subCaseIDToParentID = subCases.saved_objects.reduce((acc, subCase) => {
const parentID = subCase.references.find((ref) => ref.type === CASE_SAVED_OBJECT);
acc.set(subCase.id, parentID?.id);
return acc;
}, new Map<string, string | undefined>());
const deleteCommentMapper = async (comment: SavedObject<CommentAttributes>) =>
attachmentService.delete({ unsecuredSavedObjectsClient, attachmentId: comment.id });
@ -129,25 +121,6 @@ async function deleteSubCase(ids: string[], clientArgs: CasesClientArgs): Promis
await pMap(ids, deleteSubCasesMapper, {
concurrency: MAX_CONCURRENT_SEARCHES,
});
const deleteDate = new Date().toISOString();
await userActionService.bulkCreate({
unsecuredSavedObjectsClient,
actions: subCases.saved_objects.map((subCase) =>
buildCaseUserActionItem({
action: 'delete',
actionAt: deleteDate,
actionBy: user,
// if for some reason the sub case didn't have a reference to its parent, we'll still log a user action
// but we won't have the case ID
caseId: subCaseIDToParentID.get(subCase.id) ?? '',
subCaseId: subCase.id,
fields: ['sub_case', 'comment', 'status'],
owner: subCase.attributes.owner,
})
),
});
} catch (error) {
throw createCaseError({
message: `Failed to delete sub cases ids: ${JSON.stringify(ids)}: ${error}`,

View file

@ -36,7 +36,6 @@ import {
} from '../../../common/api';
import { CASE_COMMENT_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../../common/constants';
import { getCaseToUpdate } from '../utils';
import { buildSubCaseUserActions } from '../../services/user_actions/helpers';
import { createCaseError } from '../../common/error';
import {
createAlertUpdateRequest,
@ -381,14 +380,11 @@ export async function update({
[]
);
await userActionService.bulkCreate({
await userActionService.bulkCreateUpdateCase({
unsecuredSavedObjectsClient,
actions: buildSubCaseUserActions({
originalSubCases: bulkSubCases.saved_objects,
updatedSubCases: updatedCases.saved_objects,
actionDate: updatedAt,
actionBy: user,
}),
originalCases: bulkSubCases.saved_objects,
updatedCases: updatedCases.saved_objects,
user,
});
return SubCasesResponseRt.encode(returnUpdatedSubCases);

View file

@ -38,6 +38,15 @@ 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',
@ -46,12 +55,12 @@ describe('utils', () => {
};
it('transform correctly', () => {
const myCase = {
newCase: { ...newCase, type: CaseType.individual },
connector,
createdDate: '2020-04-09T09:43:51.778Z',
email: 'elastic@elastic.co',
full_name: 'Elastic',
username: 'elastic',
newCase: { ...newCase, type: CaseType.individual, connector },
user: {
email: 'elastic@elastic.co',
full_name: 'Elastic',
username: 'elastic',
},
};
const res = transformNewCase(myCase);
@ -94,104 +103,5 @@ describe('utils', () => {
}
`);
});
it('transform correctly without optional fields', () => {
const myCase = {
newCase: { ...newCase, type: CaseType.individual },
connector,
createdDate: '2020-04-09T09:43:51.778Z',
};
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": undefined,
"full_name": undefined,
"username": undefined,
},
"description": "A description",
"external_service": null,
"owner": "securitySolution",
"settings": Object {
"syncAlerts": true,
},
"status": "open",
"tags": Array [
"new",
"case",
],
"title": "My new case",
"type": "individual",
"updated_at": null,
"updated_by": null,
}
`);
});
it('transform correctly with optional fields as null', () => {
const myCase = {
newCase: { ...newCase, type: CaseType.individual },
connector,
createdDate: '2020-04-09T09:43:51.778Z',
email: null,
full_name: null,
username: null,
};
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": null,
"full_name": null,
"username": null,
},
"description": "A description",
"external_service": null,
"owner": "securitySolution",
"settings": Object {
"syncAlerts": true,
},
"status": "open",
"tags": Array [
"new",
"case",
],
"title": "My new case",
"type": "individual",
"updated_at": null,
"updated_by": null,
}
`);
});
});
});

View file

@ -22,18 +22,6 @@ export const CONNECTOR_ID_REFERENCE_NAME = 'connectorId';
*/
export const PUSH_CONNECTOR_ID_REFERENCE_NAME = 'pushConnectorId';
/**
* The name of the saved object reference indicating the action connector ID that was used for
* adding a connector, or updating the existing connector for a user action's old_value field.
*/
export const USER_ACTION_OLD_ID_REF_NAME = 'oldConnectorId';
/**
* The name of the saved object reference indicating the action connector ID that was used for pushing a case,
* for a user action's old_value field.
*/
export const USER_ACTION_OLD_PUSH_ID_REF_NAME = 'oldPushConnectorId';
/**
* The name of the saved object reference indicating the caseId reference
*/

View file

@ -19,7 +19,6 @@ import { LensServerPluginSetup } from '../../../lens/server';
import {
AssociationType,
CaseAttributes,
CaseConnector,
CaseResponse,
CasesClientPostRequest,
CasesFindResponse,
@ -55,27 +54,17 @@ export const defaultSortField = 'created_at';
export const nullUser: User = { username: null, full_name: null, email: null };
export const transformNewCase = ({
connector,
createdDate,
email,
// eslint-disable-next-line @typescript-eslint/naming-convention
full_name,
user,
newCase,
username,
}: {
connector: CaseConnector;
createdDate: string;
email?: string | null;
full_name?: string | null;
user: User;
newCase: CasesClientPostRequest;
username?: string | null;
}): CaseAttributes => ({
...newCase,
closed_at: null,
closed_by: null,
connector,
created_at: createdDate,
created_by: { email, full_name, username },
created_at: new Date().toISOString(),
created_by: user,
external_service: null,
status: CaseStatuses.open,
updated_at: null,

View file

@ -75,7 +75,7 @@ async function getAttachmentsAndUserActionsForCases(
getAssociatedObjects<CaseUserActionAttributes>({
savedObjectsClient,
caseIds,
sortField: 'action_at',
sortField: defaultSortField,
type: CASE_USER_ACTION_SAVED_OBJECT,
}),
]);

View file

@ -10,7 +10,7 @@ import {
CaseAttributes,
CaseFullExternalService,
ConnectorTypes,
noneConnectorId,
NONE_CONNECTOR_ID,
} from '../../../common/api';
import { CASE_SAVED_OBJECT } from '../../../common/constants';
import { getNoneCaseConnector } from '../../common/utils';
@ -116,7 +116,7 @@ describe('case migrations', () => {
it('does not create a reference when the external_service.connector_id is none', () => {
const caseSavedObject = create_7_14_0_case({
externalService: createExternalService({ connector_id: noneConnectorId }),
externalService: createExternalService({ connector_id: NONE_CONNECTOR_ID }),
});
const migratedConnector = caseConnectorIdMigration(
@ -247,7 +247,7 @@ describe('case migrations', () => {
it('does not create a reference and preserves the existing external_service fields when connector_id is null', () => {
const caseSavedObject = create_7_14_0_case({
externalService: {
connector_id: null,
connector_id: 'none',
connector_name: '.jira',
external_id: '100',
external_title: 'awesome',

View file

@ -14,14 +14,14 @@ import {
} from '../../../../../../src/core/server';
import { ESConnectorFields } from '../../services';
import { ConnectorTypes, CaseType } from '../../../common/api';
import {
transformConnectorIdToReference,
transformPushConnectorIdToReference,
} from '../../services/user_actions/transform';
import {
CONNECTOR_ID_REFERENCE_NAME,
PUSH_CONNECTOR_ID_REFERENCE_NAME,
} from '../../common/constants';
import {
transformConnectorIdToReference,
transformPushConnectorIdToReference,
} from './user_actions/connector_id';
interface UnsanitizedCaseConnector {
connector_id: string;

View file

@ -13,8 +13,8 @@ import {
} from '../../../../../../src/core/server';
import { ConnectorTypes } from '../../../common/api';
import { addOwnerToSO, SanitizedCaseOwner } from '.';
import { transformConnectorIdToReference } from '../../services/user_actions/transform';
import { CONNECTOR_ID_REFERENCE_NAME } from '../../common/constants';
import { transformConnectorIdToReference } from './user_actions/connector_id';
interface UnsanitizedConfigureConnector {
connector_id: string;

View file

@ -1,149 +0,0 @@
/*
* 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.
*/
/* eslint-disable @typescript-eslint/naming-convention */
import { addOwnerToSO, SanitizedCaseOwner } from '.';
import {
SavedObjectUnsanitizedDoc,
SavedObjectSanitizedDoc,
SavedObjectMigrationContext,
} from '../../../../../../src/core/server';
import { isPush, isUpdateConnector, isCreateConnector } from '../../../common/utils/user_actions';
import { ConnectorTypes } from '../../../common/api';
import { extractConnectorIdFromJson } from '../../services/user_actions/transform';
import { UserActionFieldType } from '../../services/user_actions/types';
import { logError } from './utils';
interface UserActions {
action_field: string[];
new_value: string;
old_value: string;
}
interface UserActionUnmigratedConnectorDocument {
action?: string;
action_field?: string[];
new_value?: string | null;
old_value?: string | null;
}
export function userActionsConnectorIdMigration(
doc: SavedObjectUnsanitizedDoc<UserActionUnmigratedConnectorDocument>,
context: SavedObjectMigrationContext
): SavedObjectSanitizedDoc<unknown> {
const originalDocWithReferences = { ...doc, references: doc.references ?? [] };
if (!isConnectorUserAction(doc.attributes.action, doc.attributes.action_field)) {
return originalDocWithReferences;
}
try {
return formatDocumentWithConnectorReferences(doc);
} catch (error) {
logError({
id: doc.id,
context,
error,
docType: 'user action connector',
docKey: 'userAction',
});
return originalDocWithReferences;
}
}
function isConnectorUserAction(action?: string, actionFields?: string[]): boolean {
return (
isCreateConnector(action, actionFields) ||
isUpdateConnector(action, actionFields) ||
isPush(action, actionFields)
);
}
function formatDocumentWithConnectorReferences(
doc: SavedObjectUnsanitizedDoc<UserActionUnmigratedConnectorDocument>
): SavedObjectSanitizedDoc<unknown> {
const { new_value, old_value, action, action_field, ...restAttributes } = doc.attributes;
const { references = [] } = doc;
const { transformedActionDetails: transformedNewValue, references: newValueConnectorRefs } =
extractConnectorIdFromJson({
action,
actionFields: action_field,
actionDetails: new_value,
fieldType: UserActionFieldType.New,
});
const { transformedActionDetails: transformedOldValue, references: oldValueConnectorRefs } =
extractConnectorIdFromJson({
action,
actionFields: action_field,
actionDetails: old_value,
fieldType: UserActionFieldType.Old,
});
return {
...doc,
attributes: {
...restAttributes,
action,
action_field,
new_value: transformedNewValue,
old_value: transformedOldValue,
},
references: [...references, ...newValueConnectorRefs, ...oldValueConnectorRefs],
};
}
export const userActionsMigrations = {
'7.10.0': (doc: SavedObjectUnsanitizedDoc<UserActions>): SavedObjectSanitizedDoc<UserActions> => {
const { action_field, new_value, old_value, ...restAttributes } = doc.attributes;
if (
action_field == null ||
!Array.isArray(action_field) ||
action_field[0] !== 'connector_id'
) {
return { ...doc, references: doc.references || [] };
}
return {
...doc,
attributes: {
...restAttributes,
action_field: ['connector'],
new_value:
new_value != null
? JSON.stringify({
id: new_value,
name: 'none',
type: ConnectorTypes.none,
fields: null,
})
: new_value,
old_value:
old_value != null
? JSON.stringify({
id: old_value,
name: 'none',
type: ConnectorTypes.none,
fields: null,
})
: old_value,
},
references: doc.references || [],
};
},
'7.14.0': (
doc: SavedObjectUnsanitizedDoc<Record<string, unknown>>
): SavedObjectSanitizedDoc<SanitizedCaseOwner> => {
return addOwnerToSO(doc);
},
'7.16.0': userActionsConnectorIdMigration,
};

View file

@ -11,25 +11,32 @@ import {
SavedObjectMigrationContext,
SavedObjectSanitizedDoc,
SavedObjectsMigrationLogger,
SavedObjectUnsanitizedDoc,
} from 'kibana/server';
import { migrationMocks } from 'src/core/server/mocks';
import { CaseUserActionAttributes } from '../../../common/api';
import { CASE_USER_ACTION_SAVED_OBJECT } from '../../../common/constants';
import {
CASE_USER_ACTION_SAVED_OBJECT,
SECURITY_SOLUTION_OWNER,
} from '../../../../common/constants';
import {
createConnectorObject,
createExternalService,
createJiraConnector,
} from '../../services/test_utils';
import { userActionsConnectorIdMigration } from './user_actions';
} from '../../../services/test_utils';
import { userActionsConnectorIdMigration } from './connector_id';
import { UserActions } from './types';
const create_7_14_0_userAction = (
params: {
action?: string;
action_field?: string[];
new_value?: string | null | object;
old_value?: string | null | object;
} = {}
) => {
interface Pre810UserActionAttributes {
new_value?: string;
old_value?: string;
}
const create_7_14_0_userAction = (params: {
action: string;
action_field: string[];
new_value: string | null | object;
old_value: string | null | object;
}): SavedObjectUnsanitizedDoc<UserActions> => {
const { new_value, old_value, ...restParams } = params;
return {
@ -37,8 +44,17 @@ const create_7_14_0_userAction = (
id: '1',
attributes: {
...restParams,
new_value: new_value && typeof new_value === 'object' ? JSON.stringify(new_value) : new_value,
old_value: old_value && typeof old_value === 'object' ? JSON.stringify(old_value) : old_value,
action_at: '2022-01-09T22:00:00.000Z',
action_by: {
email: 'elastic@elastic.co',
full_name: 'Elastic User',
username: 'elastic',
},
new_value:
new_value && typeof new_value === 'object' ? JSON.stringify(new_value) : new_value ?? null,
old_value:
old_value && typeof old_value === 'object' ? JSON.stringify(old_value) : old_value ?? null,
owner: SECURITY_SOLUTION_OWNER,
},
};
};
@ -64,7 +80,7 @@ describe('user action migrations', () => {
const migratedUserAction = userActionsConnectorIdMigration(
userAction,
context
) as SavedObjectSanitizedDoc<CaseUserActionAttributes>;
) as SavedObjectSanitizedDoc<Pre810UserActionAttributes>;
const parsedExternalService = JSON.parse(migratedUserAction.attributes.new_value!);
expect(parsedExternalService).not.toHaveProperty('connector_id');
@ -107,7 +123,7 @@ describe('user action migrations', () => {
const migratedUserAction = userActionsConnectorIdMigration(
userAction,
context
) as SavedObjectSanitizedDoc<CaseUserActionAttributes>;
) as SavedObjectSanitizedDoc<Pre810UserActionAttributes>;
const parsedNewExternalService = JSON.parse(migratedUserAction.attributes.new_value!);
const parsedOldExternalService = JSON.parse(migratedUserAction.attributes.old_value!);
@ -134,7 +150,7 @@ describe('user action migrations', () => {
const migratedUserAction = userActionsConnectorIdMigration(
userAction,
context
) as SavedObjectSanitizedDoc<CaseUserActionAttributes>;
) as SavedObjectSanitizedDoc<Pre810UserActionAttributes>;
const parsedNewExternalService = JSON.parse(migratedUserAction.attributes.new_value!);
const parsedOldExternalService = JSON.parse(migratedUserAction.attributes.old_value!);
@ -159,18 +175,25 @@ describe('user action migrations', () => {
const migratedUserAction = userActionsConnectorIdMigration(
userAction,
context
) as SavedObjectSanitizedDoc<CaseUserActionAttributes>;
) as SavedObjectSanitizedDoc<Pre810UserActionAttributes>;
expect(migratedUserAction.attributes.old_value).toBeNull();
expect(migratedUserAction).toMatchInlineSnapshot(`
Object {
"attributes": Object {
"action": "push-to-service",
"action_at": "2022-01-09T22:00:00.000Z",
"action_by": Object {
"email": "elastic@elastic.co",
"full_name": "Elastic User",
"username": "elastic",
},
"action_field": Array [
"invalid field",
],
"new_value": "hello",
"old_value": null,
"owner": "securitySolution",
},
"id": "1",
"references": Array [],
@ -190,7 +213,7 @@ describe('user action migrations', () => {
const migratedUserAction = userActionsConnectorIdMigration(
userAction,
context
) as SavedObjectSanitizedDoc<CaseUserActionAttributes>;
) as SavedObjectSanitizedDoc<Pre810UserActionAttributes>;
expect(migratedUserAction.attributes.old_value).toBeNull();
expect(migratedUserAction.attributes.new_value).toEqual('{a');
@ -198,11 +221,18 @@ describe('user action migrations', () => {
Object {
"attributes": Object {
"action": "push-to-service",
"action_at": "2022-01-09T22:00:00.000Z",
"action_by": Object {
"email": "elastic@elastic.co",
"full_name": "Elastic User",
"username": "elastic",
},
"action_field": Array [
"pushed",
],
"new_value": "{a",
"old_value": null,
"owner": "securitySolution",
},
"id": "1",
"references": Array [],
@ -249,7 +279,7 @@ describe('user action migrations', () => {
const migratedUserAction = userActionsConnectorIdMigration(
userAction,
context
) as SavedObjectSanitizedDoc<CaseUserActionAttributes>;
) as SavedObjectSanitizedDoc<Pre810UserActionAttributes>;
const parsedConnector = JSON.parse(migratedUserAction.attributes.new_value!);
expect(parsedConnector).not.toHaveProperty('id');
@ -289,7 +319,7 @@ describe('user action migrations', () => {
const migratedUserAction = userActionsConnectorIdMigration(
userAction,
context
) as SavedObjectSanitizedDoc<CaseUserActionAttributes>;
) as SavedObjectSanitizedDoc<Pre810UserActionAttributes>;
const parsedNewConnector = JSON.parse(migratedUserAction.attributes.new_value!);
const parsedOldConnector = JSON.parse(migratedUserAction.attributes.new_value!);
@ -317,7 +347,7 @@ describe('user action migrations', () => {
const migratedUserAction = userActionsConnectorIdMigration(
userAction,
context
) as SavedObjectSanitizedDoc<CaseUserActionAttributes>;
) as SavedObjectSanitizedDoc<Pre810UserActionAttributes>;
const parsedNewConnectorId = JSON.parse(migratedUserAction.attributes.new_value!);
const parsedOldConnectorId = JSON.parse(migratedUserAction.attributes.old_value!);
@ -342,17 +372,24 @@ describe('user action migrations', () => {
const migratedUserAction = userActionsConnectorIdMigration(
userAction,
context
) as SavedObjectSanitizedDoc<CaseUserActionAttributes>;
) as SavedObjectSanitizedDoc<Pre810UserActionAttributes>;
expect(migratedUserAction).toMatchInlineSnapshot(`
Object {
"attributes": Object {
"action": "update",
"action_at": "2022-01-09T22:00:00.000Z",
"action_by": Object {
"email": "elastic@elastic.co",
"full_name": "Elastic User",
"username": "elastic",
},
"action_field": Array [
"invalid action",
],
"new_value": "new json value",
"old_value": "old value",
"owner": "securitySolution",
},
"id": "1",
"references": Array [],
@ -372,17 +409,24 @@ describe('user action migrations', () => {
const migratedUserAction = userActionsConnectorIdMigration(
userAction,
context
) as SavedObjectSanitizedDoc<CaseUserActionAttributes>;
) as SavedObjectSanitizedDoc<Pre810UserActionAttributes>;
expect(migratedUserAction).toMatchInlineSnapshot(`
Object {
"attributes": Object {
"action": "update",
"action_at": "2022-01-09T22:00:00.000Z",
"action_by": Object {
"email": "elastic@elastic.co",
"full_name": "Elastic User",
"username": "elastic",
},
"action_field": Array [
"connector",
],
"new_value": "{}",
"old_value": "{b",
"owner": "securitySolution",
},
"id": "1",
"references": Array [],
@ -429,7 +473,7 @@ describe('user action migrations', () => {
const migratedUserAction = userActionsConnectorIdMigration(
userAction,
context
) as SavedObjectSanitizedDoc<CaseUserActionAttributes>;
) as SavedObjectSanitizedDoc<Pre810UserActionAttributes>;
const parsedConnector = JSON.parse(migratedUserAction.attributes.new_value!);
expect(parsedConnector.connector).not.toHaveProperty('id');
@ -471,7 +515,7 @@ describe('user action migrations', () => {
const migratedUserAction = userActionsConnectorIdMigration(
userAction,
context
) as SavedObjectSanitizedDoc<CaseUserActionAttributes>;
) as SavedObjectSanitizedDoc<Pre810UserActionAttributes>;
const parsedNewConnector = JSON.parse(migratedUserAction.attributes.new_value!);
const parsedOldConnector = JSON.parse(migratedUserAction.attributes.new_value!);
@ -499,7 +543,7 @@ describe('user action migrations', () => {
const migratedUserAction = userActionsConnectorIdMigration(
userAction,
context
) as SavedObjectSanitizedDoc<CaseUserActionAttributes>;
) as SavedObjectSanitizedDoc<Pre810UserActionAttributes>;
const parsedNewConnectorId = JSON.parse(migratedUserAction.attributes.new_value!);
const parsedOldConnectorId = JSON.parse(migratedUserAction.attributes.old_value!);
@ -524,17 +568,24 @@ describe('user action migrations', () => {
const migratedUserAction = userActionsConnectorIdMigration(
userAction,
context
) as SavedObjectSanitizedDoc<CaseUserActionAttributes>;
) as SavedObjectSanitizedDoc<Pre810UserActionAttributes>;
expect(migratedUserAction).toMatchInlineSnapshot(`
Object {
"attributes": Object {
"action": "create",
"action_at": "2022-01-09T22:00:00.000Z",
"action_by": Object {
"email": "elastic@elastic.co",
"full_name": "Elastic User",
"username": "elastic",
},
"action_field": Array [
"invalid action",
],
"new_value": "new json value",
"old_value": "old value",
"owner": "securitySolution",
},
"id": "1",
"references": Array [],
@ -554,17 +605,24 @@ describe('user action migrations', () => {
const migratedUserAction = userActionsConnectorIdMigration(
userAction,
context
) as SavedObjectSanitizedDoc<CaseUserActionAttributes>;
) as SavedObjectSanitizedDoc<Pre810UserActionAttributes>;
expect(migratedUserAction).toMatchInlineSnapshot(`
Object {
"attributes": Object {
"action": "create",
"action_at": "2022-01-09T22:00:00.000Z",
"action_by": Object {
"email": "elastic@elastic.co",
"full_name": "Elastic User",
"username": "elastic",
},
"action_field": Array [
"connector",
],
"new_value": "new json value",
"old_value": "old value",
"owner": "securitySolution",
},
"id": "1",
"references": Array [],

View file

@ -8,26 +8,49 @@
/* eslint-disable @typescript-eslint/naming-convention */
import * as rt from 'io-ts';
import { isString } from 'lodash';
import { SavedObjectReference } from '../../../../../../src/core/server';
import { isCreateConnector, isPush, isUpdateConnector } from '../../../common/utils/user_actions';
import {
SavedObjectMigrationContext,
SavedObjectReference,
SavedObjectSanitizedDoc,
SavedObjectUnsanitizedDoc,
} from '../../../../../../../src/core/server';
import {
CaseAttributes,
CaseConnector,
CaseConnectorRt,
CaseExternalServiceBasicRt,
noneConnectorId,
} from '../../../common/api';
NONE_CONNECTOR_ID,
} from '../../../../common/api';
import {
CONNECTOR_ID_REFERENCE_NAME,
PUSH_CONNECTOR_ID_REFERENCE_NAME,
USER_ACTION_OLD_ID_REF_NAME,
USER_ACTION_OLD_PUSH_ID_REF_NAME,
} from '../../common/constants';
import { getNoneCaseConnector } from '../../common/utils';
import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server';
import { UserActionFieldType } from './types';
} from '../../../common/constants';
import { getNoneCaseConnector } from '../../../common/utils';
import { ACTION_SAVED_OBJECT_TYPE } from '../../../../../actions/server';
import { UserActionUnmigratedConnectorDocument } from './types';
import { logError } from '../utils';
import { USER_ACTION_OLD_ID_REF_NAME, USER_ACTION_OLD_PUSH_ID_REF_NAME } from './constants';
export function isCreateConnector(action?: string, actionFields?: string[]): boolean {
return action === 'create' && actionFields != null && actionFields.includes('connector');
}
export function isUpdateConnector(action?: string, actionFields?: string[]): boolean {
return action === 'update' && actionFields != null && actionFields.includes('connector');
}
export function isPush(action?: string, actionFields?: string[]): boolean {
return action === 'push-to-service' && actionFields != null && actionFields.includes('pushed');
}
/**
* Indicates whether which user action field is being parsed, the new_value or the old_value.
*/
export enum UserActionFieldType {
New = 'New',
Old = 'Old',
}
/**
* Extracts the connector id from a json encoded string and formats it as a saved object reference. This will remove
@ -58,49 +81,6 @@ export function extractConnectorIdFromJson({
});
}
/**
* Extracts the connector id from an unencoded object and formats it as a saved object reference.
* This will remove the field it extracted the connector id from.
*/
export function extractConnectorId({
action,
actionFields,
actionDetails,
fieldType,
}: {
action: string;
actionFields: string[];
actionDetails?: Record<string, unknown> | string | null;
fieldType: UserActionFieldType;
}): {
transformedActionDetails?: string | null;
references: SavedObjectReference[];
} {
if (!actionDetails || isString(actionDetails)) {
// the action was null, undefined, or a regular string so just return it unmodified and not encoded
return { transformedActionDetails: actionDetails, references: [] };
}
try {
return extractConnectorIdHelper({
action,
actionFields,
actionDetails,
fieldType,
});
} catch (error) {
return { transformedActionDetails: encodeActionDetails(actionDetails), references: [] };
}
}
function encodeActionDetails(actionDetails: Record<string, unknown>): string | null {
try {
return JSON.stringify(actionDetails);
} catch (error) {
return null;
}
}
/**
* Internal helper function for extracting the connector id. This is only exported for usage in unit tests.
* This function handles encoding the transformed fields as a json string
@ -153,7 +133,7 @@ export function extractConnectorIdHelper({
};
}
function isCreateCaseConnector(
export function isCreateCaseConnector(
action: string,
actionFields: string[],
actionDetails: unknown
@ -176,7 +156,7 @@ export const ConnectorIdReferenceName: Record<UserActionFieldType, ConnectorIdRe
[UserActionFieldType.Old]: USER_ACTION_OLD_ID_REF_NAME,
};
function transformConnectorFromCreateAndUpdateAction(
export function transformConnectorFromCreateAndUpdateAction(
connector: CaseConnector,
fieldType: UserActionFieldType
): {
@ -240,7 +220,7 @@ const createConnectorReference = (
};
const isConnectorIdValid = (id: string | null | undefined): id is string =>
id != null && id !== noneConnectorId;
id != null && id !== NONE_CONNECTOR_ID;
function isUpdateCaseConnector(
action: string,
@ -316,3 +296,72 @@ export const transformPushConnectorIdToReference = (
references,
};
};
export function isConnectorUserAction(action?: string, actionFields?: string[]): boolean {
return (
isCreateConnector(action, actionFields) ||
isUpdateConnector(action, actionFields) ||
isPush(action, actionFields)
);
}
export function formatDocumentWithConnectorReferences(
doc: SavedObjectUnsanitizedDoc<UserActionUnmigratedConnectorDocument>
): SavedObjectSanitizedDoc<unknown> {
const { new_value, old_value, action, action_field, ...restAttributes } = doc.attributes;
const { references = [] } = doc;
const { transformedActionDetails: transformedNewValue, references: newValueConnectorRefs } =
extractConnectorIdFromJson({
action,
actionFields: action_field,
actionDetails: new_value,
fieldType: UserActionFieldType.New,
});
const { transformedActionDetails: transformedOldValue, references: oldValueConnectorRefs } =
extractConnectorIdFromJson({
action,
actionFields: action_field,
actionDetails: old_value,
fieldType: UserActionFieldType.Old,
});
return {
...doc,
attributes: {
...restAttributes,
action,
action_field,
new_value: transformedNewValue,
old_value: transformedOldValue,
},
references: [...references, ...newValueConnectorRefs, ...oldValueConnectorRefs],
};
}
// 8.1.0 migration util functions
export function userActionsConnectorIdMigration(
doc: SavedObjectUnsanitizedDoc<UserActionUnmigratedConnectorDocument>,
context: SavedObjectMigrationContext
): SavedObjectSanitizedDoc<unknown> {
const originalDocWithReferences = { ...doc, references: doc.references ?? [] };
if (!isConnectorUserAction(doc.attributes.action, doc.attributes.action_field)) {
return originalDocWithReferences;
}
try {
return formatDocumentWithConnectorReferences(doc);
} catch (error) {
logError({
id: doc.id,
context,
error,
docType: 'user action connector',
docKey: 'userAction',
});
return originalDocWithReferences;
}
}

View file

@ -0,0 +1,18 @@
/*
* 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.
*/
/**
* The name of the saved object reference indicating the action connector ID that was used for
* adding a connector, or updating the existing connector for a user action's old_value field.
*/
export const USER_ACTION_OLD_ID_REF_NAME = 'oldConnectorId';
/**
* The name of the saved object reference indicating the action connector ID that was used for pushing a case,
* for a user action's old_value field.
*/
export const USER_ACTION_OLD_PUSH_ID_REF_NAME = 'oldPushConnectorId';

View file

@ -0,0 +1,67 @@
/*
* 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.
*/
/* eslint-disable @typescript-eslint/naming-convention */
import { addOwnerToSO, SanitizedCaseOwner } from '..';
import {
SavedObjectUnsanitizedDoc,
SavedObjectSanitizedDoc,
} from '../../../../../../../src/core/server';
import { ConnectorTypes } from '../../../../common/api';
import { userActionsConnectorIdMigration } from './connector_id';
import { payloadMigration } from './payload';
import { UserActions } from './types';
export const userActionsMigrations = {
'7.10.0': (doc: SavedObjectUnsanitizedDoc<UserActions>): SavedObjectSanitizedDoc<UserActions> => {
const { action_field, new_value, old_value, ...restAttributes } = doc.attributes;
if (
action_field == null ||
!Array.isArray(action_field) ||
action_field[0] !== 'connector_id'
) {
return { ...doc, references: doc.references || [] };
}
return {
...doc,
attributes: {
...restAttributes,
action_field: ['connector'],
new_value:
new_value != null
? JSON.stringify({
id: new_value,
name: 'none',
type: ConnectorTypes.none,
fields: null,
})
: new_value,
old_value:
old_value != null
? JSON.stringify({
id: old_value,
name: 'none',
type: ConnectorTypes.none,
fields: null,
})
: old_value,
},
references: doc.references || [],
};
},
'7.14.0': (
doc: SavedObjectUnsanitizedDoc<Record<string, unknown>>
): SavedObjectSanitizedDoc<SanitizedCaseOwner> => {
return addOwnerToSO(doc);
},
'7.16.0': userActionsConnectorIdMigration,
'8.1.0': payloadMigration,
};

View file

@ -0,0 +1,405 @@
/*
* 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.
*/
/* eslint-disable @typescript-eslint/naming-convention */
import { SavedObjectMigrationContext, SavedObjectUnsanitizedDoc } from 'kibana/server';
import { migrationMocks } from 'src/core/server/mocks';
import { CommentType } from '../../../../common/api';
import {
CASE_USER_ACTION_SAVED_OBJECT,
SECURITY_SOLUTION_OWNER,
} from '../../../../common/constants';
import { createJiraConnector } from '../../../services/test_utils';
import { payloadMigration } from './payload';
import { UserActions } from './types';
const create_7_14_0_userAction = (params: {
action: string;
action_field: string[];
new_value: string | null | object;
old_value: string | null | object;
}): SavedObjectUnsanitizedDoc<UserActions> => {
const { new_value, old_value, ...restParams } = params;
return {
type: CASE_USER_ACTION_SAVED_OBJECT,
id: '1',
attributes: {
...restParams,
action_at: '2022-01-09T22:00:00.000Z',
action_by: {
email: 'elastic@elastic.co',
full_name: 'Elastic User',
username: 'elastic',
},
new_value:
new_value && typeof new_value === 'object' ? JSON.stringify(new_value) : new_value ?? null,
old_value:
old_value && typeof old_value === 'object' ? JSON.stringify(old_value) : old_value ?? null,
owner: SECURITY_SOLUTION_OWNER,
},
};
};
describe('user action migrations', () => {
describe('8.1.0', () => {
let context: jest.Mocked<SavedObjectMigrationContext>;
beforeEach(() => {
context = migrationMocks.createContext();
});
describe('references', () => {
it('removes the old references', () => {
const userAction = create_7_14_0_userAction({
action: 'update',
action_field: ['connector'],
new_value: createJiraConnector(),
old_value: { ...createJiraConnector(), id: '5' },
});
const migratedUserAction = payloadMigration(
{
...userAction,
references: [
{ id: '1', name: 'connectorId', type: 'action' },
{ id: '5', name: 'oldConnectorId', type: 'action' },
{ id: '100', name: 'pushConnectorId', type: 'action' },
{ id: '5', name: 'oldPushConnectorId', type: 'action' },
],
},
context
);
expect(migratedUserAction.references).toEqual([
{ id: '1', name: 'connectorId', type: 'action' },
{ id: '100', name: 'pushConnectorId', type: 'action' },
]);
});
});
describe('payloadMigration', () => {
const expectedCreateCaseUserAction = {
action: 'create',
created_at: '2022-01-09T22:00:00.000Z',
created_by: {
email: 'elastic@elastic.co',
full_name: 'Elastic User',
username: 'elastic',
},
owner: 'securitySolution',
payload: {
connector: {
fields: null,
name: 'none',
type: '.none',
},
description: 'a desc',
tags: ['some tags'],
title: 'old case',
status: 'open',
owner: 'securitySolution',
settings: { syncAlerts: true },
},
type: 'create_case',
};
it('it transforms a comment user action where the new_value is a string', () => {
const userAction = create_7_14_0_userAction({
action: 'create',
action_field: ['comment'],
new_value: 'A comment',
old_value: null,
});
const migratedUserAction = payloadMigration(userAction, context);
expect(migratedUserAction.attributes).toEqual({
action: 'create',
created_at: '2022-01-09T22:00:00.000Z',
created_by: {
email: 'elastic@elastic.co',
full_name: 'Elastic User',
username: 'elastic',
},
owner: 'securitySolution',
payload: {
comment: {
comment: 'A comment',
owner: 'securitySolution',
type: 'user',
},
},
type: 'comment',
});
});
it('adds the owner to the comment if it is missing', () => {
const userAction = create_7_14_0_userAction({
action: 'create',
action_field: ['comment'],
new_value: { comment: 'A comment', type: CommentType.user },
old_value: null,
});
const migratedUserAction = payloadMigration(userAction, context);
expect(migratedUserAction.attributes).toEqual({
action: 'create',
created_at: '2022-01-09T22:00:00.000Z',
created_by: {
email: 'elastic@elastic.co',
full_name: 'Elastic User',
username: 'elastic',
},
owner: 'securitySolution',
payload: {
comment: {
comment: 'A comment',
owner: 'securitySolution',
type: 'user',
},
},
type: 'comment',
});
});
it('transforms a create case user action without a connector or status', () => {
const userAction = create_7_14_0_userAction({
action: 'create',
action_field: ['description', 'title', 'tags', 'owner', 'settings'],
new_value: {
title: 'old case',
description: 'a desc',
tags: ['some tags'],
owner: SECURITY_SOLUTION_OWNER,
settings: { syncAlerts: true },
},
old_value: null,
});
const migratedUserAction = payloadMigration(userAction, context);
expect(migratedUserAction.attributes).toEqual(expectedCreateCaseUserAction);
});
it('adds the owner in the payload on a create case user action if it is missing', () => {
const userAction = create_7_14_0_userAction({
action: 'create',
action_field: ['description', 'title', 'tags', 'settings'],
new_value: {
title: 'old case',
description: 'a desc',
tags: ['some tags'],
settings: { syncAlerts: true },
},
old_value: null,
});
const migratedUserAction = payloadMigration(userAction, context);
expect(migratedUserAction.attributes).toEqual(expectedCreateCaseUserAction);
});
it('adds the settings in the payload on a create case user action if it is missing', () => {
const userAction = create_7_14_0_userAction({
action: 'create',
action_field: ['description', 'title', 'tags'],
new_value: {
title: 'old case',
description: 'a desc',
tags: ['some tags'],
},
old_value: null,
});
const migratedUserAction = payloadMigration(userAction, context);
expect(migratedUserAction.attributes).toEqual(expectedCreateCaseUserAction);
});
describe('user actions', () => {
const fieldsTests: Array<[string, string | object]> = [
['description', 'a desc'],
['title', 'a title'],
['status', 'open'],
['comment', { comment: 'a comment', type: 'user', owner: 'securitySolution' }],
[
'connector',
{
fields: {
issueType: 'bug',
parent: '2',
priority: 'high',
},
name: '.jira',
type: '.jira',
},
],
['settings', { syncAlerts: false }],
];
it('migrates a create case user action correctly', () => {
const userAction = create_7_14_0_userAction({
action: 'create',
action_field: [
'description',
'title',
'tags',
'status',
'settings',
'owner',
'connector',
],
new_value: {
title: 'old case',
description: 'a desc',
tags: ['some tags'],
status: 'in-progress',
settings: { syncAlerts: false },
connector: {
fields: {
issueType: 'bug',
parent: '2',
priority: 'high',
},
name: '.jira',
type: '.jira',
},
owner: 'testOwner',
},
old_value: null,
});
const migratedUserAction = payloadMigration(userAction, context);
expect(migratedUserAction.attributes).toEqual({
action: 'create',
created_at: '2022-01-09T22:00:00.000Z',
created_by: {
email: 'elastic@elastic.co',
full_name: 'Elastic User',
username: 'elastic',
},
owner: 'securitySolution',
payload: {
connector: {
fields: {
issueType: 'bug',
parent: '2',
priority: 'high',
},
name: '.jira',
type: '.jira',
},
description: 'a desc',
tags: ['some tags'],
title: 'old case',
settings: {
syncAlerts: false,
},
status: 'in-progress',
owner: 'testOwner',
},
type: 'create_case',
});
});
it.each(fieldsTests)('migrates a user action for %s correctly', (field, value) => {
const userAction = create_7_14_0_userAction({
action: 'update',
action_field: [field],
new_value: value,
old_value: null,
});
const migratedUserAction = payloadMigration(userAction, context);
expect(migratedUserAction.attributes).toEqual({
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: {
[field]: value,
},
type: field,
});
});
it('migrates a user action for tags correctly', () => {
const userAction = create_7_14_0_userAction({
action: 'update',
action_field: ['tags'],
new_value: 'one, two',
old_value: null,
});
const migratedUserAction = payloadMigration(userAction, context);
expect(migratedUserAction.attributes).toEqual({
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: {
tags: ['one', 'two'],
},
type: 'tags',
});
});
it('migrates a user action for external services correctly', () => {
const userAction = create_7_14_0_userAction({
action: 'update',
action_field: ['pushed'],
new_value: {
connector_name: 'jira',
external_title: 'awesome',
external_url: 'http://www.google.com',
pushed_at: '2019-11-25T21:54:48.952Z',
pushed_by: {
full_name: 'elastic',
email: 'testemail@elastic.co',
username: 'elastic',
},
},
old_value: null,
});
const migratedUserAction = payloadMigration(userAction, context);
expect(migratedUserAction.attributes).toEqual({
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: {
externalService: {
connector_name: 'jira',
external_title: 'awesome',
external_url: 'http://www.google.com',
pushed_at: '2019-11-25T21:54:48.952Z',
pushed_by: {
full_name: 'elastic',
email: 'testemail@elastic.co',
username: 'elastic',
},
},
},
type: 'pushed',
});
});
});
});
describe('failures', () => {});
});
});

View file

@ -0,0 +1,226 @@
/*
* 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.
*/
/* eslint-disable @typescript-eslint/naming-convention */
import { isEmpty, isPlainObject, isString } from 'lodash';
import {
SavedObjectMigrationContext,
SavedObjectReference,
SavedObjectSanitizedDoc,
SavedObjectUnsanitizedDoc,
} from '../../../../../../../src/core/server';
import {
Actions,
ActionTypes,
CaseStatuses,
CommentType,
UserActionTypes,
} from '../../../../common/api';
import { USER_ACTION_OLD_ID_REF_NAME, USER_ACTION_OLD_PUSH_ID_REF_NAME } from './constants';
import { getNoneCaseConnector } from '../../../common/utils';
import { logError } from '../utils';
import { UserActions } from './types';
export function payloadMigration(
doc: SavedObjectUnsanitizedDoc<UserActions>,
context: SavedObjectMigrationContext
): SavedObjectSanitizedDoc<unknown> {
const originalDocWithReferences = { ...doc, references: doc.references ?? [] };
const owner = originalDocWithReferences.attributes.owner;
const { new_value, old_value, action_field, action_at, action_by, action, ...restAttributes } =
originalDocWithReferences.attributes;
const newAction = action === 'push-to-service' ? Actions.push_to_service : action;
const type = getUserActionType(action_field, action);
try {
const payload = getPayload(type, action_field, new_value, old_value, owner);
const references = removeOldReferences(doc.references);
return {
...originalDocWithReferences,
attributes: {
...restAttributes,
action: newAction,
created_at: action_at,
created_by: action_by,
payload,
type,
},
references,
};
} catch (error) {
logError({
id: doc.id,
context,
error,
docType: 'user action',
docKey: 'userAction',
});
return {
...originalDocWithReferences,
attributes: {
...restAttributes,
action: newAction,
created_at: action_at,
created_by: action_by,
payload: {},
type,
},
references: doc.references ?? [],
};
}
}
export const getUserActionType = (fields: string[], action: string): string => {
if (fields.length > 1 && action === Actions.create) {
return ActionTypes.create_case;
}
if (fields.length > 1 && action === Actions.delete) {
return ActionTypes.delete_case;
}
const field = fields[0] as UserActionTypes;
return ActionTypes[field] ?? '';
};
export const getPayload = (
type: string,
action_field: string[],
new_value: string | null,
old_value: string | null,
owner: string
): Record<string, unknown> => {
const payload = convertPayload(action_field, new_value ?? old_value ?? null, owner);
/**
* From 7.10+ the cases saved object has the connector attribute
* Create case user actions did not get migrated to have the
* connector attribute included.
*
* We are taking care of it in this migration by adding the none
* connector as a default. The same applies to the status field.
*
* If a create_case user action does not have the
* owner field we default to the owner of the of the
* user action. It is impossible to create a user action
* with different owner from the original case.
*/
const { id, ...noneConnector } = getNoneCaseConnector();
return {
...payload,
...(payload.connector == null &&
(type === ActionTypes.create_case || type === ActionTypes.connector) && {
connector: noneConnector,
}),
...(isEmpty(payload.status) &&
type === ActionTypes.create_case && { status: CaseStatuses.open }),
...(type === ActionTypes.create_case && isEmpty(payload.owner) && { owner }),
...(type === ActionTypes.create_case &&
isEmpty(payload.settings) && { settings: { syncAlerts: true } }),
};
};
const convertPayload = (
fields: string[],
value: string | null,
owner: string
): Record<string, unknown> => {
if (value == null) {
return {};
}
const unsafeDecodedValue = decodeValue(value);
return fields.reduce(
(payload, field) => ({
...payload,
...getSingleFieldPayload(field, unsafeDecodedValue[field] ?? unsafeDecodedValue, owner),
}),
{}
);
};
const decodeValue = (value: string) => {
try {
return isString(value) ? JSON.parse(value) : value ?? {};
} catch {
return value;
}
};
const getSingleFieldPayload = (
field: string,
value: Record<string, unknown> | string,
owner: string
): Record<string, unknown> => {
switch (field) {
case 'title':
case 'status':
case 'description':
return { [field]: isString(value) ? value : '' };
case 'owner':
return { [field]: isString(value) ? value : owner };
case 'settings':
case 'connector':
return { [field]: isPlainObject(value) ? value : {} };
case 'pushed':
return { externalService: isPlainObject(value) ? value : {} };
case 'tags':
return {
tags: isString(value)
? value.split(',').map((item) => item.trim())
: Array.isArray(value)
? value
: [],
};
case 'comment':
/**
* Until 7.10 the new_value of the comment user action
* was a string. In 7.11+ more fields were introduced to the comment's
* saved object and the new_value of the user actions changes to an
* stringify object. At that point of time no migrations were made to
* the user actions to accommodate the new formatting.
*
* We are taking care of it in this migration.
* If there response of the decodeValue function is not an object
* then we assume that the value is a string coming for a 7.10
* user action saved object.
*
* Also if the comment does not have an owner we default to the owner
* of the user action. It is impossible to create a user action
* with a different owner from the original case.
*/
return {
comment: isPlainObject(value)
? {
...(value as Record<string, unknown>),
...((value as Record<string, unknown>).owner == null && { owner }),
}
: {
comment: isString(value) ? value : '',
type: CommentType.user,
owner,
},
};
default:
return {};
}
};
export const removeOldReferences = (
references: SavedObjectUnsanitizedDoc<UserActions>['references']
): SavedObjectReference[] =>
(references ?? []).filter(
(ref) =>
ref.name !== USER_ACTION_OLD_ID_REF_NAME && ref.name !== USER_ACTION_OLD_PUSH_ID_REF_NAME
);

View file

@ -0,0 +1,23 @@
/*
* 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 interface UserActions {
action: string;
action_field: string[];
action_at: string;
action_by: { email: string; username: string; full_name: string };
new_value: string | null;
old_value: string | null;
owner: string;
}
export interface UserActionUnmigratedConnectorDocument {
action?: string;
action_field?: string[];
new_value?: string | null;
old_value?: string | null;
}

View file

@ -16,16 +16,13 @@ export const caseUserActionSavedObjectType: SavedObjectsType = {
convertToMultiNamespaceTypeVersion: '8.0.0',
mappings: {
properties: {
action_field: {
type: 'keyword',
},
action: {
type: 'keyword',
},
action_at: {
created_at: {
type: 'date',
},
action_by: {
created_by: {
properties: {
email: {
type: 'keyword',
@ -38,15 +35,24 @@ export const caseUserActionSavedObjectType: SavedObjectsType = {
},
},
},
new_value: {
type: 'text',
},
old_value: {
type: 'text',
payload: {
dynamic: false,
properties: {
connector: {
properties: {
// connector.type
type: { type: 'keyword' },
},
},
},
},
owner: {
type: 'keyword',
},
// The type of the action
type: {
type: 'keyword',
},
},
},
migrations: userActionsMigrations,

View file

@ -820,7 +820,7 @@ describe('CasesService', () => {
`);
});
it('returns a null external service connector when it cannot find the reference', async () => {
it('returns none external service connector when it cannot find the reference', async () => {
const { connector_id: id, ...restExternalConnector } = createExternalService()!;
const returnValue: SavedObjectsUpdateResponse<ESCaseAttributes> = {
type: CASE_SAVED_OBJECT,
@ -841,7 +841,7 @@ describe('CasesService', () => {
originalCase: {} as SavedObject<CaseAttributes>,
});
expect(res.attributes.external_service?.connector_id).toBeNull();
expect(res.attributes.external_service?.connector_id).toBe('none');
});
it('returns the saved object fields when it cannot find the reference for connector_id', async () => {
@ -866,28 +866,28 @@ describe('CasesService', () => {
});
expect(res).toMatchInlineSnapshot(`
Object {
"attributes": Object {
"external_service": Object {
"connector_id": null,
"connector_name": ".jira",
"external_id": "100",
"external_title": "awesome",
"external_url": "http://www.google.com",
"pushed_at": "2019-11-25T21:54:48.952Z",
"pushed_by": Object {
"email": "testemail@elastic.co",
"full_name": "elastic",
"username": "elastic",
},
Object {
"attributes": Object {
"external_service": Object {
"connector_id": "none",
"connector_name": ".jira",
"external_id": "100",
"external_title": "awesome",
"external_url": "http://www.google.com",
"pushed_at": "2019-11-25T21:54:48.952Z",
"pushed_by": Object {
"email": "testemail@elastic.co",
"full_name": "elastic",
"username": "elastic",
},
},
"id": "1",
"references": undefined,
"type": "cases",
"version": "1",
}
`);
},
"id": "1",
"references": undefined,
"type": "cases",
"version": "1",
}
`);
});
it('returns the connector.id after finding the reference', async () => {
@ -1082,7 +1082,7 @@ describe('CasesService', () => {
);
const res = await service.getCase({ unsecuredSavedObjectsClient, id: 'a' });
expect(res.attributes.external_service?.connector_id).toMatchInlineSnapshot(`null`);
expect(res.attributes.external_service?.connector_id).toMatchInlineSnapshot(`"none"`);
});
it('includes the external services fields when the connector id cannot be found in the references', async () => {
@ -1093,7 +1093,7 @@ describe('CasesService', () => {
expect(res.attributes.external_service).toMatchInlineSnapshot(`
Object {
"connector_id": null,
"connector_id": "none",
"connector_name": ".jira",
"external_id": "100",
"external_title": "awesome",

View file

@ -61,7 +61,7 @@ describe('case transforms', () => {
).toBeNull();
});
it('return a null external_service.connector_id field if it is none', () => {
it('return none external_service.connector_id field if it is none', () => {
expect(
transformUpdateResponseToExternalModel({
type: 'a',
@ -71,7 +71,7 @@ describe('case transforms', () => {
},
references: undefined,
}).attributes.external_service?.connector_id
).toBeNull();
).toBe('none');
});
it('return the external_service fields if it is populated', () => {
@ -87,7 +87,7 @@ describe('case transforms', () => {
}).attributes.external_service
).toMatchInlineSnapshot(`
Object {
"connector_id": null,
"connector_id": "none",
"connector_name": ".jira",
"external_id": "100",
"external_title": "awesome",
@ -267,7 +267,8 @@ describe('case transforms', () => {
it('creates an empty references array to delete the connector_id when connector_id is null and the original references is undefined', () => {
const transformedAttributes = transformAttributesToESModel({
external_service: createExternalService({ connector_id: null }),
// TODO: It was null. Check if it is correct
external_service: createExternalService({ connector_id: 'none' }),
});
expect(transformedAttributes.referenceHandler.build()).toEqual([]);
@ -386,17 +387,18 @@ describe('case transforms', () => {
).toBeNull();
});
it('sets external_service.connector_id to null when a reference cannot be found', () => {
it('sets external_service.connector_id to none when a reference cannot be found', () => {
const transformedSO = transformSavedObjectToExternalModel(
createCaseSavedObjectResponse({
externalService: createExternalService({ connector_id: null }),
// TODO: It was null. Check if it is correct
externalService: createExternalService({ connector_id: 'none' }),
})
);
expect(transformedSO.attributes.external_service?.connector_id).toBeNull();
expect(transformedSO.attributes.external_service?.connector_id).toBe('none');
expect(transformedSO.attributes.external_service).toMatchInlineSnapshot(`
Object {
"connector_id": null,
"connector_id": "none",
"connector_name": ".jira",
"external_id": "100",
"external_title": "awesome",

View file

@ -21,7 +21,7 @@ import {
CONNECTOR_ID_REFERENCE_NAME,
PUSH_CONNECTOR_ID_REFERENCE_NAME,
} from '../../common/constants';
import { CaseAttributes, CaseFullExternalService } from '../../../common/api';
import { CaseAttributes, CaseFullExternalService, NONE_CONNECTOR_ID } from '../../../common/api';
import {
findConnectorIdReference,
transformFieldsToESModel,
@ -200,6 +200,6 @@ function transformESExternalService(
return {
...externalService,
connector_id: connectorIdRef?.id ?? null,
connector_id: connectorIdRef?.id ?? NONE_CONNECTOR_ID,
};
}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { noneConnectorId } from '../../common/api';
import { NONE_CONNECTOR_ID } from '../../common/api';
import { ConnectorReferenceHandler } from './connector_reference_handler';
describe('ConnectorReferenceHandler', () => {
@ -60,7 +60,7 @@ describe('ConnectorReferenceHandler', () => {
it('removes a reference when the id field is the none connector', () => {
const handler = new ConnectorReferenceHandler([
{ id: noneConnectorId, name: 'a', type: '1' },
{ id: NONE_CONNECTOR_ID, name: 'a', type: '1' },
]);
expect(handler.build([{ id: 'hello', type: '1', name: 'a' }])).toMatchInlineSnapshot(

View file

@ -6,7 +6,7 @@
*/
import { SavedObjectReference } from 'kibana/server';
import { noneConnectorId } from '../../common/api';
import { NONE_CONNECTOR_ID } from '../../common/api';
interface Reference {
soReference?: SavedObjectReference;
@ -21,7 +21,7 @@ export class ConnectorReferenceHandler {
// When id is null, or the none connector we'll try to remove the reference if it exists
// When id is undefined it means that we're doing a patch request and this particular field shouldn't be updated
// so we'll ignore it. If it was already in the reference array then it'll stay there when we merge them together below
if (id === null || id === noneConnectorId) {
if (id === null || id === NONE_CONNECTOR_ID) {
this.newReferences.push({ name });
} else if (id) {
this.newReferences.push({ soReference: { id, name, type }, name });

View file

@ -85,6 +85,11 @@ export const connectorMappingsServiceMock = (): ConnectorMappingsServiceMock =>
export const createUserActionServiceMock = (): CaseUserActionServiceMock => {
const service: PublicMethodsOf<CaseUserActionService> = {
bulkCreateCaseDeletion: jest.fn(),
bulkCreateUpdateCase: jest.fn(),
bulkCreateAttachmentDeletion: jest.fn(),
createUserAction: jest.fn(),
create: jest.fn(),
getAll: jest.fn(),
bulkCreate: jest.fn(),
};

View file

@ -10,11 +10,12 @@ import { ESConnectorFields } from '.';
import { CONNECTOR_ID_REFERENCE_NAME, PUSH_CONNECTOR_ID_REFERENCE_NAME } from '../common/constants';
import {
CaseConnector,
CaseExternalServiceBasic,
CaseFullExternalService,
CaseStatuses,
CaseType,
ConnectorTypes,
noneConnectorId,
NONE_CONNECTOR_ID,
} from '../../common/api';
import { CASE_SAVED_OBJECT, SECURITY_SOLUTION_OWNER } from '../../common/constants';
import { ESCaseAttributes, ExternalServicesWithoutConnectorId } from './cases/types';
@ -80,8 +81,8 @@ export const createJiraConnector = ({
};
export const createExternalService = (
overrides?: Partial<CaseFullExternalService>
): CaseFullExternalService => ({
overrides?: Partial<CaseExternalServiceBasic>
): CaseExternalServiceBasic => ({
connector_id: '100',
connector_name: '.jira',
external_id: '100',
@ -178,7 +179,7 @@ export const createSavedObjectReferences = ({
connector?: ESCaseConnectorWithId;
externalService?: CaseFullExternalService;
} = {}): SavedObjectReference[] => [
...(connector && connector.id !== noneConnectorId
...(connector && connector.id !== NONE_CONNECTOR_ID
? [
{
id: connector.id,

View file

@ -0,0 +1,137 @@
/*
* 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 { SavedObjectReference } from 'kibana/server';
import {
CASE_COMMENT_SAVED_OBJECT,
CASE_SAVED_OBJECT,
SUB_CASE_SAVED_OBJECT,
} from '../../../common/constants';
import {
CASE_REF_NAME,
COMMENT_REF_NAME,
CONNECTOR_ID_REFERENCE_NAME,
PUSH_CONNECTOR_ID_REFERENCE_NAME,
SUB_CASE_REF_NAME,
} from '../../common/constants';
import {
ActionTypes,
CaseConnector,
CaseExternalServiceBasic,
NONE_CONNECTOR_ID,
User,
} from '../../../common/api';
import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server';
import {
BuilderParameters,
BuilderReturnValue,
CommonBuilderArguments,
UserActionParameters,
} from './types';
export abstract class UserActionBuilder {
protected getCommonUserActionAttributes({ user, owner }: { user: User; owner: string }) {
return {
created_at: new Date().toISOString(),
created_by: user,
owner,
};
}
protected extractConnectorId(connector: CaseConnector): Omit<CaseConnector, 'id'> {
const { id, ...restConnector } = connector;
return restConnector;
}
protected createCaseReferences(caseId: string, subCaseId?: string): SavedObjectReference[] {
return [
{
type: CASE_SAVED_OBJECT,
name: CASE_REF_NAME,
id: caseId,
},
...(subCaseId
? [
{
type: SUB_CASE_SAVED_OBJECT,
name: SUB_CASE_REF_NAME,
id: subCaseId,
},
]
: []),
];
}
protected createActionReference(id: string | null, name: string): SavedObjectReference[] {
return id != null && id !== NONE_CONNECTOR_ID
? [{ id, type: ACTION_SAVED_OBJECT_TYPE, name }]
: [];
}
protected createCommentReferences(id: string | null): SavedObjectReference[] {
return id != null
? [
{
type: CASE_COMMENT_SAVED_OBJECT,
name: COMMENT_REF_NAME,
id,
},
]
: [];
}
protected createConnectorReference(id: string | null): SavedObjectReference[] {
return this.createActionReference(id, CONNECTOR_ID_REFERENCE_NAME);
}
protected createConnectorPushReference(id: string | null): SavedObjectReference[] {
return this.createActionReference(id, PUSH_CONNECTOR_ID_REFERENCE_NAME);
}
protected extractConnectorIdFromExternalService(
externalService: CaseExternalServiceBasic
): Omit<CaseExternalServiceBasic, 'connector_id'> {
const { connector_id: connectorId, ...restExternalService } = externalService;
return restExternalService;
}
protected buildCommonUserAction = ({
action,
user,
owner,
value,
valueKey,
caseId,
subCaseId,
attachmentId,
connectorId,
type,
}: CommonBuilderArguments): BuilderReturnValue => {
return {
attributes: {
...this.getCommonUserActionAttributes({ user, owner }),
action,
payload: { [valueKey]: value },
type,
},
references: [
...this.createCaseReferences(caseId, subCaseId),
...this.createCommentReferences(attachmentId ?? null),
...(type === ActionTypes.connector
? this.createConnectorReference(connectorId ?? null)
: []),
...(type === ActionTypes.pushed
? this.createConnectorPushReference(connectorId ?? null)
: []),
],
};
};
public abstract build<T extends keyof BuilderParameters>(
args: UserActionParameters<T>
): BuilderReturnValue;
}

View file

@ -0,0 +1,477 @@
/*
* 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 { SECURITY_SOLUTION_OWNER } from '../../../common';
import {
Actions,
ActionTypes,
CaseStatuses,
CommentType,
ConnectorTypes,
} from '../../../common/api';
import { BuilderFactory } from './builder_factory';
import { casePayload, externalService } from './mocks';
describe('UserActionBuilder', () => {
const builderFactory = new BuilderFactory();
const commonArgs = {
caseId: '123',
user: { full_name: 'Elastic User', username: 'elastic', email: 'elastic@elastic.co' },
owner: SECURITY_SOLUTION_OWNER,
};
beforeAll(() => {
jest.useFakeTimers('modern');
jest.setSystemTime(new Date('2022-01-09T22:00:00.000Z'));
});
afterAll(() => {
jest.useRealTimers();
});
it('builds a title user action correctly', () => {
const builder = builderFactory.getBuilder(ActionTypes.title)!;
const userAction = builder.build({
payload: { title: 'test' },
...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 {
"title": "test",
},
"type": "title",
},
"references": Array [
Object {
"id": "123",
"name": "associated-cases",
"type": "cases",
},
],
}
`);
});
it('builds a connector user action correctly', () => {
const builder = builderFactory.getBuilder(ActionTypes.connector)!;
const userAction = builder.build({
payload: {
connector: {
id: '456',
name: 'ServiceNow SN',
type: ConnectorTypes.serviceNowSIR,
fields: {
category: 'Denial of Service',
destIp: true,
malwareHash: true,
malwareUrl: true,
priority: '2',
sourceIp: true,
subcategory: '45',
},
},
},
...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 {
"connector": Object {
"fields": Object {
"category": "Denial of Service",
"destIp": true,
"malwareHash": true,
"malwareUrl": true,
"priority": "2",
"sourceIp": true,
"subcategory": "45",
},
"name": "ServiceNow SN",
"type": ".servicenow-sir",
},
},
"type": "connector",
},
"references": Array [
Object {
"id": "123",
"name": "associated-cases",
"type": "cases",
},
Object {
"id": "456",
"name": "connectorId",
"type": "action",
},
],
}
`);
});
it('builds a comment user action correctly', () => {
const builder = builderFactory.getBuilder(ActionTypes.comment)!;
const userAction = builder.build({
action: Actions.update,
payload: {
attachment: {
comment: 'a comment!',
type: CommentType.user,
owner: SECURITY_SOLUTION_OWNER,
},
},
attachmentId: 'test-id',
...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 {
"comment": Object {
"comment": "a comment!",
"owner": "securitySolution",
"type": "user",
},
},
"type": "comment",
},
"references": Array [
Object {
"id": "123",
"name": "associated-cases",
"type": "cases",
},
Object {
"id": "test-id",
"name": "associated-cases-comments",
"type": "cases-comments",
},
],
}
`);
});
it('builds a description user action correctly', () => {
const builder = builderFactory.getBuilder(ActionTypes.description)!;
const userAction = builder.build({
payload: { description: 'test' },
...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 {
"description": "test",
},
"type": "description",
},
"references": Array [
Object {
"id": "123",
"name": "associated-cases",
"type": "cases",
},
],
}
`);
});
it('builds a pushed user action correctly', () => {
const builder = builderFactory.getBuilder(ActionTypes.pushed)!;
const userAction = builder.build({
payload: { externalService },
...commonArgs,
});
expect(userAction).toMatchInlineSnapshot(`
Object {
"attributes": Object {
"action": "push_to_service",
"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 {
"externalService": Object {
"connector_name": "ServiceNow SN",
"external_id": "external-id",
"external_title": "SIR0010037",
"external_url": "https://dev92273.service-now.com/nav_to.do?uri=sn_si_incident.do?sys_id=external-id",
"pushed_at": "2021-02-03T17:41:26.108Z",
"pushed_by": Object {
"email": "elastic@elastic.co",
"full_name": "Elastic",
"username": "elastic",
},
},
},
"type": "pushed",
},
"references": Array [
Object {
"id": "123",
"name": "associated-cases",
"type": "cases",
},
Object {
"id": "456",
"name": "pushConnectorId",
"type": "action",
},
],
}
`);
});
it('builds a tags user action correctly', () => {
const builder = builderFactory.getBuilder(ActionTypes.tags)!;
const userAction = builder.build({
action: Actions.add,
payload: { tags: ['one', 'two'] },
...commonArgs,
});
expect(userAction).toMatchInlineSnapshot(`
Object {
"attributes": Object {
"action": "add",
"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 {
"tags": Array [
"one",
"two",
],
},
"type": "tags",
},
"references": Array [
Object {
"id": "123",
"name": "associated-cases",
"type": "cases",
},
],
}
`);
});
it('builds a status user action correctly', () => {
const builder = builderFactory.getBuilder(ActionTypes.status)!;
const userAction = builder.build({
payload: { status: CaseStatuses.open },
...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 {
"status": "open",
},
"type": "status",
},
"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({
payload: { settings: { syncAlerts: true } },
...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 {
"settings": Object {
"syncAlerts": true,
},
},
"type": "settings",
},
"references": Array [
Object {
"id": "123",
"name": "associated-cases",
"type": "cases",
},
],
}
`);
});
it('builds a create case user action correctly', () => {
const builder = builderFactory.getBuilder(ActionTypes.create_case)!;
const userAction = builder.build({
payload: casePayload,
...commonArgs,
});
expect(userAction).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 {
"connector": Object {
"fields": Object {
"category": "Denial of Service",
"destIp": true,
"malwareHash": true,
"malwareUrl": true,
"priority": "2",
"sourceIp": true,
"subcategory": "45",
},
"name": "ServiceNow SN",
"type": ".servicenow-sir",
},
"description": "testing sir",
"owner": "securitySolution",
"settings": Object {
"syncAlerts": true,
},
"status": "open",
"tags": Array [
"sir",
],
"title": "Case SIR",
},
"type": "create_case",
},
"references": Array [
Object {
"id": "123",
"name": "associated-cases",
"type": "cases",
},
Object {
"id": "456",
"name": "connectorId",
"type": "action",
},
],
}
`);
});
it('builds a delete case user action correctly', () => {
const builder = builderFactory.getBuilder(ActionTypes.delete_case)!;
const userAction = builder.build({
payload: {},
connectorId: '456',
...commonArgs,
});
expect(userAction).toMatchInlineSnapshot(`
Object {
"attributes": Object {
"action": "delete",
"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 {},
"type": "delete_case",
},
"references": Array [
Object {
"id": "123",
"name": "associated-cases",
"type": "cases",
},
Object {
"id": "456",
"name": "connectorId",
"type": "action",
},
],
}
`);
});
});

View file

@ -0,0 +1,38 @@
/*
* 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 '../../../common/api';
import { CreateCaseUserActionBuilder } from './builders/create_case';
import { TitleUserActionBuilder } from './builders/title';
import { CommentUserActionBuilder } from './builders/comment';
import { ConnectorUserActionBuilder } from './builders/connector';
import { DescriptionUserActionBuilder } from './builders/description';
import { PushedUserActionBuilder } from './builders/pushed';
import { StatusUserActionBuilder } from './builders/status';
import { TagsUserActionBuilder } from './builders/tags';
import { SettingsUserActionBuilder } from './builders/settings';
import { DeleteCaseUserActionBuilder } from './builders/delete_case';
import { UserActionBuilder } from './abstract_builder';
const builderMap = {
title: TitleUserActionBuilder,
create_case: CreateCaseUserActionBuilder,
connector: ConnectorUserActionBuilder,
comment: CommentUserActionBuilder,
description: DescriptionUserActionBuilder,
pushed: PushedUserActionBuilder,
tags: TagsUserActionBuilder,
status: StatusUserActionBuilder,
settings: SettingsUserActionBuilder,
delete_case: DeleteCaseUserActionBuilder,
};
export class BuilderFactory {
getBuilder<T extends UserActionTypes>(type: T): UserActionBuilder | undefined {
return new builderMap[type]();
}
}

View file

@ -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 { ActionTypes, Actions } from '../../../../common/api';
import { UserActionBuilder } from '../abstract_builder';
import { UserActionParameters, BuilderReturnValue } from '../types';
export class CommentUserActionBuilder extends UserActionBuilder {
build(args: UserActionParameters<'comment'>): BuilderReturnValue {
return this.buildCommonUserAction({
...args,
action: args.action ?? Actions.update,
valueKey: 'comment',
value: args.payload.attachment,
type: ActionTypes.comment,
});
}
}

View file

@ -0,0 +1,23 @@
/*
* 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 ConnectorUserActionBuilder extends UserActionBuilder {
build(args: UserActionParameters<'connector'>): BuilderReturnValue {
return this.buildCommonUserAction({
...args,
action: Actions.update,
valueKey: 'connector',
value: this.extractConnectorId(args.payload.connector),
type: ActionTypes.connector,
connectorId: args.payload.connector.id,
});
}
}

View file

@ -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 { Actions, ActionTypes, CaseStatuses } from '../../../../common/api';
import { UserActionBuilder } from '../abstract_builder';
import { UserActionParameters, BuilderReturnValue } from '../types';
export class CreateCaseUserActionBuilder extends UserActionBuilder {
build(args: UserActionParameters<'create_case'>): BuilderReturnValue {
const { payload, caseId, subCaseId, owner, user } = args;
const connectorWithoutId = this.extractConnectorId(payload.connector);
return {
attributes: {
...this.getCommonUserActionAttributes({ user, owner }),
action: Actions.create,
payload: { ...payload, connector: connectorWithoutId, status: CaseStatuses.open },
type: ActionTypes.create_case,
},
references: [
...this.createCaseReferences(caseId, subCaseId),
...this.createConnectorReference(payload.connector.id),
],
};
}
}

View file

@ -0,0 +1,28 @@
/*
* 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 DeleteCaseUserActionBuilder extends UserActionBuilder {
build(args: UserActionParameters<'delete_case'>): BuilderReturnValue {
const { caseId, owner, user, connectorId } = args;
return {
attributes: {
...this.getCommonUserActionAttributes({ user, owner }),
action: Actions.delete,
payload: {},
type: ActionTypes.delete_case,
},
references: [
...this.createCaseReferences(caseId),
...this.createConnectorReference(connectorId ?? null),
],
};
}
}

View file

@ -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 DescriptionUserActionBuilder extends UserActionBuilder {
build(args: UserActionParameters<'description'>): BuilderReturnValue {
return this.buildCommonUserAction({
...args,
action: Actions.update,
valueKey: 'description',
type: ActionTypes.description,
value: args.payload.description,
});
}
}

View file

@ -0,0 +1,23 @@
/*
* 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 PushedUserActionBuilder extends UserActionBuilder {
build(args: UserActionParameters<'pushed'>): BuilderReturnValue {
return this.buildCommonUserAction({
...args,
action: Actions.push_to_service,
valueKey: 'externalService',
value: this.extractConnectorIdFromExternalService(args.payload.externalService),
type: ActionTypes.pushed,
connectorId: args.payload.externalService.connector_id,
});
}
}

View file

@ -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 SettingsUserActionBuilder extends UserActionBuilder {
build(args: UserActionParameters<'settings'>): BuilderReturnValue {
return this.buildCommonUserAction({
...args,
action: Actions.update,
valueKey: 'settings',
value: args.payload.settings,
type: ActionTypes.settings,
});
}
}

View file

@ -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 StatusUserActionBuilder extends UserActionBuilder {
build(args: UserActionParameters<'status'>): BuilderReturnValue {
return this.buildCommonUserAction({
...args,
action: Actions.update,
valueKey: 'status',
value: args.payload.status,
type: ActionTypes.status,
});
}
}

View file

@ -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 { ActionTypes, Actions } from '../../../../common/api';
import { UserActionBuilder } from '../abstract_builder';
import { UserActionParameters, BuilderReturnValue } from '../types';
export class TagsUserActionBuilder extends UserActionBuilder {
build(args: UserActionParameters<'tags'>): BuilderReturnValue {
return this.buildCommonUserAction({
...args,
action: args.action ?? Actions.add,
valueKey: 'tags',
value: args.payload.tags,
type: ActionTypes.tags,
});
}
}

View file

@ -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 { BuilderReturnValue, UserActionParameters } from '../types';
export class TitleUserActionBuilder extends UserActionBuilder {
build(args: UserActionParameters<'title'>): BuilderReturnValue {
return this.buildCommonUserAction({
...args,
action: Actions.update,
valueKey: 'title',
value: args.payload.title,
type: ActionTypes.title,
});
}
}

View file

@ -1,332 +0,0 @@
/*
* 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 { UserActionField } from '../../../common/api';
import { createConnectorObject, createExternalService, createJiraConnector } from '../test_utils';
import { buildCaseUserActionItem } from './helpers';
const defaultFields = () => ({
actionAt: 'now',
actionBy: {
email: 'a',
full_name: 'j',
username: '1',
},
caseId: '300',
owner: 'securitySolution',
});
describe('user action helpers', () => {
describe('buildCaseUserActionItem', () => {
describe('push user action', () => {
it('extracts the external_service connector_id to references for a new pushed user action', () => {
const userAction = buildCaseUserActionItem({
...defaultFields(),
action: 'push-to-service',
fields: ['pushed'],
newValue: createExternalService(),
});
const parsedExternalService = JSON.parse(userAction.attributes.new_value!);
expect(parsedExternalService).not.toHaveProperty('connector_id');
expect(parsedExternalService).toMatchInlineSnapshot(`
Object {
"connector_name": ".jira",
"external_id": "100",
"external_title": "awesome",
"external_url": "http://www.google.com",
"pushed_at": "2019-11-25T21:54:48.952Z",
"pushed_by": Object {
"email": "testemail@elastic.co",
"full_name": "elastic",
"username": "elastic",
},
}
`);
expect(userAction.references).toMatchInlineSnapshot(`
Array [
Object {
"id": "300",
"name": "associated-cases",
"type": "cases",
},
Object {
"id": "100",
"name": "pushConnectorId",
"type": "action",
},
]
`);
expect(userAction.attributes.old_value).toBeNull();
});
it('extract the external_service connector_id to references for new and old pushed user action', () => {
const userAction = buildCaseUserActionItem({
...defaultFields(),
action: 'push-to-service',
fields: ['pushed'],
newValue: createExternalService(),
oldValue: createExternalService({ connector_id: '5' }),
});
const parsedNewExternalService = JSON.parse(userAction.attributes.new_value!);
const parsedOldExternalService = JSON.parse(userAction.attributes.old_value!);
expect(parsedNewExternalService).not.toHaveProperty('connector_id');
expect(parsedOldExternalService).not.toHaveProperty('connector_id');
expect(userAction.references).toEqual([
{ id: '300', name: 'associated-cases', type: 'cases' },
{ id: '100', name: 'pushConnectorId', type: 'action' },
{ id: '5', name: 'oldPushConnectorId', type: 'action' },
]);
});
it('leaves the object unmodified when it is not a valid push user action', () => {
const userAction = buildCaseUserActionItem({
...defaultFields(),
action: 'push-to-service',
fields: ['invalid field'] as unknown as UserActionField,
newValue: 'hello' as unknown as Record<string, unknown>,
});
expect(userAction.attributes.old_value).toBeNull();
expect(userAction).toMatchInlineSnapshot(`
Object {
"attributes": Object {
"action": "push-to-service",
"action_at": "now",
"action_by": Object {
"email": "a",
"full_name": "j",
"username": "1",
},
"action_field": Array [
"invalid field",
],
"new_value": "hello",
"old_value": null,
"owner": "securitySolution",
},
"references": Array [
Object {
"id": "300",
"name": "associated-cases",
"type": "cases",
},
],
}
`);
});
});
describe('update connector user action', () => {
it('extracts the connector id to references for a new create connector user action', () => {
const userAction = buildCaseUserActionItem({
...defaultFields(),
action: 'update',
fields: ['connector'],
newValue: createJiraConnector(),
});
const parsedConnector = JSON.parse(userAction.attributes.new_value!);
expect(parsedConnector).not.toHaveProperty('id');
expect(parsedConnector).toMatchInlineSnapshot(`
Object {
"fields": Object {
"issueType": "bug",
"parent": "2",
"priority": "high",
},
"name": ".jira",
"type": ".jira",
}
`);
expect(userAction.references).toMatchInlineSnapshot(`
Array [
Object {
"id": "300",
"name": "associated-cases",
"type": "cases",
},
Object {
"id": "1",
"name": "connectorId",
"type": "action",
},
]
`);
expect(userAction.attributes.old_value).toBeNull();
});
it('extracts the connector id to references for a new and old create connector user action', () => {
const userAction = buildCaseUserActionItem({
...defaultFields(),
action: 'update',
fields: ['connector'],
newValue: createJiraConnector(),
oldValue: { ...createJiraConnector(), id: '5' },
});
const parsedNewConnector = JSON.parse(userAction.attributes.new_value!);
const parsedOldConnector = JSON.parse(userAction.attributes.new_value!);
expect(parsedNewConnector).not.toHaveProperty('id');
expect(parsedOldConnector).not.toHaveProperty('id');
expect(userAction.references).toEqual([
{ id: '300', name: 'associated-cases', type: 'cases' },
{ id: '1', name: 'connectorId', type: 'action' },
{ id: '5', name: 'oldConnectorId', type: 'action' },
]);
});
it('leaves the object unmodified when it is not a valid create connector user action', () => {
const userAction = buildCaseUserActionItem({
...defaultFields(),
action: 'update',
fields: ['invalid field'] as unknown as UserActionField,
newValue: 'hello' as unknown as Record<string, unknown>,
oldValue: 'old value' as unknown as Record<string, unknown>,
});
expect(userAction).toMatchInlineSnapshot(`
Object {
"attributes": Object {
"action": "update",
"action_at": "now",
"action_by": Object {
"email": "a",
"full_name": "j",
"username": "1",
},
"action_field": Array [
"invalid field",
],
"new_value": "hello",
"old_value": "old value",
"owner": "securitySolution",
},
"references": Array [
Object {
"id": "300",
"name": "associated-cases",
"type": "cases",
},
],
}
`);
});
});
describe('create connector user action', () => {
it('extracts the connector id to references for a new create connector user action', () => {
const userAction = buildCaseUserActionItem({
...defaultFields(),
action: 'create',
fields: ['connector'],
newValue: createConnectorObject(),
});
const parsedConnector = JSON.parse(userAction.attributes.new_value!);
expect(parsedConnector.connector).not.toHaveProperty('id');
expect(parsedConnector).toMatchInlineSnapshot(`
Object {
"connector": Object {
"fields": Object {
"issueType": "bug",
"parent": "2",
"priority": "high",
},
"name": ".jira",
"type": ".jira",
},
}
`);
expect(userAction.references).toMatchInlineSnapshot(`
Array [
Object {
"id": "300",
"name": "associated-cases",
"type": "cases",
},
Object {
"id": "1",
"name": "connectorId",
"type": "action",
},
]
`);
expect(userAction.attributes.old_value).toBeNull();
});
it('extracts the connector id to references for a new and old create connector user action', () => {
const userAction = buildCaseUserActionItem({
...defaultFields(),
action: 'create',
fields: ['connector'],
newValue: createConnectorObject(),
oldValue: createConnectorObject({ id: '5' }),
});
const parsedNewConnector = JSON.parse(userAction.attributes.new_value!);
const parsedOldConnector = JSON.parse(userAction.attributes.new_value!);
expect(parsedNewConnector.connector).not.toHaveProperty('id');
expect(parsedOldConnector.connector).not.toHaveProperty('id');
expect(userAction.references).toEqual([
{ id: '300', name: 'associated-cases', type: 'cases' },
{ id: '1', name: 'connectorId', type: 'action' },
{ id: '5', name: 'oldConnectorId', type: 'action' },
]);
});
it('leaves the object unmodified when it is not a valid create connector user action', () => {
const userAction = buildCaseUserActionItem({
...defaultFields(),
action: 'create',
fields: ['invalid action'] as unknown as UserActionField,
newValue: 'new json value' as unknown as Record<string, unknown>,
oldValue: 'old value' as unknown as Record<string, unknown>,
});
expect(userAction).toMatchInlineSnapshot(`
Object {
"attributes": Object {
"action": "create",
"action_at": "now",
"action_by": Object {
"email": "a",
"full_name": "j",
"username": "1",
},
"action_field": Array [
"invalid action",
],
"new_value": "new json value",
"old_value": "old value",
"owner": "securitySolution",
},
"references": Array [
Object {
"id": "300",
"name": "associated-cases",
"type": "cases",
},
],
}
`);
});
});
});
});

View file

@ -1,343 +0,0 @@
/*
* 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 { SavedObject, SavedObjectReference, SavedObjectsUpdateResponse } from 'kibana/server';
import { get, isPlainObject, isString } from 'lodash';
import deepEqual from 'fast-deep-equal';
import {
CaseUserActionAttributes,
SubCaseAttributes,
User,
UserAction,
UserActionField,
CaseAttributes,
OWNER_FIELD,
} from '../../../common/api';
import {
CASE_COMMENT_SAVED_OBJECT,
CASE_SAVED_OBJECT,
SUB_CASE_SAVED_OBJECT,
} from '../../../common/constants';
import { isTwoArraysDifference } from '../../client/utils';
import { UserActionItem } from '.';
import { extractConnectorId } from './transform';
import { UserActionFieldType } from './types';
import { CASE_REF_NAME, COMMENT_REF_NAME, SUB_CASE_REF_NAME } from '../../common/constants';
interface BuildCaseUserActionParams {
action: UserAction;
actionAt: string;
actionBy: User;
caseId: string;
owner: string;
fields: UserActionField;
newValue?: Record<string, unknown> | string | null;
oldValue?: Record<string, unknown> | string | null;
subCaseId?: string;
}
export const buildCaseUserActionItem = ({
action,
actionAt,
actionBy,
caseId,
fields,
newValue,
oldValue,
subCaseId,
owner,
}: BuildCaseUserActionParams): UserActionItem => {
const { transformedActionDetails: transformedNewValue, references: newValueReferences } =
extractConnectorId({
action,
actionFields: fields,
actionDetails: newValue,
fieldType: UserActionFieldType.New,
});
const { transformedActionDetails: transformedOldValue, references: oldValueReferences } =
extractConnectorId({
action,
actionFields: fields,
actionDetails: oldValue,
fieldType: UserActionFieldType.Old,
});
return {
attributes: transformNewUserAction({
actionField: fields,
action,
actionAt,
owner,
...actionBy,
newValue: transformedNewValue,
oldValue: transformedOldValue,
}),
references: [
...createCaseReferences(caseId, subCaseId),
...newValueReferences,
...oldValueReferences,
],
};
};
const transformNewUserAction = ({
actionField,
action,
actionAt,
email,
// eslint-disable-next-line @typescript-eslint/naming-convention
full_name,
owner,
newValue = null,
oldValue = null,
username,
}: {
actionField: UserActionField;
action: UserAction;
actionAt: string;
owner: string;
email?: string | null;
full_name?: string | null;
newValue?: string | null;
oldValue?: string | null;
username?: string | null;
}): CaseUserActionAttributes => ({
action_field: actionField,
action,
action_at: actionAt,
action_by: { email, full_name, username },
new_value: newValue,
old_value: oldValue,
owner,
});
const createCaseReferences = (caseId: string, subCaseId?: string): SavedObjectReference[] => [
{
type: CASE_SAVED_OBJECT,
name: CASE_REF_NAME,
id: caseId,
},
...(subCaseId
? [
{
type: SUB_CASE_SAVED_OBJECT,
name: SUB_CASE_REF_NAME,
id: subCaseId,
},
]
: []),
];
interface BuildCommentUserActionItem extends BuildCaseUserActionParams {
commentId: string;
}
export const buildCommentUserActionItem = (params: BuildCommentUserActionItem): UserActionItem => {
const { commentId } = params;
const { attributes, references } = buildCaseUserActionItem(params);
return {
attributes,
references: [
...references,
{
type: CASE_COMMENT_SAVED_OBJECT,
name: COMMENT_REF_NAME,
id: commentId,
},
],
};
};
const userActionFieldsAllowed: UserActionField = [
'comment',
'connector',
'description',
'tags',
'title',
'status',
'settings',
'sub_case',
OWNER_FIELD,
];
interface CaseSubIDs {
caseId: string;
subCaseId?: string;
}
type GetCaseAndSubID = <T>(so: SavedObjectsUpdateResponse<T>) => CaseSubIDs;
/**
* Abstraction functions to retrieve a given field and the caseId and subCaseId depending on
* whether we're interacting with a case or a sub case.
*/
interface Getters {
getCaseAndSubID: GetCaseAndSubID;
}
interface OwnerEntity {
owner: string;
}
/**
* The entity associated with the user action must contain an owner field
*/
const buildGenericCaseUserActions = <T extends OwnerEntity>({
actionDate,
actionBy,
originalCases,
updatedCases,
allowedFields,
getters,
}: {
actionDate: string;
actionBy: User;
originalCases: Array<SavedObject<T>>;
updatedCases: Array<SavedObjectsUpdateResponse<T>>;
allowedFields: UserActionField;
getters: Getters;
}): UserActionItem[] => {
const { getCaseAndSubID } = getters;
return updatedCases.reduce<UserActionItem[]>((acc, updatedItem) => {
const { caseId, subCaseId } = getCaseAndSubID(updatedItem);
// regardless of whether we're looking at a sub case or case, the id field will always be used to match between
// the original and the updated saved object
const originalItem = originalCases.find((oItem) => oItem.id === updatedItem.id);
if (originalItem != null) {
let userActions: UserActionItem[] = [];
const updatedFields = Object.keys(updatedItem.attributes) as UserActionField;
updatedFields.forEach((field) => {
if (allowedFields.includes(field)) {
const origValue = get(originalItem, ['attributes', field]);
const updatedValue = get(updatedItem, ['attributes', field]);
if (isString(origValue) && isString(updatedValue) && origValue !== updatedValue) {
userActions = [
...userActions,
buildCaseUserActionItem({
action: 'update',
actionAt: actionDate,
actionBy,
caseId,
subCaseId,
fields: [field],
newValue: updatedValue,
oldValue: origValue,
owner: originalItem.attributes.owner,
}),
];
} else if (Array.isArray(origValue) && Array.isArray(updatedValue)) {
const compareValues = isTwoArraysDifference(origValue, updatedValue);
if (compareValues && compareValues.addedItems.length > 0) {
userActions = [
...userActions,
buildCaseUserActionItem({
action: 'add',
actionAt: actionDate,
actionBy,
caseId,
subCaseId,
fields: [field],
newValue: compareValues.addedItems.join(', '),
owner: originalItem.attributes.owner,
}),
];
}
if (compareValues && compareValues.deletedItems.length > 0) {
userActions = [
...userActions,
buildCaseUserActionItem({
action: 'delete',
actionAt: actionDate,
actionBy,
caseId,
subCaseId,
fields: [field],
newValue: compareValues.deletedItems.join(', '),
owner: originalItem.attributes.owner,
}),
];
}
} else if (
isPlainObject(origValue) &&
isPlainObject(updatedValue) &&
!deepEqual(origValue, updatedValue)
) {
userActions = [
...userActions,
buildCaseUserActionItem({
action: 'update',
actionAt: actionDate,
actionBy,
caseId,
subCaseId,
fields: [field],
newValue: updatedValue,
oldValue: origValue,
owner: originalItem.attributes.owner,
}),
];
}
}
});
return [...acc, ...userActions];
}
return acc;
}, []);
};
/**
* Create a user action for an updated sub case.
*/
export const buildSubCaseUserActions = (args: {
actionDate: string;
actionBy: User;
originalSubCases: Array<SavedObject<SubCaseAttributes>>;
updatedSubCases: Array<SavedObjectsUpdateResponse<SubCaseAttributes>>;
}): UserActionItem[] => {
const getCaseAndSubID = (so: SavedObjectsUpdateResponse<SubCaseAttributes>): CaseSubIDs => {
const caseId = so.references?.find((ref) => ref.type === CASE_SAVED_OBJECT)?.id ?? '';
return { caseId, subCaseId: so.id };
};
const getters: Getters = {
getCaseAndSubID,
};
return buildGenericCaseUserActions({
actionDate: args.actionDate,
actionBy: args.actionBy,
originalCases: args.originalSubCases,
updatedCases: args.updatedSubCases,
allowedFields: ['status'],
getters,
});
};
/**
* Create a user action for an updated case.
*/
export const buildCaseUserActions = (args: {
actionDate: string;
actionBy: User;
originalCases: Array<SavedObject<CaseAttributes>>;
updatedCases: Array<SavedObjectsUpdateResponse<CaseAttributes>>;
}): UserActionItem[] => {
const caseGetIds: GetCaseAndSubID = <T>(so: SavedObjectsUpdateResponse<T>): CaseSubIDs => {
return { caseId: so.id };
};
const getters: Getters = {
getCaseAndSubID: caseGetIds,
};
return buildGenericCaseUserActions({ ...args, allowedFields: userActionFieldsAllowed, getters });
};

View file

@ -5,15 +5,35 @@
* 2.0.
*/
import { get, isEmpty } from 'lodash';
import {
Logger,
SavedObject,
SavedObjectReference,
SavedObjectsFindResponse,
SavedObjectsFindResult,
SavedObjectsUpdateResponse,
} from 'kibana/server';
import { isCreateConnector, isPush, isUpdateConnector } from '../../../common/utils/user_actions';
import { CaseUserActionAttributes, CaseUserActionResponse } from '../../../common/api';
import {
isConnectorUserAction,
isPushedUserAction,
isUserActionType,
isCreateCaseUserAction,
} from '../../../common/utils/user_actions';
import {
Actions,
ActionTypes,
CaseAttributes,
CaseUserActionAttributes,
CaseUserActionAttributesWithoutConnectorId,
CaseUserActionResponse,
CommentRequest,
NONE_CONNECTOR_ID,
SubCaseAttributes,
User,
} from '../../../common/api';
import {
CASE_SAVED_OBJECT,
CASE_USER_ACTION_SAVED_OBJECT,
@ -22,10 +42,17 @@ import {
CASE_COMMENT_SAVED_OBJECT,
} from '../../../common/constants';
import { ClientArgs } from '..';
import { UserActionFieldType } from './types';
import { CASE_REF_NAME, COMMENT_REF_NAME, SUB_CASE_REF_NAME } from '../../common/constants';
import { ConnectorIdReferenceName, PushConnectorIdReferenceName } from './transform';
import {
CASE_REF_NAME,
COMMENT_REF_NAME,
CONNECTOR_ID_REFERENCE_NAME,
PUSH_CONNECTOR_ID_REFERENCE_NAME,
SUB_CASE_REF_NAME,
} from '../../common/constants';
import { findConnectorIdReference } from '../transform';
import { isTwoArraysDifference } from '../../client/utils';
import { BuilderParameters, BuilderReturnValue, CommonArguments, CreateUserAction } from './types';
import { BuilderFactory } from './builder_factory';
interface GetCaseUserActionArgs extends ClientArgs {
caseId: string;
@ -33,17 +60,262 @@ interface GetCaseUserActionArgs extends ClientArgs {
}
export interface UserActionItem {
attributes: CaseUserActionAttributes;
attributes: CaseUserActionAttributesWithoutConnectorId;
references: SavedObjectReference[];
}
interface PostCaseUserActionArgs extends ClientArgs {
actions: UserActionItem[];
actions: BuilderReturnValue[];
}
interface CreateUserActionES<T> extends ClientArgs {
attributes: T;
references: SavedObjectReference[];
}
type CommonUserActionArgs = ClientArgs & CommonArguments;
interface BulkCreateCaseDeletionUserAction extends ClientArgs {
cases: Array<{ id: string; owner: string; subCaseId?: string; connectorId: string }>;
user: User;
}
interface GetUserActionItemByDifference extends CommonUserActionArgs {
field: string;
originalValue: unknown;
newValue: unknown;
}
interface BulkCreateBulkUpdateCaseUserActions extends ClientArgs {
originalCases: Array<SavedObject<CaseAttributes | SubCaseAttributes>>;
updatedCases: Array<SavedObjectsUpdateResponse<CaseAttributes | SubCaseAttributes>>;
user: User;
}
interface BulkCreateAttachmentDeletionUserAction extends Omit<CommonUserActionArgs, 'owner'> {
attachments: Array<{ id: string; owner: string; attachment: CommentRequest }>;
}
type CreateUserActionClient<T extends keyof BuilderParameters> = CreateUserAction<T> &
CommonUserActionArgs;
export class CaseUserActionService {
private static readonly userActionFieldsAllowed: Set<string> = new Set(Object.keys(ActionTypes));
private readonly builderFactory: BuilderFactory = new BuilderFactory();
constructor(private readonly log: Logger) {}
private getUserActionItemByDifference({
field,
originalValue,
newValue,
caseId,
subCaseId,
owner,
user,
}: GetUserActionItemByDifference): BuilderReturnValue[] {
if (!CaseUserActionService.userActionFieldsAllowed.has(field)) {
return [];
}
if (field === ActionTypes.tags) {
const tagsUserActionBuilder = this.builderFactory.getBuilder(ActionTypes.tags);
const compareValues = isTwoArraysDifference(originalValue, newValue);
const userActions = [];
if (compareValues && compareValues.addedItems.length > 0) {
const tagAddUserAction = tagsUserActionBuilder?.build({
action: Actions.add,
caseId,
subCaseId,
user,
owner,
payload: { tags: compareValues.addedItems },
});
if (tagAddUserAction) {
userActions.push(tagAddUserAction);
}
}
if (compareValues && compareValues.deletedItems.length > 0) {
const tagsDeleteUserAction = tagsUserActionBuilder?.build({
action: Actions.delete,
caseId,
subCaseId,
user,
owner,
payload: { tags: compareValues.deletedItems },
});
if (tagsDeleteUserAction) {
userActions.push(tagsDeleteUserAction);
}
}
return userActions;
}
if (isUserActionType(field) && newValue != null) {
const userActionBuilder = this.builderFactory.getBuilder(ActionTypes[field]);
const fieldUserAction = userActionBuilder?.build({
caseId,
subCaseId,
owner,
user,
payload: { [field]: newValue },
});
return fieldUserAction ? [fieldUserAction] : [];
}
return [];
}
public async bulkCreateCaseDeletion({
unsecuredSavedObjectsClient,
cases,
user,
}: BulkCreateCaseDeletionUserAction): Promise<void> {
this.log.debug(`Attempting to create a create case user action`);
const userActionsWithReferences = cases.reduce<BuilderReturnValue[]>((acc, caseInfo) => {
const userActionBuilder = this.builderFactory.getBuilder(ActionTypes.delete_case);
const deleteCaseUserAction = userActionBuilder?.build({
action: Actions.delete,
caseId: caseInfo.id,
user,
owner: caseInfo.owner,
connectorId: caseInfo.connectorId,
payload: {},
});
if (deleteCaseUserAction == null) {
return acc;
}
return [...acc, deleteCaseUserAction];
}, []);
await this.bulkCreate({ unsecuredSavedObjectsClient, actions: userActionsWithReferences });
}
public async bulkCreateUpdateCase({
unsecuredSavedObjectsClient,
originalCases,
updatedCases,
user,
}: BulkCreateBulkUpdateCaseUserActions): Promise<void> {
const userActionsWithReferences = updatedCases.reduce<BuilderReturnValue[]>(
(acc, updatedCase) => {
const originalCase = originalCases.find(({ id }) => id === updatedCase.id);
if (originalCase == null) {
return acc;
}
const caseId = updatedCase.id;
const owner = originalCase.attributes.owner;
const userActions: BuilderReturnValue[] = [];
const updatedFields = Object.keys(updatedCase.attributes);
updatedFields
.filter((field) => CaseUserActionService.userActionFieldsAllowed.has(field))
.forEach((field) => {
const originalValue = get(originalCase, ['attributes', field]);
const newValue = get(updatedCase, ['attributes', field]);
userActions.push(
...this.getUserActionItemByDifference({
unsecuredSavedObjectsClient,
field,
originalValue,
newValue,
user,
owner,
caseId,
})
);
});
return [...acc, ...userActions];
},
[]
);
await this.bulkCreate({ unsecuredSavedObjectsClient, actions: userActionsWithReferences });
}
public async bulkCreateAttachmentDeletion({
unsecuredSavedObjectsClient,
caseId,
subCaseId,
attachments,
user,
}: BulkCreateAttachmentDeletionUserAction): Promise<void> {
this.log.debug(`Attempting to create a create case user action`);
const userActionsWithReferences = attachments.reduce<BuilderReturnValue[]>(
(acc, attachment) => {
const userActionBuilder = this.builderFactory.getBuilder(ActionTypes.comment);
const deleteCommentUserAction = userActionBuilder?.build({
action: Actions.delete,
caseId,
subCaseId,
user,
owner: attachment.owner,
attachmentId: attachment.id,
payload: { attachment: attachment.attachment },
});
if (deleteCommentUserAction == null) {
return acc;
}
return [...acc, deleteCommentUserAction];
},
[]
);
await this.bulkCreate({ unsecuredSavedObjectsClient, actions: userActionsWithReferences });
}
public async createUserAction<T extends keyof BuilderParameters>({
unsecuredSavedObjectsClient,
action,
type,
caseId,
subCaseId,
user,
owner,
payload,
connectorId,
attachmentId,
}: CreateUserActionClient<T>) {
try {
this.log.debug(`Attempting to create a user action of type: ${type}`);
const userActionBuilder = this.builderFactory.getBuilder<T>(type);
const userAction = userActionBuilder?.build({
action,
caseId,
subCaseId,
user,
owner,
connectorId,
attachmentId,
payload,
});
if (userAction) {
const { attributes, references } = userAction;
await this.create({ unsecuredSavedObjectsClient, attributes, references });
}
} catch (error) {
this.log.error(`Error on creating user action of type: ${type}. Error: ${error}`);
throw error;
}
}
public async getAll({
unsecuredSavedObjectsClient,
caseId,
@ -53,14 +325,15 @@ export class CaseUserActionService {
const id = subCaseId ?? caseId;
const type = subCaseId ? SUB_CASE_SAVED_OBJECT : CASE_SAVED_OBJECT;
const userActions = await unsecuredSavedObjectsClient.find<CaseUserActionAttributes>({
type: CASE_USER_ACTION_SAVED_OBJECT,
hasReference: { type, id },
page: 1,
perPage: MAX_DOCS_PER_PAGE,
sortField: 'action_at',
sortOrder: 'asc',
});
const userActions =
await unsecuredSavedObjectsClient.find<CaseUserActionAttributesWithoutConnectorId>({
type: CASE_USER_ACTION_SAVED_OBJECT,
hasReference: { type, id },
page: 1,
perPage: MAX_DOCS_PER_PAGE,
sortField: 'created_at',
sortOrder: 'asc',
});
return transformFindResponseToExternalModel(userActions);
} catch (error) {
@ -69,14 +342,35 @@ export class CaseUserActionService {
}
}
public async create<T>({
unsecuredSavedObjectsClient,
attributes,
references,
}: CreateUserActionES<T>): Promise<void> {
try {
this.log.debug(`Attempting to POST a new case user action`);
await unsecuredSavedObjectsClient.create<T>(CASE_USER_ACTION_SAVED_OBJECT, attributes, {
references: references ?? [],
});
} catch (error) {
this.log.error(`Error on POST a new case user action: ${error}`);
throw error;
}
}
public async bulkCreate({
unsecuredSavedObjectsClient,
actions,
}: PostCaseUserActionArgs): Promise<void> {
if (isEmpty(actions)) {
return;
}
try {
this.log.debug(`Attempting to POST a new case user action`);
await unsecuredSavedObjectsClient.bulkCreate<CaseUserActionAttributes>(
await unsecuredSavedObjectsClient.bulkCreate(
actions.map((action) => ({ type: CASE_USER_ACTION_SAVED_OBJECT, ...action }))
);
} catch (error) {
@ -87,7 +381,7 @@ export class CaseUserActionService {
}
export function transformFindResponseToExternalModel(
userActions: SavedObjectsFindResponse<CaseUserActionAttributes>
userActions: SavedObjectsFindResponse<CaseUserActionAttributesWithoutConnectorId>
): SavedObjectsFindResponse<CaseUserActionResponse> {
return {
...userActions,
@ -99,17 +393,15 @@ export function transformFindResponseToExternalModel(
}
function transformToExternalModel(
userAction: SavedObjectsFindResult<CaseUserActionAttributes>
userAction: SavedObjectsFindResult<CaseUserActionAttributesWithoutConnectorId>
): SavedObjectsFindResult<CaseUserActionResponse> {
const { references } = userAction;
const newValueConnectorId = getConnectorIdFromReferences(UserActionFieldType.New, userAction);
const oldValueConnectorId = getConnectorIdFromReferences(UserActionFieldType.Old, userAction);
const caseId = findReferenceId(CASE_REF_NAME, CASE_SAVED_OBJECT, references) ?? '';
const commentId =
findReferenceId(COMMENT_REF_NAME, CASE_COMMENT_SAVED_OBJECT, references) ?? null;
const subCaseId = findReferenceId(SUB_CASE_REF_NAME, SUB_CASE_SAVED_OBJECT, references) ?? '';
const payload = addReferenceIdToPayload(userAction);
return {
...userAction,
@ -119,28 +411,50 @@ function transformToExternalModel(
case_id: caseId,
comment_id: commentId,
sub_case_id: subCaseId,
new_val_connector_id: newValueConnectorId,
old_val_connector_id: oldValueConnectorId,
},
payload,
} as CaseUserActionResponse,
};
}
const addReferenceIdToPayload = (
userAction: SavedObjectsFindResult<CaseUserActionAttributes>
): CaseUserActionAttributes['payload'] => {
const connectorId = getConnectorIdFromReferences(userAction);
const userActionAttributes = userAction.attributes;
if (isConnectorUserAction(userActionAttributes) || isCreateCaseUserAction(userActionAttributes)) {
return {
...userActionAttributes.payload,
connector: {
...userActionAttributes.payload.connector,
id: connectorId ?? NONE_CONNECTOR_ID,
},
};
} else if (isPushedUserAction(userActionAttributes)) {
return {
...userAction.attributes.payload,
externalService: {
...userActionAttributes.payload.externalService,
connector_id: connectorId ?? NONE_CONNECTOR_ID,
},
};
}
return userAction.attributes.payload;
};
function getConnectorIdFromReferences(
fieldType: UserActionFieldType,
userAction: SavedObjectsFindResult<CaseUserActionAttributes>
): string | null {
const {
// eslint-disable-next-line @typescript-eslint/naming-convention
attributes: { action, action_field },
references,
} = userAction;
const { references } = userAction;
if (isCreateConnector(action, action_field) || isUpdateConnector(action, action_field)) {
return findConnectorIdReference(ConnectorIdReferenceName[fieldType], references)?.id ?? null;
} else if (isPush(action, action_field)) {
return (
findConnectorIdReference(PushConnectorIdReferenceName[fieldType], references)?.id ?? null
);
if (
isConnectorUserAction(userAction.attributes) ||
isCreateCaseUserAction(userAction.attributes)
) {
return findConnectorIdReference(CONNECTOR_ID_REFERENCE_NAME, references)?.id ?? null;
} else if (isPushedUserAction(userAction.attributes)) {
return findConnectorIdReference(PUSH_CONNECTOR_ID_REFERENCE_NAME, references)?.id ?? null;
}
return null;

View file

@ -0,0 +1,96 @@
/*
* 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 { CASE_SAVED_OBJECT } from '../../../common/constants';
import { SECURITY_SOLUTION_OWNER } from '../../../common';
import { CaseStatuses, CommentType, ConnectorTypes } from '../../../common/api';
import { createCaseSavedObjectResponse } from '../test_utils';
export const casePayload = {
title: 'Case SIR',
tags: ['sir'],
description: 'testing sir',
connector: {
id: '456',
name: 'ServiceNow SN',
type: ConnectorTypes.serviceNowSIR as const,
fields: {
category: 'Denial of Service',
destIp: true,
malwareHash: true,
malwareUrl: true,
priority: '2',
sourceIp: true,
subcategory: '45',
},
},
settings: { syncAlerts: true },
owner: SECURITY_SOLUTION_OWNER,
};
export const externalService = {
pushed_at: '2021-02-03T17:41:26.108Z',
pushed_by: { username: 'elastic', full_name: 'Elastic', email: 'elastic@elastic.co' },
connector_id: '456',
connector_name: 'ServiceNow SN',
external_id: 'external-id',
external_title: 'SIR0010037',
external_url:
'https://dev92273.service-now.com/nav_to.do?uri=sn_si_incident.do?sys_id=external-id',
};
export const originalCases = [
{ ...createCaseSavedObjectResponse(), id: '1' },
{ ...createCaseSavedObjectResponse(), id: '2' },
];
export const updatedCases = [
{
...createCaseSavedObjectResponse(),
id: '1',
type: CASE_SAVED_OBJECT,
attributes: {
title: 'updated title',
status: CaseStatuses.closed,
connector: casePayload.connector,
},
references: [],
},
{
...createCaseSavedObjectResponse(),
id: '2',
type: CASE_SAVED_OBJECT,
attributes: {
description: 'updated desc',
tags: ['one', 'two'],
settings: { syncAlerts: false },
},
references: [],
},
];
export const comment = {
comment: 'a comment',
type: CommentType.user as const,
owner: SECURITY_SOLUTION_OWNER,
};
const alertComment = {
alertId: 'alert-id-1',
index: 'alert-index-1',
rule: {
id: 'rule-id-1',
name: 'rule-name-1',
},
type: CommentType.alert as const,
owner: SECURITY_SOLUTION_OWNER,
};
export const attachments = [
{ id: '1', attachment: { ...comment }, owner: SECURITY_SOLUTION_OWNER },
{ id: '2', attachment: { ...alertComment }, owner: SECURITY_SOLUTION_OWNER },
];

View file

@ -5,10 +5,99 @@
* 2.0.
*/
/**
* Indicates whether which user action field is being parsed, the new_value or the old_value.
*/
export enum UserActionFieldType {
New = 'New',
Old = 'Old',
import { SavedObjectReference } from 'kibana/server';
import {
CasePostRequest,
CaseSettings,
CaseStatuses,
CommentUserAction,
ConnectorUserAction,
PushedUserAction,
User,
UserAction,
UserActionTypes,
} from '../../../common/api';
export interface BuilderParameters {
title: {
parameters: { payload: { title: string } };
};
description: {
parameters: { payload: { description: string } };
};
status: {
parameters: { payload: { status: CaseStatuses } };
};
tags: {
parameters: { payload: { tags: string[] } };
};
pushed: {
parameters: {
payload: {
externalService: PushedUserAction['payload']['externalService'];
};
};
};
settings: {
parameters: { payload: { settings: CaseSettings } };
};
comment: {
parameters: {
payload: { attachment: CommentUserAction['payload']['comment'] };
};
};
connector: {
parameters: {
payload: {
connector: ConnectorUserAction['payload']['connector'];
};
};
};
create_case: {
parameters: {
payload: CasePostRequest;
};
};
delete_case: {
parameters: { payload: {} };
};
}
export interface CreateUserAction<T extends keyof BuilderParameters> {
type: T;
payload: BuilderParameters[T]['parameters']['payload'];
}
export type UserActionParameters<T extends keyof BuilderParameters> =
BuilderParameters[T]['parameters'] & CommonArguments;
export interface CommonArguments {
user: User;
caseId: string;
owner: string;
subCaseId?: string;
attachmentId?: string;
connectorId?: string;
action?: UserAction;
}
export interface Attributes {
action: UserAction;
created_at: string;
created_by: User;
owner: string;
type: UserActionTypes;
payload: Record<string, unknown>;
}
export interface BuilderReturnValue {
attributes: Attributes;
references: SavedObjectReference[];
}
export type CommonBuilderArguments = CommonArguments & {
action: UserAction;
type: UserActionTypes;
value: unknown;
valueKey: string;
};

View file

@ -1,5 +1,5 @@
{"attributes":{"closed_at":null,"closed_by":null,"connector":{"fields":[],"name":"none","type":".none"},"created_at":"2021-08-26T19:48:01.292Z","created_by":{"email":null,"full_name":null,"username":"elastic"},"description":"a description","external_service":null,"owner":"securitySolution","settings":{"syncAlerts":true},"status":"open","tags":["some tags"],"title":"A case to export","type":"individual","updated_at":"2021-08-26T19:48:30.151Z","updated_by":{"email":null,"full_name":null,"username":"elastic"}},"coreMigrationVersion":"8.0.0","id":"85541260-06a6-11ec-b3f9-3d05c48a7d46","migrationVersion":{"cases":"7.15.0"},"references":[],"type":"cases","updated_at":"2021-08-26T19:48:30.162Z","version":"WzM0NDEsMV0="}
{"attributes":{"action":"create","action_at":"2021-08-26T19:48:01.292Z","action_by":{"email":null,"full_name":null,"username":"elastic"},"action_field":["description","status","tags","title","connector","settings","owner"],"new_value":"{\"type\":\"individual\",\"title\":\"A case to export\",\"tags\":[\"some tags\"],\"description\":\"a description\",\"connector\":{\"id\":\"none\",\"name\":\"none\",\"type\":\".none\",\"fields\":null},\"settings\":{\"syncAlerts\":true},\"owner\":\"securitySolution\"}","old_value":null,"owner":"securitySolution"},"coreMigrationVersion":"8.0.0","id":"8cb85070-06a6-11ec-b3f9-3d05c48a7d46","migrationVersion":{"cases-user-actions":"7.14.0"},"references":[{"id":"85541260-06a6-11ec-b3f9-3d05c48a7d46","name":"associated-cases","type":"cases"}],"score":null,"sort":[1630007281292,7288],"type":"cases-user-actions","updated_at":"2021-08-26T19:48:13.687Z","version":"WzIzODIsMV0="}
{"attributes":{"associationType":"case","comment":"A comment for my case","created_at":"2021-08-26T19:48:30.151Z","created_by":{"email":null,"full_name":null,"username":"elastic"},"owner":"securitySolution","pushed_at":null,"pushed_by":null,"type":"user","updated_at":null,"updated_by":null},"coreMigrationVersion":"8.0.0","id":"9687c220-06a6-11ec-b3f9-3d05c48a7d46","migrationVersion":{"cases-comments":"7.16.0"},"references":[{"id":"85541260-06a6-11ec-b3f9-3d05c48a7d46","name":"associated-cases","type":"cases"}],"score":null,"sort":[1630007310151,9470],"type":"cases-comments","updated_at":"2021-08-26T19:48:30.161Z","version":"WzM0NDIsMV0="}
{"attributes":{"action":"create","action_at":"2021-08-26T19:48:30.151Z","action_by":{"email":null,"full_name":null,"username":"elastic"},"action_field":["comment"],"new_value":"{\"comment\":\"A comment for my case\",\"type\":\"user\",\"owner\":\"securitySolution\"}","old_value":null,"owner":"securitySolution"},"coreMigrationVersion":"8.0.0","id":"9710c840-06a6-11ec-b3f9-3d05c48a7d46","migrationVersion":{"cases-user-actions":"7.14.0"},"references":[{"id":"85541260-06a6-11ec-b3f9-3d05c48a7d46","name":"associated-cases","type":"cases"},{"id":"9687c220-06a6-11ec-b3f9-3d05c48a7d46","name":"associated-cases-comments","type":"cases-comments"}],"score":null,"sort":[1630007310151,9542],"type":"cases-user-actions","updated_at":"2021-08-26T19:48:31.044Z","version":"WzM1MTIsMV0="}
{"attributes":{"closed_at":null,"closed_by":null,"connector":{"fields":[],"name":"none","type":".none"},"created_at":"2021-12-14T13:59:41.342Z","created_by":{"email":"","full_name":"","username":"cnasikas"},"description":"a description","external_service":null,"owner":"securitySolution","settings":{"syncAlerts":true},"status":"open","tags":["some tags"],"title":"A case to export","type":"individual","updated_at":"2021-12-14T13:59:53.303Z","updated_by":{"email":"","full_name":"","username":"cnasikas"}},"coreMigrationVersion":"8.1.0","id":"156cd450-5ce6-11ec-a615-15461784e410","migrationVersion":{"cases":"8.0.0"},"references":[],"type":"cases","updated_at":"2021-12-14T13:59:53.315Z","version":"WzE5MjYsMV0="}
{"attributes":{"action":"create","created_at":"2021-12-14T13:59:41.943Z","created_by":{"email":"","full_name":"","username":"cnasikas"},"owner":"securitySolution","payload":{"connector":{"fields":null,"name":"none","type":".none"},"description":"a description","owner":"securitySolution","settings":{"syncAlerts":true},"status":"open","tags":["some tags"],"title":"A case to export","type":"individual"},"type":"create_case"},"coreMigrationVersion":"8.1.0","id":"15ca0f80-5ce6-11ec-a615-15461784e410","migrationVersion":{"cases-user-actions":"8.1.0"},"references":[{"id":"156cd450-5ce6-11ec-a615-15461784e410","name":"associated-cases","type":"cases"}],"score":null,"sort":[1639490381943,6125],"type":"cases-user-actions","updated_at":"2021-12-14T13:59:41.944Z","version":"WzE5MjIsMV0="}
{"attributes":{"associationType":"case","comment":"A comment for my case","created_at":"2021-12-14T13:59:53.303Z","created_by":{"email":"","full_name":"","username":"cnasikas"},"owner":"securitySolution","pushed_at":null,"pushed_by":null,"type":"user","updated_at":null,"updated_by":null},"coreMigrationVersion":"8.1.0","id":"1c8e6410-5ce6-11ec-a615-15461784e410","migrationVersion":{"cases-comments":"8.0.0"},"references":[{"id":"156cd450-5ce6-11ec-a615-15461784e410","name":"associated-cases","type":"cases"}],"score":null,"sort":[1639490393303,6122],"type":"cases-comments","updated_at":"2021-12-14T13:59:53.312Z","version":"WzE5MjcsMV0="}
{"attributes":{"action":"create","created_at":"2021-12-14T13:59:54.039Z","created_by":{"email":"","full_name":"","username":"cnasikas"},"owner":"securitySolution","payload":{"comment":{"comment":"A comment for my case","owner":"securitySolution","type":"user"}},"type":"comment"},"coreMigrationVersion":"8.1.0","id":"1cff9c70-5ce6-11ec-a615-15461784e410","migrationVersion":{"cases-user-actions":"8.1.0"},"references":[{"id":"156cd450-5ce6-11ec-a615-15461784e410","name":"associated-cases","type":"cases"},{"id":"1c8e6410-5ce6-11ec-a615-15461784e410","name":"associated-cases-comments","type":"cases-comments"}],"score":null,"sort":[1639490394039,6128],"type":"cases-user-actions","updated_at":"2021-12-14T13:59:54.039Z","version":"WzE5MjgsMV0="}
{"excludedObjects":[],"excludedObjectsCount":0,"exportedCount":4,"missingRefCount":0,"missingReferences":[]}

View file

@ -1,6 +1,6 @@
{"attributes":{"actionTypeId":".jira","config":{"apiUrl":"https://cases-testing.atlassian.net","projectKey":"TPN"},"isMissingSecrets":true,"name":"A jira connector"},"coreMigrationVersion":"8.0.0","id":"1cd34740-06ad-11ec-babc-0b08808e8e01","migrationVersion":{"action":"7.14.0"},"references":[],"type":"action","updated_at":"2021-08-26T20:35:12.447Z","version":"WzM1ODQsMV0="}
{"attributes":{"closed_at":null,"closed_by":null,"connector":{"fields":[],"name":"none","type":".none"},"created_at":"2021-08-26T20:35:42.131Z","created_by":{"email":null,"full_name":null,"username":"elastic"},"description":"super description","external_service":{"connector_name":"A jira connector","external_id":"10125","external_title":"TPN-118","external_url":"https://cases-testing.atlassian.net/browse/TPN-118","pushed_at":"2021-08-26T20:35:44.302Z","pushed_by":{"email":null,"full_name":null,"username":"elastic"}},"owner":"securitySolution","settings":{"syncAlerts":true},"status":"open","tags":["other tags"],"title":"A case with a connector","type":"individual","updated_at":"2021-08-26T20:36:35.536Z","updated_by":{"email":null,"full_name":null,"username":"elastic"}},"coreMigrationVersion":"8.0.0","id":"2e85c3f0-06ad-11ec-babc-0b08808e8e01","migrationVersion":{"cases":"7.15.0"},"references":[{"id":"1cd34740-06ad-11ec-babc-0b08808e8e01","name":"pushConnectorId","type":"action"}],"type":"cases","updated_at":"2021-08-26T20:36:35.537Z","version":"WzM1OTIsMV0="}
{"attributes":{"action":"create","action_at":"2021-08-26T20:35:42.131Z","action_by":{"email":null,"full_name":null,"username":"elastic"},"action_field":["description","status","tags","title","connector","settings","owner"],"new_value":"{\"type\":\"individual\",\"title\":\"A case with a connector\",\"tags\":[\"other tags\"],\"description\":\"super description\",\"connector\":{\"id\":\"1cd34740-06ad-11ec-babc-0b08808e8e01\",\"name\":\"A jira connector\",\"type\":\".jira\",\"fields\":{\"issueType\":\"10002\",\"parent\":null,\"priority\":\"High\"}},\"settings\":{\"syncAlerts\":true},\"owner\":\"securitySolution\"}","old_value":null,"owner":"securitySolution"},"coreMigrationVersion":"8.0.0","id":"2e9db8c0-06ad-11ec-babc-0b08808e8e01","migrationVersion":{"cases-user-actions":"7.14.0"},"references":[{"id":"2e85c3f0-06ad-11ec-babc-0b08808e8e01","name":"associated-cases","type":"cases"}],"score":null,"sort":[1630010142131,4024],"type":"cases-user-actions","updated_at":"2021-08-26T20:35:42.284Z","version":"WzM1ODksMV0="}
{"attributes":{"action":"push-to-service","action_at":"2021-08-26T20:35:44.302Z","action_by":{"email":null,"full_name":null,"username":"elastic"},"action_field":["pushed"],"new_value":"{\"pushed_at\":\"2021-08-26T20:35:44.302Z\",\"pushed_by\":{\"username\":\"elastic\",\"full_name\":null,\"email\":null},\"connector_id\":\"1cd34740-06ad-11ec-babc-0b08808e8e01\",\"connector_name\":\"A jira connector\",\"external_id\":\"10125\",\"external_title\":\"TPN-118\",\"external_url\":\"https://cases-testing.atlassian.net/browse/TPN-118\"}","old_value":null,"owner":"securitySolution"},"coreMigrationVersion":"8.0.0","id":"2fd1cbf0-06ad-11ec-babc-0b08808e8e01","migrationVersion":{"cases-user-actions":"7.14.0"},"references":[{"id":"2e85c3f0-06ad-11ec-babc-0b08808e8e01","name":"associated-cases","type":"cases"}],"score":null,"sort":[1630010144302,4029],"type":"cases-user-actions","updated_at":"2021-08-26T20:35:44.303Z","version":"WzM1OTAsMV0="}
{"attributes":{"action":"update","action_at":"2021-08-26T20:36:35.536Z","action_by":{"email":null,"full_name":null,"username":"elastic"},"action_field":["connector"],"new_value":"{\"id\":\"none\",\"name\":\"none\",\"type\":\".none\",\"fields\":null}","old_value":"{\"id\":\"1cd34740-06ad-11ec-babc-0b08808e8e01\",\"name\":\"A jira connector\",\"type\":\".jira\",\"fields\":{\"issueType\":\"10002\",\"parent\":null,\"priority\":\"High\"}}","owner":"securitySolution"},"coreMigrationVersion":"8.0.0","id":"4ee9b250-06ad-11ec-babc-0b08808e8e01","migrationVersion":{"cases-user-actions":"7.14.0"},"references":[{"id":"2e85c3f0-06ad-11ec-babc-0b08808e8e01","name":"associated-cases","type":"cases"}],"score":null,"sort":[1630010195536,4033],"type":"cases-user-actions","updated_at":"2021-08-26T20:36:36.469Z","version":"WzM1OTMsMV0="}
{"excludedObjects":[],"excludedObjectsCount":0,"exportedCount":5,"missingRefCount":0,"missingReferences":[]}
{"attributes":{"actionTypeId":".jira","config":{"apiUrl":"https://cases-testing.atlassian.net","projectKey":"CASES"},"isMissingSecrets":true,"name":"A jira connector"},"coreMigrationVersion":"8.1.0","id":"51a4cbe0-5cea-11ec-a615-15461784e410","migrationVersion":{"action":"8.0.0"},"references":[],"type":"action","updated_at":"2021-12-14T14:31:31.361Z","version":"WzE5ODQsMV0="}
{"attributes":{"closed_at":null,"closed_by":null,"connector":{"fields":[],"name":"none","type":".none"},"created_at":"2021-12-14T14:32:38.547Z","created_by":{"email":"","full_name":"","username":"cnasikas"},"description":"super description","external_service":{"connector_name":"A jira connector","external_id":"26213","external_title":"CASES-227","external_url":"https://cases-testing.atlassian.net/browse/CASES-227","pushed_at":"2021-12-14T14:32:42.594Z","pushed_by":{"email":"","full_name":"","username":"cnasikas"}},"owner":"securitySolution","settings":{"syncAlerts":true},"status":"open","tags":["other tags"],"title":"A case with a connector","type":"individual","updated_at":"2021-12-14T14:33:32.902Z","updated_by":{"email":"","full_name":"","username":"cnasikas"}},"coreMigrationVersion":"8.1.0","id":"afeefae0-5cea-11ec-a615-15461784e410","migrationVersion":{"cases":"8.0.0"},"references":[{"id":"51a4cbe0-5cea-11ec-a615-15461784e410","name":"pushConnectorId","type":"action"}],"type":"cases","updated_at":"2021-12-14T14:33:32.902Z","version":"WzIwMDQsMV0="}
{"attributes":{"action":"create","created_at":"2021-12-14T14:32:39.517Z","created_by":{"email":"","full_name":"","username":"cnasikas"},"owner":"securitySolution","payload":{"connector":{"fields":{"issueType":"10001","parent":null,"priority":"Highest"},"name":"A jira connector","type":".jira"},"description":"super description","owner":"securitySolution","settings":{"syncAlerts":true},"status":"open","tags":["other tags"],"title":"A case with a connector","type":"individual"},"type":"create_case"},"coreMigrationVersion":"8.1.0","id":"b083c0d0-5cea-11ec-a615-15461784e410","migrationVersion":{"cases-user-actions":"8.1.0"},"references":[{"id":"afeefae0-5cea-11ec-a615-15461784e410","name":"associated-cases","type":"cases"},{"id":"51a4cbe0-5cea-11ec-a615-15461784e410","name":"connectorId","type":"action"}],"score":null,"sort":[1639492359517,6171],"type":"cases-user-actions","updated_at":"2021-12-14T14:32:39.517Z","version":"WzE5OTgsMV0="}
{"attributes":{"action":"push_to_service","created_at":"2021-12-14T14:32:43.552Z","created_by":{"email":"","full_name":"","username":"cnasikas"},"owner":"securitySolution","payload":{"externalService":{"connector_name":"A jira connector","external_id":"26213","external_title":"CASES-227","external_url":"https://cases-testing.atlassian.net/browse/CASES-227","pushed_at":"2021-12-14T14:32:42.594Z","pushed_by":{"email":"","full_name":"","username":"cnasikas"}}},"type":"pushed"},"coreMigrationVersion":"8.1.0","id":"b2eb7200-5cea-11ec-a615-15461784e410","migrationVersion":{"cases-user-actions":"8.1.0"},"references":[{"id":"afeefae0-5cea-11ec-a615-15461784e410","name":"associated-cases","type":"cases"},{"id":"51a4cbe0-5cea-11ec-a615-15461784e410","name":"pushConnectorId","type":"action"}],"score":null,"sort":[1639492363552,6176],"type":"cases-user-actions","updated_at":"2021-12-14T14:32:43.552Z","version":"WzIwMDAsMV0="}
{"attributes":{"action":"update","created_at":"2021-12-14T14:33:33.692Z","created_by":{"email":"","full_name":"","username":"cnasikas"},"owner":"securitySolution","payload":{"connector":{"fields":null,"name":"none","type":".none"}},"type":"connector"},"coreMigrationVersion":"8.1.0","id":"d0ce33c0-5cea-11ec-a615-15461784e410","migrationVersion":{"cases-user-actions":"8.1.0"},"references":[{"id":"afeefae0-5cea-11ec-a615-15461784e410","name":"associated-cases","type":"cases"}],"score":null,"sort":[1639492413692,6173],"type":"cases-user-actions","updated_at":"2021-12-14T14:33:33.692Z","version":"WzIwMDUsMV0="}
{"excludedObjects":[],"excludedObjectsCount":0,"exportedCount":5,"missingRefCount":0,"missingReferences":[]}

View file

@ -442,7 +442,7 @@ export const removeServerGeneratedPropertiesFromSavedObject = <
export const removeServerGeneratedPropertiesFromUserAction = (
attributes: CaseUserActionResponse
) => {
const keysToRemove: Array<keyof CaseUserActionResponse> = ['action_id', 'action_at'];
const keysToRemove: Array<keyof CaseUserActionResponse> = ['action_id', 'created_at'];
return removeServerGeneratedPropertiesFromObject<
CaseUserActionResponse,
typeof keysToRemove[number]
@ -694,6 +694,7 @@ export const createCaseWithConnector = async ({
}): Promise<{
postedCase: CaseResponse;
connector: CreateConnectorResponse;
configuration: CasesConfigureResponse;
}> => {
const connector = await createConnector({
supertest,
@ -705,7 +706,7 @@ export const createCaseWithConnector = async ({
});
actionsRemover.add(auth.space ?? 'default', connector.id, 'action', 'actions');
await createConfiguration(
const configuration = await createConfiguration(
supertest,
{
...getConfigurationRequest({
@ -740,7 +741,7 @@ export const createCaseWithConnector = async ({
auth
);
return { postedCase, connector };
return { postedCase, connector, configuration };
};
export const createCase = async (

View file

@ -92,25 +92,13 @@ export default ({ getService }: FtrProviderContext): void => {
const creationUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[1]);
expect(creationUserAction).to.eql({
action_field: [
'description',
'status',
'tags',
'title',
'connector',
'settings',
'owner',
'comment',
],
action: 'delete',
action_by: defaultUser,
old_value: null,
new_value: null,
new_val_connector_id: null,
old_val_connector_id: null,
case_id: `${postedCase.id}`,
type: 'delete_case',
created_by: defaultUser,
case_id: postedCase.id,
comment_id: null,
sub_case_id: '',
payload: {},
owner: 'securitySolutionFixture',
});
});

View file

@ -33,6 +33,11 @@ import {
CaseUserActionAttributes,
CasePostRequest,
CaseUserActionResponse,
PushedUserAction,
ConnectorUserAction,
CommentUserAction,
CreateCaseUserAction,
CaseStatuses,
} from '../../../../../../plugins/cases/common/api';
// eslint-disable-next-line import/no-default-export
@ -109,12 +114,10 @@ export default ({ getService }: FtrProviderContext): void => {
expect(userActions).to.have.length(2);
expect(userActions[0].action).to.eql('create');
expect(includesAllCreateCaseActionFields(userActions[0].action_field)).to.eql(true);
expect(userActions[1].action).to.eql('create');
expect(userActions[1].action_field).to.eql(['comment']);
expect(userActions[1].old_value).to.eql(null);
expect(JSON.parse(userActions[1].new_value!)).to.eql({
expect(userActions[1].type).to.eql('comment');
expect((userActions[1] as CommentUserAction).payload.comment).to.eql({
comment: 'A comment for my case',
type: 'user',
owner: 'securitySolution',
@ -135,13 +138,13 @@ export default ({ getService }: FtrProviderContext): void => {
.set('kbn-xsrf', 'true')
.expect(200);
actionsRemover.add('default', '1cd34740-06ad-11ec-babc-0b08808e8e01', 'action', 'actions');
actionsRemover.add('default', '51a4cbe0-5cea-11ec-a615-15461784e410', 'action', 'actions');
await expectImportToHaveOneCase(supertestService);
const userActions = await getCaseUserActions({
supertest: supertestService,
caseID: '2e85c3f0-06ad-11ec-babc-0b08808e8e01',
caseID: 'afeefae0-5cea-11ec-a615-15461784e410',
});
expect(userActions).to.have.length(3);
@ -161,32 +164,25 @@ const expectImportToHaveOneCase = async (supertestService: supertest.SuperTest<s
const expectImportToHaveCreateCaseUserAction = (userAction: CaseUserActionResponse) => {
expect(userAction.action).to.eql('create');
expect(includesAllCreateCaseActionFields(userAction.action_field)).to.eql(true);
};
const expectImportToHavePushUserAction = (userAction: CaseUserActionResponse) => {
expect(userAction.action).to.eql('push-to-service');
expect(userAction.action_field).to.eql(['pushed']);
expect(userAction.old_value).to.eql(null);
const pushedUserAction = userAction as PushedUserAction;
expect(userAction.action).to.eql('push_to_service');
expect(userAction.type).to.eql('pushed');
const parsedPushNewValue = JSON.parse(userAction.new_value!);
expect(parsedPushNewValue.connector_name).to.eql('A jira connector');
expect(parsedPushNewValue).to.not.have.property('connector_id');
expect(userAction.new_val_connector_id).to.eql('1cd34740-06ad-11ec-babc-0b08808e8e01');
expect(pushedUserAction.payload.externalService.connector_name).to.eql('A jira connector');
expect(pushedUserAction.payload.externalService.connector_id).to.eql(
'51a4cbe0-5cea-11ec-a615-15461784e410'
);
};
const expectImportToHaveUpdateConnector = (userAction: CaseUserActionResponse) => {
const connectorUserAction = userAction as ConnectorUserAction;
expect(userAction.action).to.eql('update');
expect(userAction.action_field).to.eql(['connector']);
expect(userAction.type).to.eql('connector');
const parsedUpdateNewValue = JSON.parse(userAction.new_value!);
expect(parsedUpdateNewValue).to.not.have.property('id');
// the new val connector id is null because it is the none connector
expect(userAction.new_val_connector_id).to.eql(null);
const parsedUpdateOldValue = JSON.parse(userAction.old_value!);
expect(parsedUpdateOldValue).to.not.have.property('id');
expect(userAction.old_val_connector_id).to.eql('1cd34740-06ad-11ec-babc-0b08808e8e01');
expect(connectorUserAction.payload.connector.id).to.eql('none');
};
const ndjsonToObject = (input: string) => {
@ -227,43 +223,37 @@ const expectCaseCreateUserAction = (
userActions: Array<SavedObject<CaseUserActionAttributes>>,
caseRequest: CasePostRequest
) => {
const userActionForCaseCreate = findUserActionSavedObject(
userActions,
'create',
createCaseActionFields
);
const userActionForCaseCreate = findUserActionSavedObject(userActions, 'create', 'create_case');
expect(userActionForCaseCreate?.attributes.action).to.eql('create');
const createCaseUserAction = userActionForCaseCreate!.attributes as CreateCaseUserAction;
const parsedCaseNewValue = JSON.parse(userActionForCaseCreate?.attributes.new_value as string);
const {
connector: { id: ignoreParsedId, ...restParsedConnector },
...restParsedCreateCase
} = parsedCaseNewValue;
} = createCaseUserAction.payload;
const {
connector: { id: ignoreConnectorId, ...restConnector },
...restCreateCase
} = caseRequest;
expect(restParsedCreateCase).to.eql({ ...restCreateCase, type: CaseType.individual });
expect(restParsedCreateCase).to.eql({
...restCreateCase,
type: CaseType.individual,
status: CaseStatuses.open,
});
expect(restParsedConnector).to.eql(restConnector);
expect(userActionForCaseCreate?.attributes.old_value).to.eql(null);
expect(
includesAllCreateCaseActionFields(userActionForCaseCreate?.attributes.action_field)
).to.eql(true);
};
const expectCreateCommentUserAction = (
userActions: Array<SavedObject<CaseUserActionAttributes>>
) => {
const userActionForComment = findUserActionSavedObject(userActions, 'create', ['comment']);
const userActionForComment = findUserActionSavedObject(userActions, 'create', 'comment');
const createCommentUserAction = userActionForComment!.attributes as CommentUserAction;
expect(userActionForComment?.attributes.action).to.eql('create');
expect(JSON.parse(userActionForComment!.attributes.new_value!)).to.eql(postCommentUserReq);
expect(userActionForComment?.attributes.old_value).to.eql(null);
expect(userActionForComment?.attributes.action_field).to.eql(['comment']);
expect(userActionForComment?.attributes.type).to.eql('comment');
expect(createCommentUserAction.payload.comment).to.eql(postCommentUserReq);
};
const expectExportToHaveAComment = (objects: SavedObject[]) => {
@ -276,22 +266,6 @@ const expectExportToHaveAComment = (objects: SavedObject[]) => {
expect(commentSO.attributes.type).to.eql(postCommentUserReq.type);
};
const createCaseActionFields = [
'description',
'status',
'tags',
'title',
'connector',
'settings',
'owner',
];
const includesAllCreateCaseActionFields = (actionFields?: string[]): boolean => {
return createCaseActionFields.every(
(field) => actionFields != null && actionFields.includes(field)
);
};
const findSavedObjectsByType = <ReturnType>(
savedObjects: SavedObject[],
type: string
@ -302,14 +276,7 @@ const findSavedObjectsByType = <ReturnType>(
const findUserActionSavedObject = (
savedObjects: Array<SavedObject<CaseUserActionAttributes>>,
action: string,
actionFields: string[]
type: string
): SavedObject<CaseUserActionAttributes> | undefined => {
return savedObjects.find(
(so) =>
so.attributes.action === action && hasAllStrings(so.attributes.action_field, actionFields)
);
};
const hasAllStrings = (collection: string[], stringsToFind: string[]): boolean => {
return stringsToFind.every((str) => collection.includes(str));
return savedObjects.find((so) => so.attributes.action === action && so.attributes.type === type);
};

View file

@ -125,14 +125,11 @@ export default ({ getService }: FtrProviderContext): void => {
});
expect(statusUserAction).to.eql({
action_field: ['status'],
type: 'status',
action: 'update',
action_by: defaultUser,
new_value: CaseStatuses.closed,
new_val_connector_id: null,
old_val_connector_id: null,
old_value: CaseStatuses.open,
case_id: `${postedCase.id}`,
created_by: defaultUser,
payload: { status: CaseStatuses.closed },
case_id: postedCase.id,
comment_id: null,
sub_case_id: '',
owner: 'securitySolutionFixture',
@ -165,14 +162,11 @@ export default ({ getService }: FtrProviderContext): void => {
});
expect(statusUserAction).to.eql({
action_field: ['status'],
type: 'status',
action: 'update',
action_by: defaultUser,
new_value: CaseStatuses['in-progress'],
old_value: CaseStatuses.open,
old_val_connector_id: null,
new_val_connector_id: null,
case_id: `${postedCase.id}`,
created_by: defaultUser,
payload: { status: CaseStatuses['in-progress'] },
case_id: postedCase.id,
comment_id: null,
sub_case_id: '',
owner: 'securitySolutionFixture',

View file

@ -5,8 +5,6 @@
* 2.0.
*/
/* eslint-disable @typescript-eslint/naming-convention */
import expect from '@kbn/expect';
import { CASES_URL } from '../../../../../../plugins/cases/common/constants';
@ -14,7 +12,6 @@ import {
ConnectorTypes,
ConnectorJiraTypeFields,
CaseStatuses,
CaseUserActionResponse,
CaseType,
} from '../../../../../../plugins/cases/common/api';
import { getPostCaseRequest, postCaseResp, defaultUser } from '../../../../common/lib/mock';
@ -111,41 +108,24 @@ export default ({ getService }: FtrProviderContext): void => {
const userActions = await getCaseUserActions({ supertest, caseID: postedCase.id });
const creationUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[0]);
const { new_value, ...rest } = creationUserAction as CaseUserActionResponse;
const parsedNewValue = JSON.parse(new_value!);
const { id: connectorId, ...restCaseConnector } = postedCase.connector;
expect(rest).to.eql({
action_field: [
'description',
'status',
'tags',
'title',
'connector',
'settings',
'owner',
],
expect(creationUserAction).to.eql({
action: 'create',
action_by: defaultUser,
old_value: null,
old_val_connector_id: null,
// the connector id will be null here because it the connector is none
new_val_connector_id: null,
case_id: `${postedCase.id}`,
type: 'create_case',
created_by: defaultUser,
case_id: postedCase.id,
comment_id: null,
sub_case_id: '',
owner: 'securitySolutionFixture',
});
expect(parsedNewValue).to.eql({
type: postedCase.type,
description: postedCase.description,
title: postedCase.title,
tags: postedCase.tags,
connector: restCaseConnector,
settings: postedCase.settings,
owner: postedCase.owner,
payload: {
type: postedCase.type,
description: postedCase.description,
title: postedCase.title,
tags: postedCase.tags,
connector: postedCase.connector,
settings: postedCase.settings,
owner: postedCase.owner,
status: CaseStatuses.open,
},
});
});

View file

@ -146,15 +146,18 @@ export default ({ getService }: FtrProviderContext): void => {
const commentUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[1]);
expect(commentUserAction).to.eql({
action_field: ['comment'],
type: 'comment',
action: 'create',
action_by: defaultUser,
new_value: `{"comment":"${postCommentUserReq.comment}","type":"${postCommentUserReq.type}","owner":"securitySolutionFixture"}`,
new_val_connector_id: null,
old_value: null,
old_val_connector_id: null,
case_id: `${postedCase.id}`,
comment_id: `${patchedCase.comments![0].id}`,
created_by: defaultUser,
payload: {
comment: {
comment: postCommentUserReq.comment,
type: postCommentUserReq.type,
owner: 'securitySolutionFixture',
},
},
case_id: postedCase.id,
comment_id: patchedCase.comments![0].id,
sub_case_id: '',
owner: 'securitySolutionFixture',
});

Some files were not shown because too many files have changed in this diff Show more