mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Cases] User actions enhancements (#120342)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
47abd52f03
commit
61dd51ae01
105 changed files with 6463 additions and 4554 deletions
|
@ -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;
|
||||
|
|
|
@ -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>;
|
|
@ -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>>;
|
55
x-pack/plugins/cases/common/api/cases/user_actions/common.ts
Normal file
55
x-pack/plugins/cases/common/api/cases/user_actions/common.ts
Normal 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>;
|
|
@ -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>
|
||||
>;
|
|
@ -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>
|
||||
>;
|
|
@ -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>
|
||||
>;
|
|
@ -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>
|
||||
>;
|
91
x-pack/plugins/cases/common/api/cases/user_actions/index.ts
Normal file
91
x-pack/plugins/cases/common/api/cases/user_actions/index.ts
Normal 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
|
||||
>;
|
33
x-pack/plugins/cases/common/api/cases/user_actions/pushed.ts
Normal file
33
x-pack/plugins/cases/common/api/cases/user_actions/pushed.ts
Normal 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>
|
||||
>;
|
|
@ -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>>;
|
18
x-pack/plugins/cases/common/api/cases/user_actions/status.ts
Normal file
18
x-pack/plugins/cases/common/api/cases/user_actions/status.ts
Normal 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>>;
|
18
x-pack/plugins/cases/common/api/cases/user_actions/tags.ts
Normal file
18
x-pack/plugins/cases/common/api/cases/user_actions/tags.ts
Normal 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>>;
|
18
x-pack/plugins/cases/common/api/cases/user_actions/title.ts
Normal file
18
x-pack/plugins/cases/common/api/cases/user_actions/title.ts
Normal 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>>;
|
|
@ -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([
|
||||
|
|
16
x-pack/plugins/cases/common/types.ts
Normal file
16
x-pack/plugins/cases/common/types.ts
Normal 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;
|
|
@ -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;
|
||||
|
|
110
x-pack/plugins/cases/common/utils/user_actions.test.ts
Normal file
110
x-pack/plugins/cases/common/utils/user_actions.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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';
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 && (
|
||||
|
|
|
@ -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()];
|
||||
|
|
|
@ -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 [
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) },
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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), []);
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
|
||||
|
|
|
@ -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}`,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -75,7 +75,7 @@ async function getAttachmentsAndUserActionsForCases(
|
|||
getAssociatedObjects<CaseUserActionAttributes>({
|
||||
savedObjectsClient,
|
||||
caseIds,
|
||||
sortField: 'action_at',
|
||||
sortField: defaultSortField,
|
||||
type: CASE_USER_ACTION_SAVED_OBJECT,
|
||||
}),
|
||||
]);
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -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 [],
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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';
|
|
@ -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,
|
||||
};
|
|
@ -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', () => {});
|
||||
});
|
||||
});
|
|
@ -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
|
||||
);
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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(),
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -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]();
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 });
|
||||
};
|
File diff suppressed because it is too large
Load diff
|
@ -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;
|
||||
|
|
96
x-pack/plugins/cases/server/services/user_actions/mocks.ts
Normal file
96
x-pack/plugins/cases/server/services/user_actions/mocks.ts
Normal 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 },
|
||||
];
|
File diff suppressed because it is too large
Load diff
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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":[]}
|
||||
|
|
|
@ -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":[]}
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue