;
-
-export interface PipedField {
- actionType: string;
- key: string;
- pipes: string[];
- value: string;
-}
-export interface PrepareFieldsForTransformArgs {
- defaultPipes: string[];
- mappings: ConnectorMappingsAttributes[];
- params: ServiceConnectorCaseParams;
-}
-export interface EntityInformation {
- createdAt: string;
- createdBy: ElasticUser;
- updatedAt: string | null;
- updatedBy: ElasticUser | null;
-}
-export interface TransformerArgs {
- date?: string;
- previousValue?: string;
- user?: string;
- value: string;
-}
-
-export type Transformer = (args: TransformerArgs) => TransformerArgs;
-export interface TransformFieldsArgs {
- currentIncident?: S;
- fields: PipedField[];
- params: P;
-}
-
-export const ServiceConnectorUserParams = rt.type({
- fullName: rt.union([rt.string, rt.null]),
- username: rt.string,
-});
-
-export const ServiceConnectorCommentParamsRt = rt.type({
- commentId: rt.string,
- comment: rt.string,
- createdAt: rt.string,
- createdBy: ServiceConnectorUserParams,
- updatedAt: rt.union([rt.string, rt.null]),
- updatedBy: rt.union([ServiceConnectorUserParams, rt.null]),
-});
-export const ServiceConnectorBasicCaseParamsRt = rt.type({
- comments: rt.union([rt.array(ServiceConnectorCommentParamsRt), rt.null]),
- createdAt: rt.string,
- createdBy: ServiceConnectorUserParams,
- description: rt.union([rt.string, rt.null]),
- externalId: rt.union([rt.string, rt.null]),
- savedObjectId: rt.string,
- title: rt.string,
- updatedAt: rt.union([rt.string, rt.null]),
- updatedBy: rt.union([ServiceConnectorUserParams, rt.null]),
-});
-
-export const ConnectorPartialFieldsRt = rt.partial({
- ...JiraFieldsRT.props,
- ...ResilientFieldsRT.props,
- ...ServiceNowFieldsRT.props,
-});
-
-export const ServiceConnectorCaseParamsRt = rt.intersection([
- ServiceConnectorBasicCaseParamsRt,
- ConnectorPartialFieldsRt,
-]);
-export const ServiceConnectorCaseResponseRt = rt.intersection([
- rt.type({
- title: rt.string,
- id: rt.string,
- pushedDate: rt.string,
- url: rt.string,
- }),
- rt.partial({
- comments: rt.array(
- rt.intersection([
- rt.type({
- commentId: rt.string,
- pushedDate: rt.string,
- }),
- rt.partial({ externalCommentId: rt.string }),
- ])
- ),
- }),
-]);
-export type ServiceConnectorBasicCaseParams = rt.TypeOf;
-export type ServiceConnectorCaseParams = rt.TypeOf;
-export type ServiceConnectorCaseResponse = rt.TypeOf;
-export type ServiceConnectorCommentParams = rt.TypeOf;
-
-export const PostPushRequestRt = rt.type({
- connector_type: rt.string,
- params: ServiceConnectorCaseParamsRt,
-});
-
-export type PostPushRequest = rt.TypeOf;
-
-export interface SimpleComment {
- comment: string;
- commentId: string;
-}
-
-export interface MapIncident {
- incident: ExternalServiceParams;
- comments: SimpleComment[];
-}
+export type GetFieldsResponse = rt.TypeOf;
diff --git a/x-pack/plugins/case/common/api/connectors/servicenow.ts b/x-pack/plugins/case/common/api/connectors/servicenow_itsm.ts
similarity index 76%
rename from x-pack/plugins/case/common/api/connectors/servicenow.ts
rename to x-pack/plugins/case/common/api/connectors/servicenow_itsm.ts
index fc4e8f9aa09a3..2e86a26971aaa 100644
--- a/x-pack/plugins/case/common/api/connectors/servicenow.ts
+++ b/x-pack/plugins/case/common/api/connectors/servicenow_itsm.ts
@@ -7,10 +7,10 @@
import * as rt from 'io-ts';
-export const ServiceNowFieldsRT = rt.type({
+export const ServiceNowITSMFieldsRT = rt.type({
impact: rt.union([rt.string, rt.null]),
severity: rt.union([rt.string, rt.null]),
urgency: rt.union([rt.string, rt.null]),
});
-export type ServiceNowFieldsType = rt.TypeOf;
+export type ServiceNowITSMFieldsType = rt.TypeOf;
diff --git a/x-pack/plugins/case/common/api/connectors/servicenow_sir.ts b/x-pack/plugins/case/common/api/connectors/servicenow_sir.ts
new file mode 100644
index 0000000000000..749abdea87437
--- /dev/null
+++ b/x-pack/plugins/case/common/api/connectors/servicenow_sir.ts
@@ -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';
+
+export const ServiceNowSIRFieldsRT = rt.type({
+ category: rt.union([rt.string, rt.null]),
+ destIp: rt.union([rt.boolean, rt.null]),
+ malwareHash: rt.union([rt.boolean, rt.null]),
+ malwareUrl: rt.union([rt.boolean, rt.null]),
+ priority: rt.union([rt.string, rt.null]),
+ sourceIp: rt.union([rt.boolean, rt.null]),
+ subcategory: rt.union([rt.string, rt.null]),
+});
+
+export type ServiceNowSIRFieldsType = rt.TypeOf;
diff --git a/x-pack/plugins/case/common/api/helpers.ts b/x-pack/plugins/case/common/api/helpers.ts
index f9de74f45de46..24c4756a1596b 100644
--- a/x-pack/plugins/case/common/api/helpers.ts
+++ b/x-pack/plugins/case/common/api/helpers.ts
@@ -10,7 +10,7 @@ import {
CASE_COMMENTS_URL,
CASE_USER_ACTIONS_URL,
CASE_COMMENT_DETAILS_URL,
- CASE_CONFIGURE_PUSH_URL,
+ CASE_PUSH_URL,
} from '../constants';
export const getCaseDetailsUrl = (id: string): string => {
@@ -28,6 +28,6 @@ export const getCaseCommentDetailsUrl = (caseId: string, commentId: string): str
export const getCaseUserActionUrl = (id: string): string => {
return CASE_USER_ACTIONS_URL.replace('{case_id}', id);
};
-export const getCaseConfigurePushUrl = (id: string): string => {
- return CASE_CONFIGURE_PUSH_URL.replace('{connector_id}', id);
+export const getCasePushUrl = (caseId: string, connectorId: string): string => {
+ return CASE_PUSH_URL.replace('{case_id}', caseId).replace('{connector_id}', connectorId);
};
diff --git a/x-pack/plugins/case/common/constants.ts b/x-pack/plugins/case/common/constants.ts
index 231ff9ef2dc4d..92dd2312f1ecf 100644
--- a/x-pack/plugins/case/common/constants.ts
+++ b/x-pack/plugins/case/common/constants.ts
@@ -15,10 +15,9 @@ export const CASES_URL = '/api/cases';
export const CASE_DETAILS_URL = `${CASES_URL}/{case_id}`;
export const CASE_CONFIGURE_URL = `${CASES_URL}/configure`;
export const CASE_CONFIGURE_CONNECTORS_URL = `${CASE_CONFIGURE_URL}/connectors`;
-export const CASE_CONFIGURE_CONNECTOR_DETAILS_URL = `${CASE_CONFIGURE_CONNECTORS_URL}/{connector_id}`;
-export const CASE_CONFIGURE_PUSH_URL = `${CASE_CONFIGURE_CONNECTOR_DETAILS_URL}/push`;
export const CASE_COMMENTS_URL = `${CASE_DETAILS_URL}/comments`;
export const CASE_COMMENT_DETAILS_URL = `${CASE_DETAILS_URL}/comments/{comment_id}`;
+export const CASE_PUSH_URL = `${CASE_DETAILS_URL}/connector/{connector_id}/_push`;
export const CASE_REPORTERS_URL = `${CASES_URL}/reporters`;
export const CASE_STATUS_URL = `${CASES_URL}/status`;
export const CASE_TAGS_URL = `${CASES_URL}/tags`;
@@ -30,12 +29,14 @@ export const CASE_USER_ACTIONS_URL = `${CASE_DETAILS_URL}/user_actions`;
export const ACTION_URL = '/api/actions';
export const ACTION_TYPES_URL = '/api/actions/list_action_types';
-export const SERVICENOW_ACTION_TYPE_ID = '.servicenow';
+export const SERVICENOW_ITSM_ACTION_TYPE_ID = '.servicenow';
+export const SERVICENOW_SIR_ACTION_TYPE_ID = '.servicenow-sir';
export const JIRA_ACTION_TYPE_ID = '.jira';
export const RESILIENT_ACTION_TYPE_ID = '.resilient';
export const SUPPORTED_CONNECTORS = [
- SERVICENOW_ACTION_TYPE_ID,
+ SERVICENOW_ITSM_ACTION_TYPE_ID,
+ SERVICENOW_SIR_ACTION_TYPE_ID,
JIRA_ACTION_TYPE_ID,
RESILIENT_ACTION_TYPE_ID,
];
diff --git a/x-pack/plugins/case/server/client/alerts/get.ts b/x-pack/plugins/case/server/client/alerts/get.ts
new file mode 100644
index 0000000000000..718dd327aa08c
--- /dev/null
+++ b/x-pack/plugins/case/server/client/alerts/get.ts
@@ -0,0 +1,31 @@
+/*
+ * 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 Boom from '@hapi/boom';
+import { CaseClientGetAlerts, CaseClientFactoryArguments } from '../types';
+import { CaseClientGetAlertsResponse } from './types';
+
+export const get = ({ alertsService, request, context }: CaseClientFactoryArguments) => async ({
+ ids,
+}: CaseClientGetAlerts): Promise => {
+ const securitySolutionClient = context?.securitySolution?.getAppClient();
+ if (securitySolutionClient == null) {
+ throw Boom.notFound('securitySolutionClient client have not been found');
+ }
+
+ if (ids.length === 0) {
+ return [];
+ }
+
+ const index = securitySolutionClient.getSignalsIndex();
+ const alerts = await alertsService.getAlerts({ ids, index, request });
+ return alerts.hits.hits.map((alert) => ({
+ id: alert._id,
+ index: alert._index,
+ ...alert._source,
+ }));
+};
diff --git a/x-pack/plugins/maps_file_upload/server/index.js b/x-pack/plugins/case/server/client/alerts/types.ts
similarity index 59%
rename from x-pack/plugins/maps_file_upload/server/index.js
rename to x-pack/plugins/case/server/client/alerts/types.ts
index 4bf4e931c7eaa..7b9d4a8856f48 100644
--- a/x-pack/plugins/maps_file_upload/server/index.js
+++ b/x-pack/plugins/case/server/client/alerts/types.ts
@@ -5,8 +5,15 @@
* 2.0.
*/
-import { FileUploadPlugin } from './plugin';
+interface Alert {
+ id: string;
+ index: string;
+ destination?: {
+ ip: string;
+ };
+ source?: {
+ ip: string;
+ };
+}
-export * from './plugin';
-
-export const plugin = () => new FileUploadPlugin();
+export type CaseClientGetAlertsResponse = Alert[];
diff --git a/x-pack/plugins/case/server/client/cases/get.ts b/x-pack/plugins/case/server/client/cases/get.ts
new file mode 100644
index 0000000000000..c1901ccaae511
--- /dev/null
+++ b/x-pack/plugins/case/server/client/cases/get.ts
@@ -0,0 +1,45 @@
+/*
+ * 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 { flattenCaseSavedObject } from '../../routes/api/utils';
+import { CaseResponseRt, CaseResponse } from '../../../common/api';
+import { CaseClientGet, CaseClientFactoryArguments } from '../types';
+
+export const get = ({ savedObjectsClient, caseService }: CaseClientFactoryArguments) => async ({
+ id,
+ includeComments = false,
+}: CaseClientGet): Promise => {
+ const theCase = await caseService.getCase({
+ client: savedObjectsClient,
+ caseId: id,
+ });
+
+ if (!includeComments) {
+ return CaseResponseRt.encode(
+ flattenCaseSavedObject({
+ savedObject: theCase,
+ })
+ );
+ }
+
+ const theComments = await caseService.getAllCaseComments({
+ client: savedObjectsClient,
+ caseId: id,
+ options: {
+ sortField: 'created_at',
+ sortOrder: 'asc',
+ },
+ });
+
+ return CaseResponseRt.encode(
+ flattenCaseSavedObject({
+ savedObject: theCase,
+ comments: theComments.saved_objects,
+ totalComment: theComments.total,
+ })
+ );
+};
diff --git a/x-pack/plugins/case/server/client/cases/mock.ts b/x-pack/plugins/case/server/client/cases/mock.ts
new file mode 100644
index 0000000000000..57e2d4373a52b
--- /dev/null
+++ b/x-pack/plugins/case/server/client/cases/mock.ts
@@ -0,0 +1,191 @@
+/*
+ * 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 {
+ CommentResponse,
+ CommentType,
+ ConnectorMappingsAttributes,
+ CaseUserActionsResponse,
+} from '../../../common/api';
+
+import { BasicParams } from './types';
+
+export const updateUser = {
+ updated_at: '2020-03-13T08:34:53.450Z',
+ updated_by: { full_name: 'Another User', username: 'another', email: 'elastic@elastic.co' },
+};
+
+const entity = {
+ createdAt: '2020-03-13T08:34:53.450Z',
+ createdBy: { full_name: 'Elastic User', username: 'elastic', email: 'elastic@elastic.co' },
+ updatedAt: null,
+ updatedBy: null,
+};
+
+export const comment: CommentResponse = {
+ id: 'mock-comment-1',
+ comment: 'Wow, good luck catching that bad meanie!',
+ type: CommentType.user as const,
+ created_at: '2019-11-25T21:55:00.177Z',
+ created_by: {
+ full_name: 'elastic',
+ email: 'testemail@elastic.co',
+ username: 'elastic',
+ },
+ pushed_at: null,
+ pushed_by: null,
+ updated_at: '2019-11-25T21:55:00.177Z',
+ updated_by: {
+ full_name: 'elastic',
+ email: 'testemail@elastic.co',
+ username: 'elastic',
+ },
+ version: 'WzEsMV0=',
+};
+
+export const commentAlert: CommentResponse = {
+ id: 'mock-comment-1',
+ alertId: 'alert-id-1',
+ index: 'alert-index-1',
+ type: CommentType.alert as const,
+ created_at: '2019-11-25T21:55:00.177Z',
+ created_by: {
+ full_name: 'elastic',
+ email: 'testemail@elastic.co',
+ username: 'elastic',
+ },
+ pushed_at: null,
+ pushed_by: null,
+ updated_at: '2019-11-25T21:55:00.177Z',
+ updated_by: {
+ full_name: 'elastic',
+ email: 'testemail@elastic.co',
+ username: 'elastic',
+ },
+ version: 'WzEsMV0=',
+};
+
+export const defaultPipes = ['informationCreated'];
+export const basicParams: BasicParams = {
+ description: 'a description',
+ title: 'a title',
+ ...entity,
+};
+
+export const mappings: ConnectorMappingsAttributes[] = [
+ {
+ source: 'title',
+ target: 'short_description',
+ action_type: 'overwrite',
+ },
+ {
+ source: 'description',
+ target: 'description',
+ action_type: 'append',
+ },
+ {
+ source: 'comments',
+ target: 'comments',
+ action_type: 'append',
+ },
+];
+
+export const userActions: CaseUserActionsResponse = [
+ {
+ action_field: ['description', 'status', 'tags', 'title', 'connector', 'settings'],
+ action: 'create',
+ action_at: '2021-02-03T17:41:03.771Z',
+ action_by: {
+ email: 'elastic@elastic.co',
+ full_name: 'Elastic',
+ username: 'elastic',
+ },
+ new_value:
+ '{"title":"Case SIR","tags":["sir"],"description":"testing sir","connector":{"id":"456","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}}',
+ old_value: null,
+ action_id: 'fd830c60-6646-11eb-a291-51bf6b175a53',
+ case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53',
+ comment_id: null,
+ },
+ {
+ action_field: ['pushed'],
+ action: 'push-to-service',
+ action_at: '2021-02-03T17:41:26.108Z',
+ action_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_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"}',
+ old_value: null,
+ action_id: '0a801750-6647-11eb-a291-51bf6b175a53',
+ case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53',
+ comment_id: null,
+ },
+ {
+ action_field: ['comment'],
+ action: 'create',
+ action_at: '2021-02-03T17:44:21.067Z',
+ action_by: {
+ email: 'elastic@elastic.co',
+ full_name: 'Elastic',
+ username: 'elastic',
+ },
+ new_value: '{"type":"alert","alertId":"alert-id-1","index":".siem-signals-default-000008"}',
+ old_value: null,
+ action_id: '7373eb60-6647-11eb-a291-51bf6b175a53',
+ case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53',
+ comment_id: 'comment-alert-1',
+ },
+ {
+ action_field: ['comment'],
+ action: 'create',
+ action_at: '2021-02-03T17:44:33.078Z',
+ action_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,
+ action_id: '7abc6410-6647-11eb-a291-51bf6b175a53',
+ case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53',
+ comment_id: 'comment-alert-2',
+ },
+ {
+ action_field: ['pushed'],
+ action: 'push-to-service',
+ action_at: '2021-02-03T17:45:29.400Z',
+ action_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_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"}',
+ old_value: null,
+ action_id: '9b91d8f0-6647-11eb-a291-51bf6b175a53',
+ case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53',
+ comment_id: null,
+ },
+ {
+ action_field: ['comment'],
+ action: 'create',
+ action_at: '2021-02-03T17:48:30.616Z',
+ action_by: {
+ email: 'elastic@elastic.co',
+ full_name: 'Elastic',
+ username: 'elastic',
+ },
+ new_value: '{"comment":"a comment!","type":"user"}',
+ old_value: null,
+ action_id: '0818e5e0-6648-11eb-a291-51bf6b175a53',
+ case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53',
+ comment_id: 'comment-user-1',
+ },
+];
diff --git a/x-pack/plugins/case/server/client/cases/push.ts b/x-pack/plugins/case/server/client/cases/push.ts
new file mode 100644
index 0000000000000..f329fb4d00d07
--- /dev/null
+++ b/x-pack/plugins/case/server/client/cases/push.ts
@@ -0,0 +1,266 @@
+/*
+ * 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 Boom, { isBoom, Boom as BoomType } from '@hapi/boom';
+
+import { SavedObjectsBulkUpdateResponse, SavedObjectsUpdateResponse } from 'kibana/server';
+import { flattenCaseSavedObject } from '../../routes/api/utils';
+
+import {
+ ActionConnector,
+ CaseResponseRt,
+ CaseResponse,
+ CaseStatuses,
+ ExternalServiceResponse,
+ ESCaseAttributes,
+ CommentAttributes,
+} from '../../../common/api';
+import { buildCaseUserActionItem } from '../../services/user_actions/helpers';
+
+import { CaseClientPush, CaseClientFactoryArguments } from '../types';
+import { createIncident, getCommentContextFromAttributes, isCommentAlertType } from './utils';
+
+const createError = (e: Error | BoomType, message: string): Error | BoomType => {
+ if (isBoom(e)) {
+ e.message = message;
+ e.output.payload.message = message;
+ return e;
+ }
+
+ return Error(message);
+};
+
+export const push = ({
+ savedObjectsClient,
+ caseService,
+ caseConfigureService,
+ userActionService,
+ request,
+ response,
+}: CaseClientFactoryArguments) => async ({
+ actionsClient,
+ caseClient,
+ caseId,
+ connectorId,
+}: CaseClientPush): Promise => {
+ /* Start of push to external service */
+ let theCase;
+ let connector;
+ let userActions;
+ let alerts;
+ let connectorMappings;
+ let externalServiceIncident;
+
+ try {
+ [theCase, connector, userActions] = await Promise.all([
+ caseClient.get({ id: caseId, includeComments: true }),
+ actionsClient.get({ id: connectorId }),
+ caseClient.getUserActions({ caseId }),
+ ]);
+ } catch (e) {
+ const message = `Error getting case and/or connector and/or user actions: ${e.message}`;
+ throw createError(e, message);
+ }
+
+ // We need to change the logic when we support subcases
+ if (theCase?.status === CaseStatuses.closed) {
+ throw Boom.conflict(
+ `This case ${theCase.title} is closed. You can not pushed if the case is closed.`
+ );
+ }
+
+ try {
+ alerts = await caseClient.getAlerts({
+ ids: theCase?.comments?.filter(isCommentAlertType).map((comment) => comment.alertId) ?? [],
+ });
+ } catch (e) {
+ throw new Error(`Error getting alerts for case with id ${theCase.id}: ${e.message}`);
+ }
+
+ try {
+ connectorMappings = await caseClient.getMappings({
+ actionsClient,
+ caseClient,
+ connectorId: connector.id,
+ connectorType: connector.actionTypeId,
+ });
+ } catch (e) {
+ const message = `Error getting mapping for connector with id ${connector.id}: ${e.message}`;
+ throw createError(e, message);
+ }
+
+ try {
+ externalServiceIncident = await createIncident({
+ actionsClient,
+ theCase,
+ userActions,
+ connector: connector as ActionConnector,
+ mappings: connectorMappings,
+ alerts,
+ });
+ } catch (e) {
+ const message = `Error creating incident for case with id ${theCase.id}: ${e.message}`;
+ throw createError(e, message);
+ }
+
+ const pushRes = await actionsClient.execute({
+ actionId: connector?.id ?? '',
+ params: {
+ subAction: 'pushToService',
+ subActionParams: externalServiceIncident,
+ },
+ });
+
+ if (pushRes.status === 'error') {
+ throw Boom.failedDependency(
+ pushRes.serviceMessage ?? pushRes.message ?? 'Error pushing to service'
+ );
+ }
+
+ /* End of push to external service */
+
+ /* Start of update case with push information */
+ let user;
+ let myCase;
+ let myCaseConfigure;
+ let comments;
+
+ try {
+ [user, myCase, myCaseConfigure, comments] = await Promise.all([
+ caseService.getUser({ request, response }),
+ caseService.getCase({
+ client: savedObjectsClient,
+ caseId,
+ }),
+ caseConfigureService.find({ client: savedObjectsClient }),
+ caseService.getAllCaseComments({
+ client: savedObjectsClient,
+ caseId,
+ options: {
+ fields: [],
+ page: 1,
+ perPage: theCase?.totalComment ?? 0,
+ },
+ }),
+ ]);
+ } catch (e) {
+ const message = `Error getting user and/or case and/or case configuration and/or case comments: ${e.message}`;
+ throw createError(e, message);
+ }
+
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ const { username, full_name, email } = user;
+ const pushedDate = new Date().toISOString();
+ const externalServiceResponse = pushRes.data as ExternalServiceResponse;
+
+ const externalService = {
+ pushed_at: pushedDate,
+ pushed_by: { username, full_name, email },
+ connector_id: connector.id,
+ connector_name: connector.name,
+ external_id: externalServiceResponse.id,
+ external_title: externalServiceResponse.title,
+ external_url: externalServiceResponse.url,
+ };
+
+ let updatedCase: SavedObjectsUpdateResponse;
+ let updatedComments: SavedObjectsBulkUpdateResponse;
+
+ try {
+ [updatedCase, updatedComments] = await Promise.all([
+ caseService.patchCase({
+ client: savedObjectsClient,
+ caseId,
+ updatedAttributes: {
+ ...(myCaseConfigure.total > 0 &&
+ myCaseConfigure.saved_objects[0].attributes.closure_type === 'close-by-pushing'
+ ? {
+ status: CaseStatuses.closed,
+ closed_at: pushedDate,
+ closed_by: { email, full_name, username },
+ }
+ : {}),
+ external_service: externalService,
+ updated_at: pushedDate,
+ updated_by: { username, full_name, email },
+ },
+ version: myCase.version,
+ }),
+
+ caseService.patchComments({
+ client: savedObjectsClient,
+ comments: comments.saved_objects
+ .filter((comment) => comment.attributes.pushed_at == null)
+ .map((comment) => ({
+ commentId: comment.id,
+ updatedAttributes: {
+ pushed_at: pushedDate,
+ pushed_by: { username, full_name, email },
+ },
+ version: comment.version,
+ })),
+ }),
+
+ userActionService.postUserActions({
+ client: savedObjectsClient,
+ actions: [
+ ...(myCaseConfigure.total > 0 &&
+ myCaseConfigure.saved_objects[0].attributes.closure_type === 'close-by-pushing'
+ ? [
+ buildCaseUserActionItem({
+ action: 'update',
+ actionAt: pushedDate,
+ actionBy: { username, full_name, email },
+ caseId,
+ fields: ['status'],
+ newValue: CaseStatuses.closed,
+ oldValue: myCase.attributes.status,
+ }),
+ ]
+ : []),
+ buildCaseUserActionItem({
+ action: 'push-to-service',
+ actionAt: pushedDate,
+ actionBy: { username, full_name, email },
+ caseId,
+ fields: ['pushed'],
+ newValue: JSON.stringify(externalService),
+ }),
+ ],
+ }),
+ ]);
+ } catch (e) {
+ const message = `Error updating case and/or comments and/or creating user action: ${e.message}`;
+ throw createError(e, message);
+ }
+ /* End of update case with push information */
+
+ return CaseResponseRt.encode(
+ flattenCaseSavedObject({
+ savedObject: {
+ ...myCase,
+ ...updatedCase,
+ attributes: { ...myCase.attributes, ...updatedCase?.attributes },
+ references: myCase.references,
+ },
+ comments: comments.saved_objects.map((origComment) => {
+ const updatedComment = updatedComments.saved_objects.find((c) => c.id === origComment.id);
+ return {
+ ...origComment,
+ ...updatedComment,
+ attributes: {
+ ...origComment.attributes,
+ ...updatedComment?.attributes,
+ ...getCommentContextFromAttributes(origComment.attributes),
+ },
+ version: updatedComment?.version ?? origComment.version,
+ references: origComment?.references ?? [],
+ };
+ }),
+ })
+ );
+};
diff --git a/x-pack/plugins/case/server/client/cases/types.ts b/x-pack/plugins/case/server/client/cases/types.ts
new file mode 100644
index 0000000000000..f1d56e7132bd1
--- /dev/null
+++ b/x-pack/plugins/case/server/client/cases/types.ts
@@ -0,0 +1,81 @@
+/*
+ * 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 @kbn/eslint/no-restricted-paths */
+import {
+ PushToServiceApiParams as JiraPushToServiceApiParams,
+ Incident as JiraIncident,
+} from '../../../../actions/server/builtin_action_types/jira/types';
+import {
+ PushToServiceApiParams as ResilientPushToServiceApiParams,
+ Incident as ResilientIncident,
+} from '../../../../actions/server/builtin_action_types/resilient/types';
+import {
+ PushToServiceApiParamsITSM as ServiceNowITSMPushToServiceApiParams,
+ PushToServiceApiParamsSIR as ServiceNowSIRPushToServiceApiParams,
+ ServiceNowITSMIncident,
+} from '../../../../actions/server/builtin_action_types/servicenow/types';
+import { CaseResponse, ConnectorMappingsAttributes } from '../../../common/api';
+
+export type Incident = JiraIncident | ResilientIncident | ServiceNowITSMIncident;
+export type PushToServiceApiParams =
+ | JiraPushToServiceApiParams
+ | ResilientPushToServiceApiParams
+ | ServiceNowITSMPushToServiceApiParams
+ | ServiceNowSIRPushToServiceApiParams;
+
+export type ExternalServiceParams = Record;
+
+export interface BasicParams {
+ title: CaseResponse['title'];
+ description: CaseResponse['description'];
+ createdAt: CaseResponse['created_at'];
+ createdBy: CaseResponse['created_by'];
+ updatedAt: CaseResponse['updated_at'];
+ updatedBy: CaseResponse['updated_by'];
+}
+
+export interface PipedField {
+ actionType: string;
+ key: string;
+ pipes: string[];
+ value: string;
+}
+export interface PrepareFieldsForTransformArgs {
+ defaultPipes: string[];
+ mappings: ConnectorMappingsAttributes[];
+ params: { title: string; description: string };
+}
+export interface EntityInformation {
+ createdAt: CaseResponse['created_at'];
+ createdBy: CaseResponse['created_by'];
+ updatedAt: CaseResponse['updated_at'];
+ updatedBy: CaseResponse['updated_by'];
+}
+export interface TransformerArgs {
+ date?: string;
+ previousValue?: string;
+ user?: string;
+ value: string;
+}
+
+export type Transformer = (args: TransformerArgs) => TransformerArgs;
+export interface TransformFieldsArgs {
+ currentIncident?: S;
+ fields: PipedField[];
+ params: P;
+}
+
+export interface ExternalServiceComment {
+ comment: string;
+ commentId: string;
+}
+
+export interface MapIncident {
+ incident: ExternalServiceParams;
+ comments: ExternalServiceComment[];
+}
diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/utils.test.ts b/x-pack/plugins/case/server/client/cases/utils.test.ts
similarity index 52%
rename from x-pack/plugins/case/server/routes/api/cases/configure/utils.test.ts
rename to x-pack/plugins/case/server/client/cases/utils.test.ts
index 5114703c60963..dca2c34602678 100644
--- a/x-pack/plugins/case/server/routes/api/cases/configure/utils.test.ts
+++ b/x-pack/plugins/case/server/client/cases/utils.test.ts
@@ -5,34 +5,45 @@
* 2.0.
*/
+import { actionsClientMock } from '../../../../actions/server/actions_client.mock';
+import { flattenCaseSavedObject } from '../../routes/api/utils';
+import { mockCases } from '../../routes/api/__fixtures__';
+
+import { BasicParams, ExternalServiceParams, Incident } from './types';
+import {
+ comment as commentObj,
+ mappings,
+ defaultPipes,
+ basicParams,
+ userActions,
+ commentAlert,
+} from './mock';
+
import {
- mapIncident,
+ createIncident,
+ getLatestPushInfo,
prepareFieldsForTransformation,
- serviceFormatter,
transformComments,
transformers,
transformFields,
} from './utils';
-import { comment as commentObj, mappings, defaultPipes, params, updateUser } from './mock';
-import {
- ConnectorTypes,
- ExternalServiceParams,
- Incident,
- ServiceConnectorCaseParams,
-} from '../../../../../common/api/connectors';
-import { actionsClientMock } from '../../../../../../actions/server/actions_client.mock';
-import { mappings as mappingsMock } from '../../../../client/configure/mock';
-const formatComment = { commentId: commentObj.commentId, comment: commentObj.comment };
-const serviceNowParams = params[ConnectorTypes.servicenow] as ServiceConnectorCaseParams;
-describe('api/cases/configure/utils', () => {
+const formatComment = {
+ commentId: commentObj.id,
+ comment: 'Wow, good luck catching that bad meanie!',
+};
+
+const params = { ...basicParams };
+
+describe('utils', () => {
describe('prepareFieldsForTransformation', () => {
test('prepare fields with defaults', () => {
const res = prepareFieldsForTransformation({
defaultPipes,
- params: serviceNowParams,
+ params,
mappings,
});
+
expect(res).toEqual([
{
actionType: 'overwrite',
@@ -53,8 +64,9 @@ describe('api/cases/configure/utils', () => {
const res = prepareFieldsForTransformation({
defaultPipes: ['myTestPipe'],
mappings,
- params: serviceNowParams,
+ params,
});
+
expect(res).toEqual([
{
actionType: 'overwrite',
@@ -71,16 +83,17 @@ describe('api/cases/configure/utils', () => {
]);
});
});
+
describe('transformFields', () => {
test('transform fields for creation correctly', () => {
const fields = prepareFieldsForTransformation({
defaultPipes,
mappings,
- params: serviceNowParams,
+ params,
});
- const res = transformFields({
- params: serviceNowParams,
+ const res = transformFields({
+ params,
fields,
});
@@ -92,18 +105,19 @@ describe('api/cases/configure/utils', () => {
test('transform fields for update correctly', () => {
const fields = prepareFieldsForTransformation({
- params: serviceNowParams,
+ params,
mappings,
defaultPipes: ['informationUpdated'],
});
- const res = transformFields({
+ const res = transformFields({
params: {
- ...serviceNowParams,
+ ...params,
updatedAt: '2020-03-15T08:34:53.450Z',
updatedBy: {
username: 'anotherUser',
- fullName: 'Another User',
+ full_name: 'Another User',
+ email: 'elastic@elastic.co',
},
},
fields,
@@ -112,6 +126,7 @@ describe('api/cases/configure/utils', () => {
description: 'first description (created at 2020-03-13T08:34:53.450Z by Elastic User)',
},
});
+
expect(res).toEqual({
short_description: 'a title (updated at 2020-03-15T08:34:53.450Z by Another User)',
description:
@@ -121,13 +136,13 @@ describe('api/cases/configure/utils', () => {
test('add newline character to description', () => {
const fields = prepareFieldsForTransformation({
- params: serviceNowParams,
+ params,
mappings,
defaultPipes: ['informationUpdated'],
});
- const res = transformFields({
- params: serviceNowParams,
+ const res = transformFields({
+ params,
fields,
currentIncident: {
short_description: 'first title',
@@ -141,13 +156,13 @@ describe('api/cases/configure/utils', () => {
const fields = prepareFieldsForTransformation({
defaultPipes,
mappings,
- params: serviceNowParams,
+ params,
});
- const res = transformFields({
+ const res = transformFields({
params: {
- ...serviceNowParams,
- createdBy: { fullName: '', username: 'elastic' },
+ ...params,
+ createdBy: { full_name: '', username: 'elastic', email: 'elastic@elastic.co' },
},
fields,
});
@@ -162,14 +177,14 @@ describe('api/cases/configure/utils', () => {
const fields = prepareFieldsForTransformation({
defaultPipes: ['informationUpdated'],
mappings,
- params: serviceNowParams,
+ params,
});
- const res = transformFields({
+ const res = transformFields({
params: {
- ...serviceNowParams,
+ ...params,
updatedAt: '2020-03-15T08:34:53.450Z',
- updatedBy: { username: 'anotherUser', fullName: '' },
+ updatedBy: { username: 'anotherUser', full_name: '', email: 'elastic@elastic.co' },
},
fields,
});
@@ -180,6 +195,7 @@ describe('api/cases/configure/utils', () => {
});
});
});
+
describe('transformComments', () => {
test('transform creation comments', () => {
const comments = [commentObj];
@@ -187,7 +203,7 @@ describe('api/cases/configure/utils', () => {
expect(res).toEqual([
{
...formatComment,
- comment: `${formatComment.comment} (created at ${comments[0].createdAt} by ${comments[0].createdBy.fullName})`,
+ comment: `${formatComment.comment} (created at ${comments[0].created_at} by ${comments[0].created_by.full_name})`,
},
]);
});
@@ -196,14 +212,19 @@ describe('api/cases/configure/utils', () => {
const comments = [
{
...commentObj,
- ...updateUser,
+ updated_at: '2020-03-13T08:34:53.450Z',
+ updated_by: {
+ full_name: 'Another User',
+ username: 'another',
+ email: 'elastic@elastic.co',
+ },
},
];
const res = transformComments(comments, ['informationUpdated']);
expect(res).toEqual([
{
...formatComment,
- comment: `${formatComment.comment} (updated at ${updateUser.updatedAt} by ${updateUser.updatedBy.fullName})`,
+ comment: `${formatComment.comment} (updated at ${comments[0].updated_at} by ${comments[0].updated_by.full_name})`,
},
]);
});
@@ -214,19 +235,19 @@ describe('api/cases/configure/utils', () => {
expect(res).toEqual([
{
...formatComment,
- comment: `${formatComment.comment} (added at ${comments[0].createdAt} by ${comments[0].createdBy.fullName})`,
+ comment: `${formatComment.comment} (added at ${comments[0].created_at} by ${comments[0].created_by.full_name})`,
},
]);
});
test('transform comments without fullname', () => {
- const comments = [{ ...commentObj, createdBy: { username: commentObj.createdBy.username } }];
- // @ts-ignore testing no fullName
+ const comments = [{ ...commentObj, createdBy: { username: commentObj.created_by.username } }];
+ // @ts-ignore testing no full_name
const res = transformComments(comments, ['informationAdded']);
expect(res).toEqual([
{
...formatComment,
- comment: `${formatComment.comment} (added at ${comments[0].createdAt} by ${comments[0].createdBy.username})`,
+ comment: `${formatComment.comment} (added at ${comments[0].created_at} by ${comments[0].created_by.username})`,
},
]);
});
@@ -235,15 +256,15 @@ describe('api/cases/configure/utils', () => {
const comments = [
{
...commentObj,
- updatedAt: '2020-04-13T08:34:53.450Z',
- updatedBy: { fullName: 'Elastic2', username: 'elastic' },
+ updated_at: '2020-04-13T08:34:53.450Z',
+ updated_by: { full_name: 'Elastic2', username: 'elastic', email: 'elastic@elastic.co' },
},
];
const res = transformComments(comments, ['informationAdded']);
expect(res).toEqual([
{
...formatComment,
- comment: `${formatComment.comment} (added at ${comments[0].updatedAt} by ${comments[0].updatedBy.fullName})`,
+ comment: `${formatComment.comment} (added at ${comments[0].updated_at} by ${comments[0].updated_by.full_name})`,
},
]);
});
@@ -252,19 +273,20 @@ describe('api/cases/configure/utils', () => {
const comments = [
{
...commentObj,
- updatedAt: '2020-04-13T08:34:53.450Z',
- updatedBy: { fullName: '', username: 'elastic2' },
+ updated_at: '2020-04-13T08:34:53.450Z',
+ updated_by: { full_name: '', username: 'elastic2', email: 'elastic@elastic.co' },
},
];
const res = transformComments(comments, ['informationAdded']);
expect(res).toEqual([
{
...formatComment,
- comment: `${formatComment.comment} (added at ${comments[0].updatedAt} by ${comments[0].updatedBy.username})`,
+ comment: `${formatComment.comment} (added at ${comments[0].updated_at} by ${comments[0].updated_by.username})`,
},
]);
});
});
+
describe('transformers', () => {
const { informationCreated, informationUpdated, informationAdded, append } = transformers;
describe('informationCreated', () => {
@@ -389,142 +411,291 @@ describe('api/cases/configure/utils', () => {
});
});
});
- describe('mapIncident', () => {
+
+ describe('createIncident', () => {
let actionsMock = actionsClientMock.create();
- it('maps an external incident', async () => {
- const res = await mapIncident(
- actionsMock,
- '123',
- ConnectorTypes.servicenow,
- mappingsMock[ConnectorTypes.servicenow],
- serviceNowParams
- );
+ const theCase = {
+ ...flattenCaseSavedObject({
+ savedObject: mockCases[0],
+ }),
+ comments: [commentObj],
+ totalComments: 1,
+ };
+
+ const connector = {
+ id: '456',
+ actionTypeId: '.jira',
+ name: 'Connector without isCaseOwned',
+ config: {
+ apiUrl: 'https://elastic.jira.com',
+ },
+ isPreconfigured: false,
+ };
+
+ it('creates an external incident', async () => {
+ const res = await createIncident({
+ actionsClient: actionsMock,
+ theCase,
+ userActions: [],
+ connector,
+ mappings,
+ alerts: [],
+ });
+
expect(res).toEqual({
incident: {
- description: 'a description (created at 2020-03-13T08:34:53.450Z by Elastic User)',
+ priority: null,
+ labels: ['defacement'],
+ issueType: null,
+ parent: null,
+ short_description:
+ 'Super Bad Security Issue (created at 2019-11-25T21:54:48.952Z by elastic)',
+ description:
+ 'This is a brand new case of a bad meanie defacing data (created at 2019-11-25T21:54:48.952Z by elastic)',
externalId: null,
- impact: '3',
- severity: '1',
- short_description: 'a title (created at 2020-03-13T08:34:53.450Z by Elastic User)',
- urgency: '2',
},
- comments: [
+ comments: [],
+ });
+ });
+
+ it('it creates comments correctly', async () => {
+ const res = await createIncident({
+ actionsClient: actionsMock,
+ theCase: {
+ ...theCase,
+ comments: [{ ...commentObj, id: 'comment-user-1' }],
+ },
+ userActions,
+ connector,
+ mappings,
+ alerts: [],
+ });
+
+ expect(res.comments).toEqual([
+ {
+ comment:
+ 'Wow, good luck catching that bad meanie! (added at 2019-11-25T21:55:00.177Z by elastic)',
+ commentId: 'comment-user-1',
+ },
+ ]);
+ });
+
+ it('it does NOT creates comments when mapping is nothing', async () => {
+ const res = await createIncident({
+ actionsClient: actionsMock,
+ theCase: {
+ ...theCase,
+ comments: [{ ...commentObj, id: 'comment-user-1' }],
+ },
+ userActions,
+ connector,
+ mappings: [
+ mappings[0],
+ mappings[1],
{
- comment: 'first comment (added at 2020-03-13T08:34:53.450Z by Elastic User)',
- commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631',
+ source: 'comments',
+ target: 'comments',
+ action_type: 'nothing',
},
],
+ alerts: [],
});
+
+ expect(res.comments).toEqual([]);
});
- it('throws error if invalid service', async () => {
- await mapIncident(
- actionsMock,
- '123',
- 'invalid',
- mappingsMock[ConnectorTypes.servicenow],
- serviceNowParams
- ).catch((e) => {
- expect(e).not.toBeNull();
- expect(e).toEqual(new Error(`Invalid service`));
+
+ it('it creates comments of type alert correctly', async () => {
+ const res = await createIncident({
+ actionsClient: actionsMock,
+ theCase: {
+ ...theCase,
+ comments: [
+ { ...commentObj, id: 'comment-user-1' },
+ { ...commentAlert, id: 'comment-alert-1' },
+ { ...commentAlert, id: 'comment-alert-2' },
+ ],
+ },
+ // Remove second push
+ userActions: userActions.filter((item, index) => index !== 4),
+ connector,
+ mappings: [
+ ...mappings,
+ {
+ source: 'comments',
+ target: 'comments',
+ action_type: 'nothing',
+ },
+ ],
+ alerts: [],
});
+
+ expect(res.comments).toEqual([
+ {
+ comment:
+ 'Wow, good luck catching that bad meanie! (added at 2019-11-25T21:55:00.177Z by elastic)',
+ commentId: 'comment-user-1',
+ },
+ {
+ comment:
+ 'Alert with id alert-id-1 added to case (added at 2019-11-25T21:55:00.177Z by elastic)',
+ commentId: 'comment-alert-1',
+ },
+ {
+ comment:
+ 'Alert with id alert-id-1 added to case (added at 2019-11-25T21:55:00.177Z by elastic)',
+ commentId: 'comment-alert-2',
+ },
+ ]);
});
+
it('updates an existing incident', async () => {
const existingIncidentData = {
- description: 'fun description',
- impact: '3',
- severity: '3',
+ priority: null,
+ issueType: null,
+ parent: null,
short_description: 'fun title',
- urgency: '3',
+ description: 'fun description',
};
+
const execute = jest.fn().mockReturnValue(existingIncidentData);
actionsMock = { ...actionsMock, execute };
- const res = await mapIncident(
- actionsMock,
- '123',
- ConnectorTypes.servicenow,
- mappingsMock[ConnectorTypes.servicenow],
- { ...serviceNowParams, externalId: '123' }
- );
+
+ const res = await createIncident({
+ actionsClient: actionsMock,
+ theCase,
+ userActions,
+ connector,
+ mappings,
+ alerts: [],
+ });
+
expect(res).toEqual({
incident: {
- description: 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
- externalId: '123',
- impact: '3',
- severity: '1',
- short_description: 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
- urgency: '2',
+ priority: null,
+ labels: ['defacement'],
+ issueType: null,
+ parent: null,
+ description:
+ 'fun description \r\nThis is a brand new case of a bad meanie defacing data (updated at 2019-11-25T21:54:48.952Z by elastic)',
+ externalId: 'external-id',
+ short_description:
+ 'Super Bad Security Issue (updated at 2019-11-25T21:54:48.952Z by elastic)',
},
- comments: [
- {
- comment: 'first comment (added at 2020-03-13T08:34:53.450Z by Elastic User)',
- commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631',
- },
- ],
+ comments: [],
});
});
+
it('throws error when existing incident throws', async () => {
+ expect.assertions(2);
const execute = jest.fn().mockImplementation(() => {
throw new Error('exception');
});
+
actionsMock = { ...actionsMock, execute };
- await mapIncident(
- actionsMock,
- '123',
- ConnectorTypes.servicenow,
- mappingsMock[ConnectorTypes.servicenow],
- { ...serviceNowParams, externalId: '123' }
- ).catch((e) => {
+ createIncident({
+ actionsClient: actionsMock,
+ theCase,
+ userActions,
+ connector,
+ mappings,
+ alerts: [],
+ }).catch((e) => {
expect(e).not.toBeNull();
expect(e).toEqual(
new Error(
- `Retrieving Incident by id 123 from ServiceNow failed with exception: Error: exception`
+ `Retrieving Incident by id external-id from .jira failed with exception: Error: exception`
)
);
});
});
- });
- const connectors = [
- {
- name: ConnectorTypes.jira,
- result: {
- incident: {
- issueType: '10003',
- parent: '5002',
- priority: 'Highest',
- },
- thirdPartyName: 'Jira',
- },
- },
- {
- name: ConnectorTypes.resilient,
- result: {
- incident: {
- incidentTypes: ['10003'],
- severityCode: '1',
- },
- thirdPartyName: 'Resilient',
- },
- },
- {
- name: ConnectorTypes.servicenow,
- result: {
- incident: {
- impact: '3',
- severity: '1',
- urgency: '2',
- },
- thirdPartyName: 'ServiceNow',
- },
- },
- ];
- describe('serviceFormatter', () => {
- connectors.forEach((c) =>
- it(`formats ${c.name}`, () => {
- const caseParams = params[c.name] as ServiceConnectorCaseParams;
- const res = serviceFormatter(c.name, caseParams);
- expect(res).toEqual(c.result);
- })
- );
+ it('throws error if connector is not supported', async () => {
+ expect.assertions(2);
+ createIncident({
+ actionsClient: actionsMock,
+ theCase,
+ userActions,
+ connector: { ...connector, actionTypeId: 'not-supported' },
+ mappings,
+ alerts: [],
+ }).catch((e) => {
+ expect(e).not.toBeNull();
+ expect(e).toEqual(new Error('Invalid external service'));
+ });
+ });
+
+ describe('getLatestPushInfo', () => {
+ it('it returns the latest push information correctly', async () => {
+ const res = getLatestPushInfo('456', userActions);
+ expect(res).toEqual({
+ index: 4,
+ pushedInfo: {
+ 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',
+ pushed_at: '2021-02-03T17:45:29.400Z',
+ pushed_by: {
+ email: 'elastic@elastic.co',
+ full_name: 'Elastic',
+ username: 'elastic',
+ },
+ },
+ });
+ });
+
+ it('it returns null when there are not actions', async () => {
+ const res = getLatestPushInfo('456', []);
+ expect(res).toBe(null);
+ });
+
+ it('it returns null when there are no push user action', async () => {
+ const res = getLatestPushInfo('456', [userActions[0]]);
+ expect(res).toBe(null);
+ });
+
+ it('it returns the correct push information when with multiple push on different connectors', async () => {
+ 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: {
+ 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_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"}',
+ old_value: null,
+ action_id: '9b91d8f0-6647-11eb-a291-51bf6b175a53',
+ case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53',
+ comment_id: null,
+ },
+ ]);
+
+ expect(res).toEqual({
+ index: 1,
+ pushedInfo: {
+ 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',
+ pushed_at: '2021-02-03T17:41:26.108Z',
+ pushed_by: {
+ email: 'elastic@elastic.co',
+ full_name: 'Elastic',
+ username: 'elastic',
+ },
+ },
+ });
+ });
+ });
});
});
diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/utils.ts b/x-pack/plugins/case/server/client/cases/utils.ts
similarity index 50%
rename from x-pack/plugins/case/server/routes/api/cases/configure/utils.ts
rename to x-pack/plugins/case/server/client/cases/utils.ts
index 01a1a580bd78f..6974fd4ffa288 100644
--- a/x-pack/plugins/case/server/routes/api/cases/configure/utils.ts
+++ b/x-pack/plugins/case/server/client/cases/utils.ts
@@ -8,46 +8,118 @@
import { i18n } from '@kbn/i18n';
import { flow } from 'lodash';
import {
- ServiceConnectorCaseParams,
- ServiceConnectorCommentParams,
+ ActionConnector,
+ CaseResponse,
+ CaseFullExternalService,
+ CaseUserActionsResponse,
+ CommentResponse,
+ CommentResponseAlertsType,
+ CommentType,
ConnectorMappingsAttributes,
ConnectorTypes,
+ CommentAttributes,
+ CommentRequestUserType,
+ CommentRequestAlertType,
+} from '../../../common/api';
+import { ActionsClient } from '../../../../actions/server';
+import { externalServiceFormatters, FormatterConnectorTypes } from '../../connectors';
+import { CaseClientGetAlertsResponse } from '../../client/alerts/types';
+import {
+ BasicParams,
EntityInformation,
ExternalServiceParams,
+ ExternalServiceComment,
Incident,
- JiraPushToServiceApiParams,
MapIncident,
PipedField,
PrepareFieldsForTransformArgs,
PushToServiceApiParams,
- ResilientPushToServiceApiParams,
- ServiceNowITSMPushToServiceApiParams,
- SimpleComment,
Transformer,
TransformerArgs,
TransformFieldsArgs,
-} from '../../../../../common/api';
-import { ActionsClient } from '../../../../../../actions/server';
-export const mapIncident = async (
- actionsClient: ActionsClient,
+} from './types';
+
+export const getLatestPushInfo = (
connectorId: string,
- connectorType: string,
- mappings: ConnectorMappingsAttributes[],
- params: ServiceConnectorCaseParams
-): Promise => {
- const { comments: caseComments, externalId } = params;
+ userActions: CaseUserActionsResponse
+): { index: number; pushedInfo: CaseFullExternalService } | null => {
+ for (const [index, action] of [...userActions].reverse().entries()) {
+ if (action.action === 'push-to-service' && action.new_value)
+ try {
+ const pushedInfo = JSON.parse(action.new_value);
+ if (pushedInfo.connector_id === connectorId) {
+ // 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 };
+ }
+ } catch (e) {
+ // Silence JSON parse errors
+ }
+ }
+
+ return null;
+};
+
+const isConnectorSupported = (connectorId: string): connectorId is FormatterConnectorTypes =>
+ Object.values(ConnectorTypes).includes(connectorId as ConnectorTypes);
+
+const getCommentContent = (comment: CommentResponse): string => {
+ if (comment.type === CommentType.user) {
+ return comment.comment;
+ } else if (comment.type === CommentType.alert) {
+ return `Alert with id ${comment.alertId} added to case`;
+ }
+
+ return '';
+};
+
+interface CreateIncidentArgs {
+ actionsClient: ActionsClient;
+ theCase: CaseResponse;
+ userActions: CaseUserActionsResponse;
+ connector: ActionConnector;
+ mappings: ConnectorMappingsAttributes[];
+ alerts: CaseClientGetAlertsResponse;
+}
+
+export const createIncident = async ({
+ actionsClient,
+ theCase,
+ userActions,
+ connector,
+ mappings,
+ alerts,
+}: CreateIncidentArgs): Promise => {
+ const {
+ comments: caseComments,
+ title,
+ description,
+ created_at: createdAt,
+ created_by: createdBy,
+ updated_at: updatedAt,
+ updated_by: updatedBy,
+ } = theCase;
+
+ if (!isConnectorSupported(connector.actionTypeId)) {
+ throw new Error('Invalid external service');
+ }
+
+ const params = { title, description, createdAt, createdBy, updatedAt, updatedBy };
+ const latestPushInfo = getLatestPushInfo(connector.id, userActions);
+ const externalId = latestPushInfo?.pushedInfo?.external_id ?? null;
const defaultPipes = externalId ? ['informationUpdated'] : ['informationCreated'];
let currentIncident: ExternalServiceParams | undefined;
- const service = serviceFormatter(connectorType, params);
- if (service == null) {
- throw new Error(`Invalid service`);
- }
- const thirdPartyName = service.thirdPartyName;
- let incident: Partial = service.incident;
+
+ const externalServiceFields = externalServiceFormatters[connector.actionTypeId].format(
+ theCase,
+ alerts
+ );
+ let incident: Partial = { ...externalServiceFields };
+
if (externalId) {
try {
currentIncident = ((await actionsClient.execute({
- actionId: connectorId,
+ actionId: connector.id,
params: {
subAction: 'getIncident',
subActionParams: { externalId },
@@ -55,80 +127,56 @@ export const mapIncident = async (
})) as unknown) as ExternalServiceParams | undefined;
} catch (ex) {
throw new Error(
- `Retrieving Incident by id ${externalId} from ${thirdPartyName} failed with exception: ${ex}`
+ `Retrieving Incident by id ${externalId} from ${connector.actionTypeId} failed with exception: ${ex}`
);
}
}
+
const fields = prepareFieldsForTransformation({
defaultPipes,
mappings,
params,
});
- const transformedFields = transformFields<
- ServiceConnectorCaseParams,
- ExternalServiceParams,
- Incident
- >({
+
+ const transformedFields = transformFields({
params,
fields,
currentIncident,
});
+
incident = { ...incident, ...transformedFields, externalId };
- let comments: SimpleComment[] = [];
- if (caseComments && Array.isArray(caseComments) && caseComments.length > 0) {
+
+ const commentsIdsToBeUpdated = new Set(
+ userActions
+ .slice(latestPushInfo?.index ?? 0)
+ .filter(
+ (action, index) =>
+ Array.isArray(action.action_field) && action.action_field[0] === 'comment'
+ )
+ .map((action) => action.comment_id)
+ );
+ const commentsToBeUpdated = caseComments?.filter((comment) =>
+ commentsIdsToBeUpdated.has(comment.id)
+ );
+
+ let comments: ExternalServiceComment[] = [];
+ if (commentsToBeUpdated && Array.isArray(commentsToBeUpdated) && commentsToBeUpdated.length > 0) {
const commentsMapping = mappings.find((m) => m.source === 'comments');
if (commentsMapping?.action_type !== 'nothing') {
- comments = transformComments(caseComments, ['informationAdded']);
+ comments = transformComments(commentsToBeUpdated, ['informationAdded']);
}
}
return { incident, comments };
};
-export const serviceFormatter = (
- connectorType: string,
- params: unknown
-): { thirdPartyName: string; incident: Partial } | null => {
- switch (connectorType) {
- case ConnectorTypes.jira:
- const {
- priority,
- labels,
- issueType,
- parent,
- } = params as JiraPushToServiceApiParams['incident'];
- return {
- incident: { priority, labels, issueType, parent },
- thirdPartyName: 'Jira',
- };
- case ConnectorTypes.resilient:
- const { incidentTypes, severityCode } = params as ResilientPushToServiceApiParams['incident'];
- return {
- incident: { incidentTypes, severityCode },
- thirdPartyName: 'Resilient',
- };
- case ConnectorTypes.servicenow:
- const {
- severity,
- urgency,
- impact,
- } = params as ServiceNowITSMPushToServiceApiParams['incident'];
- return {
- incident: { severity, urgency, impact },
- thirdPartyName: 'ServiceNow',
- };
- default:
- return null;
- }
-};
-
export const getEntity = (entity: EntityInformation): string =>
(entity.updatedBy != null
- ? entity.updatedBy.fullName
- ? entity.updatedBy.fullName
+ ? entity.updatedBy.full_name
+ ? entity.updatedBy.full_name
: entity.updatedBy.username
: entity.createdBy != null
- ? entity.createdBy.fullName
- ? entity.createdBy.fullName
+ ? entity.createdBy.full_name
+ ? entity.createdBy.full_name
: entity.createdBy.username
: '') ?? '';
@@ -160,6 +208,7 @@ export const FIELD_INFORMATION = (
});
}
};
+
export const transformers: Record = {
informationCreated: ({ value, date, user, ...rest }: TransformerArgs): TransformerArgs => ({
value: `${value} ${FIELD_INFORMATION('create', date, user)}`,
@@ -178,6 +227,7 @@ export const transformers: Record = {
...rest,
}),
};
+
export const prepareFieldsForTransformation = ({
defaultPipes,
mappings,
@@ -226,14 +276,46 @@ export const transformFields = <
};
export const transformComments = (
- comments: ServiceConnectorCommentParams[],
+ comments: CaseResponse['comments'] = [],
pipes: string[]
-): SimpleComment[] =>
+): ExternalServiceComment[] =>
comments.map((c) => ({
comment: flow(...pipes.map((p) => transformers[p]))({
- value: c.comment,
- date: c.updatedAt ?? c.createdAt,
- user: getEntity(c),
+ value: getCommentContent(c),
+ date: c.updated_at ?? c.created_at,
+ user: getEntity({
+ createdAt: c.created_at,
+ createdBy: c.created_by,
+ updatedAt: c.updated_at,
+ updatedBy: c.updated_by,
+ }),
}).value,
- commentId: c.commentId,
+ commentId: c.id,
}));
+
+export const isCommentAlertType = (
+ comment: CommentResponse
+): comment is CommentResponseAlertsType => comment.type === CommentType.alert;
+
+export const getCommentContextFromAttributes = (
+ attributes: CommentAttributes
+): CommentRequestUserType | CommentRequestAlertType => {
+ switch (attributes.type) {
+ case CommentType.user:
+ return {
+ type: CommentType.user,
+ comment: attributes.comment,
+ };
+ case CommentType.alert:
+ return {
+ type: CommentType.alert,
+ alertId: attributes.alertId,
+ index: attributes.index,
+ };
+ default:
+ return {
+ type: CommentType.user,
+ comment: '',
+ };
+ }
+};
diff --git a/x-pack/plugins/case/server/client/comments/add.ts b/x-pack/plugins/case/server/client/comments/add.ts
index 5cfa4d70290f0..58d7c9abcbfd3 100644
--- a/x-pack/plugins/case/server/client/comments/add.ts
+++ b/x-pack/plugins/case/server/client/comments/add.ts
@@ -87,7 +87,7 @@ export const addComment = ({
// If the case is synced with alerts the newly attached alert must match the status of the case.
if (newComment.attributes.type === CommentType.alert && myCase.attributes.settings.syncAlerts) {
- caseClient.updateAlertsStatus({
+ await caseClient.updateAlertsStatus({
ids: [newComment.attributes.alertId],
status: myCase.attributes.status,
});
diff --git a/x-pack/plugins/case/server/client/configure/mock.ts b/x-pack/plugins/case/server/client/configure/mock.ts
index 46df0a7ac6756..4d0c384e23e27 100644
--- a/x-pack/plugins/case/server/client/configure/mock.ts
+++ b/x-pack/plugins/case/server/client/configure/mock.ts
@@ -70,7 +70,7 @@ export const mappings: TestMappings = {
action_type: 'append',
},
],
- [ConnectorTypes.servicenow]: [
+ [ConnectorTypes.serviceNowITSM]: [
{
source: 'title',
target: 'short_description',
@@ -611,7 +611,7 @@ export const formatFieldsTestData: FormatFieldsTestData[] = [
{ id: 'upon_reject', name: 'Upon reject', required: false, type: 'text' },
],
fields: serviceNowFields,
- type: ConnectorTypes.servicenow,
+ type: ConnectorTypes.serviceNowITSM,
},
];
export const mockGetFieldsResponse = {
diff --git a/x-pack/plugins/case/server/client/configure/utils.ts b/x-pack/plugins/case/server/client/configure/utils.ts
index 2fc9e3d17801c..7e91c2ae5a4d7 100644
--- a/x-pack/plugins/case/server/client/configure/utils.ts
+++ b/x-pack/plugins/case/server/client/configure/utils.ts
@@ -70,7 +70,9 @@ export const formatFields = (theData: unknown, theType: string): ConnectorField[
return normalizeJiraFields(theData as JiraGetFieldsResponse);
case ConnectorTypes.resilient:
return normalizeResilientFields(theData as ResilientGetFieldsResponse);
- case ConnectorTypes.servicenow:
+ case ConnectorTypes.serviceNowITSM:
+ return normalizeServiceNowFields(theData as ServiceNowGetFieldsResponse);
+ case ConnectorTypes.serviceNowSIR:
return normalizeServiceNowFields(theData as ServiceNowGetFieldsResponse);
default:
return [];
@@ -97,10 +99,14 @@ const getPreferredFields = (theType: string) => {
} else if (theType === ConnectorTypes.resilient) {
title = 'name';
description = 'description';
- } else if (theType === ConnectorTypes.servicenow) {
+ } else if (
+ theType === ConnectorTypes.serviceNowITSM ||
+ theType === ConnectorTypes.serviceNowSIR
+ ) {
title = 'short_description';
description = 'description';
}
+
return { title, description };
};
diff --git a/x-pack/plugins/case/server/client/index.test.ts b/x-pack/plugins/case/server/client/index.test.ts
index 095dc5102b720..4daa4d1c0bd8b 100644
--- a/x-pack/plugins/case/server/client/index.test.ts
+++ b/x-pack/plugins/case/server/client/index.test.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { KibanaRequest } from 'kibana/server';
+import { KibanaRequest, kibanaResponseFactory } from '../../../../../src/core/server';
import { savedObjectsClientMock } from '../../../../../src/core/server/mocks';
import { createCaseClient } from '.';
import {
@@ -17,29 +17,48 @@ import {
} from '../services/mocks';
import { create } from './cases/create';
+import { get } from './cases/get';
import { update } from './cases/update';
+import { push } from './cases/push';
import { addComment } from './comments/add';
+import { getFields } from './configure/get_fields';
+import { getMappings } from './configure/get_mappings';
import { updateAlertsStatus } from './alerts/update_status';
+import { get as getUserActions } from './user_actions/get';
+import { get as getAlerts } from './alerts/get';
import type { CasesRequestHandlerContext } from '../types';
jest.mock('./cases/create');
jest.mock('./cases/update');
+jest.mock('./cases/get');
+jest.mock('./cases/push');
jest.mock('./comments/add');
jest.mock('./alerts/update_status');
+jest.mock('./alerts/get');
+jest.mock('./user_actions/get');
+jest.mock('./configure/get_fields');
+jest.mock('./configure/get_mappings');
const caseConfigureService = createConfigureServiceMock();
const alertsService = createAlertServiceMock();
const caseService = createCaseServiceMock();
const connectorMappingsService = connectorMappingsServiceMock();
const request = {} as KibanaRequest;
+const response = kibanaResponseFactory;
const savedObjectsClient = savedObjectsClientMock.create();
const userActionService = createUserActionServiceMock();
const context = {} as CasesRequestHandlerContext;
const createMock = create as jest.Mock;
+const getMock = get as jest.Mock;
const updateMock = update as jest.Mock;
+const pushMock = push as jest.Mock;
const addCommentMock = addComment as jest.Mock;
const updateAlertsStatusMock = updateAlertsStatus as jest.Mock;
+const getAlertsStatusMock = getAlerts as jest.Mock;
+const getFieldsMock = getFields as jest.Mock;
+const getMappingsMock = getMappings as jest.Mock;
+const getUserActionsMock = getUserActions as jest.Mock;
describe('createCaseClient()', () => {
test('it creates the client correctly', async () => {
@@ -50,49 +69,34 @@ describe('createCaseClient()', () => {
connectorMappingsService,
context,
request,
+ response,
savedObjectsClient,
userActionService,
});
- expect(createMock).toHaveBeenCalledWith({
- alertsService,
- caseConfigureService,
- caseService,
- connectorMappingsService,
- request,
- savedObjectsClient,
- userActionService,
- });
-
- expect(updateMock).toHaveBeenCalledWith({
- alertsService,
- caseConfigureService,
- caseService,
- connectorMappingsService,
- request,
- savedObjectsClient,
- userActionService,
- });
-
- expect(addCommentMock).toHaveBeenCalledWith({
- alertsService,
- caseConfigureService,
- caseService,
- connectorMappingsService,
- request,
- savedObjectsClient,
- userActionService,
- });
-
- expect(updateAlertsStatusMock).toHaveBeenCalledWith({
- alertsService,
- caseConfigureService,
- caseService,
- connectorMappingsService,
- context,
- request,
- savedObjectsClient,
- userActionService,
- });
+ [
+ createMock,
+ getMock,
+ updateMock,
+ pushMock,
+ addCommentMock,
+ updateAlertsStatusMock,
+ getAlertsStatusMock,
+ getFieldsMock,
+ getMappingsMock,
+ getUserActionsMock,
+ ].forEach((method) =>
+ expect(method).toHaveBeenCalledWith({
+ caseConfigureService,
+ caseService,
+ connectorMappingsService,
+ request,
+ response,
+ savedObjectsClient,
+ userActionService,
+ alertsService,
+ context,
+ })
+ );
});
});
diff --git a/x-pack/plugins/case/server/client/index.ts b/x-pack/plugins/case/server/client/index.ts
index 1b9d3ce7ecb08..e15b9fc766562 100644
--- a/x-pack/plugins/case/server/client/index.ts
+++ b/x-pack/plugins/case/server/client/index.ts
@@ -5,73 +5,41 @@
* 2.0.
*/
-import { CaseClientFactoryArguments, CaseClient } from './types';
+import {
+ CaseClientFactoryArguments,
+ CaseClient,
+ CaseClientFactoryMethods,
+ CaseClientMethods,
+} from './types';
import { create } from './cases/create';
+import { get } from './cases/get';
import { update } from './cases/update';
+import { push } from './cases/push';
import { addComment } from './comments/add';
import { getFields } from './configure/get_fields';
import { getMappings } from './configure/get_mappings';
import { updateAlertsStatus } from './alerts/update_status';
+import { get as getUserActions } from './user_actions/get';
+import { get as getAlerts } from './alerts/get';
export { CaseClient } from './types';
-export const createCaseClient = ({
- caseConfigureService,
- caseService,
- connectorMappingsService,
- request,
- savedObjectsClient,
- userActionService,
- alertsService,
- context,
-}: CaseClientFactoryArguments): CaseClient => {
- return {
- create: create({
- alertsService,
- caseConfigureService,
- caseService,
- connectorMappingsService,
- request,
- savedObjectsClient,
- userActionService,
- }),
- update: update({
- alertsService,
- caseConfigureService,
- caseService,
- connectorMappingsService,
- request,
- savedObjectsClient,
- userActionService,
- }),
- addComment: addComment({
- alertsService,
- caseConfigureService,
- caseService,
- connectorMappingsService,
- request,
- savedObjectsClient,
- userActionService,
- }),
- getFields: getFields(),
- getMappings: getMappings({
- alertsService,
- caseConfigureService,
- caseService,
- connectorMappingsService,
- request,
- savedObjectsClient,
- userActionService,
- }),
- updateAlertsStatus: updateAlertsStatus({
- alertsService,
- caseConfigureService,
- caseService,
- connectorMappingsService,
- context,
- request,
- savedObjectsClient,
- userActionService,
- }),
+export const createCaseClient = (args: CaseClientFactoryArguments): CaseClient => {
+ const methods: CaseClientFactoryMethods = {
+ create,
+ get,
+ update,
+ push,
+ addComment,
+ getAlerts,
+ getFields,
+ getMappings,
+ getUserActions,
+ updateAlertsStatus,
};
+
+ return (Object.keys(methods) as CaseClientMethods[]).reduce((client, method) => {
+ client[method] = methods[method](args);
+ return client;
+ }, {} as CaseClient);
};
diff --git a/x-pack/plugins/case/server/client/mocks.ts b/x-pack/plugins/case/server/client/mocks.ts
index 0d7f3972e58e7..b2a07e36b3aed 100644
--- a/x-pack/plugins/case/server/client/mocks.ts
+++ b/x-pack/plugins/case/server/client/mocks.ts
@@ -6,9 +6,9 @@
*/
import { omit } from 'lodash/fp';
-import { KibanaRequest } from 'kibana/server';
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { KibanaRequest, kibanaResponseFactory } from '../../../../../src/core/server/http';
import { loggingSystemMock } from '../../../../../src/core/server/mocks';
-import { actionsClientMock } from '../../../actions/server/mocks';
import {
AlertServiceContract,
CaseConfigureService,
@@ -17,17 +17,20 @@ import {
ConnectorMappingsService,
} from '../services';
import { CaseClient } from './types';
-import { authenticationMock } from '../routes/api/__fixtures__';
+import { authenticationMock, createActionsClient } from '../routes/api/__fixtures__';
import { createCaseClient } from '.';
-import { getActions } from '../routes/api/__mocks__/request_responses';
import type { CasesRequestHandlerContext } from '../types';
export type CaseClientMock = jest.Mocked;
export const createCaseClientMock = (): CaseClientMock => ({
addComment: jest.fn(),
create: jest.fn(),
+ get: jest.fn(),
+ push: jest.fn(),
+ getAlerts: jest.fn(),
getFields: jest.fn(),
getMappings: jest.fn(),
+ getUserActions: jest.fn(),
update: jest.fn(),
updateAlertsStatus: jest.fn(),
});
@@ -47,10 +50,10 @@ export const createCaseClientWithMockSavedObjectsClient = async ({
alertsService: jest.Mocked;
};
}> => {
- const actionsMock = actionsClientMock.create();
- actionsMock.getAll.mockImplementation(() => Promise.resolve(getActions()));
+ const actionsMock = createActionsClient();
const log = loggingSystemMock.create().get('case');
const request = {} as KibanaRequest;
+ const response = kibanaResponseFactory;
const caseServicePlugin = new CaseService(log);
const caseConfigureServicePlugin = new CaseConfigureService(log);
@@ -63,11 +66,15 @@ export const createCaseClientWithMockSavedObjectsClient = async ({
const connectorMappingsService = await connectorMappingsServicePlugin.setup();
const userActionService = {
- postUserActions: jest.fn(),
getUserActions: jest.fn(),
+ postUserActions: jest.fn(),
};
- const alertsService = { initialize: jest.fn(), updateAlertsStatus: jest.fn() };
+ const alertsService = {
+ initialize: jest.fn(),
+ updateAlertsStatus: jest.fn(),
+ getAlerts: jest.fn(),
+ };
const context = {
core: {
@@ -89,6 +96,7 @@ export const createCaseClientWithMockSavedObjectsClient = async ({
const caseClient = createCaseClient({
savedObjectsClient,
request,
+ response,
caseService,
caseConfigureService,
connectorMappingsService,
diff --git a/x-pack/plugins/case/server/client/types.ts b/x-pack/plugins/case/server/client/types.ts
index a3466e26294f8..8778aa46a2d24 100644
--- a/x-pack/plugins/case/server/client/types.ts
+++ b/x-pack/plugins/case/server/client/types.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { KibanaRequest, SavedObjectsClientContract } from 'kibana/server';
+import { KibanaRequest, KibanaResponseFactory, SavedObjectsClientContract } from 'kibana/server';
import { ActionsClient } from '../../../actions/server';
import {
CasePostRequest,
@@ -16,6 +16,7 @@ import {
CommentRequest,
ConnectorMappingsAttributes,
GetFieldsResponse,
+ CaseUserActionsResponse,
} from '../../common/api';
import {
CaseConfigureServiceSetup,
@@ -25,6 +26,7 @@ import {
} from '../services';
import { ConnectorMappingsServiceSetup } from '../services/connector_mappings';
import type { CasesRequestHandlerContext } from '../types';
+import { CaseClientGetAlertsResponse } from './alerts/types';
export interface CaseClientCreate {
theCase: CasePostRequest;
@@ -35,6 +37,18 @@ export interface CaseClientUpdate {
cases: CasesPatchRequest;
}
+export interface CaseClientGet {
+ id: string;
+ includeComments?: boolean;
+}
+
+export interface CaseClientPush {
+ actionsClient: ActionsClient;
+ caseClient: CaseClient;
+ caseId: string;
+ connectorId: string;
+}
+
export interface CaseClientAddComment {
caseClient: CaseClient;
caseId: string;
@@ -46,11 +60,27 @@ export interface CaseClientUpdateAlertsStatus {
status: CaseStatuses;
}
+export interface CaseClientGetAlerts {
+ ids: string[];
+}
+
+export interface CaseClientGetUserActions {
+ caseId: string;
+}
+
+export interface MappingsClient {
+ actionsClient: ActionsClient;
+ caseClient: CaseClient;
+ connectorId: string;
+ connectorType: string;
+}
+
export interface CaseClientFactoryArguments {
caseConfigureService: CaseConfigureServiceSetup;
caseService: CaseServiceSetup;
connectorMappingsService: ConnectorMappingsServiceSetup;
request: KibanaRequest;
+ response: KibanaResponseFactory;
savedObjectsClient: SavedObjectsClientContract;
userActionService: CaseUserActionServiceSetup;
alertsService: AlertServiceContract;
@@ -65,15 +95,22 @@ export interface ConfigureFields {
export interface CaseClient {
addComment: (args: CaseClientAddComment) => Promise;
create: (args: CaseClientCreate) => Promise;
+ get: (args: CaseClientGet) => Promise;
+ getAlerts: (args: CaseClientGetAlerts) => Promise;
getFields: (args: ConfigureFields) => Promise;
getMappings: (args: MappingsClient) => Promise;
+ getUserActions: (args: CaseClientGetUserActions) => Promise;
+ push: (args: CaseClientPush) => Promise;
update: (args: CaseClientUpdate) => Promise;
updateAlertsStatus: (args: CaseClientUpdateAlertsStatus) => Promise;
}
-export interface MappingsClient {
- actionsClient: ActionsClient;
- caseClient: CaseClient;
- connectorId: string;
- connectorType: string;
-}
+export type CaseClientFactoryMethod = (
+ factoryArgs: CaseClientFactoryArguments
+) => (methodArgs: any) => Promise;
+
+export type CaseClientMethods = keyof CaseClient;
+
+export type CaseClientFactoryMethods = {
+ [K in CaseClientMethods]: CaseClientFactoryMethod;
+};
diff --git a/x-pack/plugins/case/server/client/user_actions/get.ts b/x-pack/plugins/case/server/client/user_actions/get.ts
new file mode 100644
index 0000000000000..e83a9e3484262
--- /dev/null
+++ b/x-pack/plugins/case/server/client/user_actions/get.ts
@@ -0,0 +1,31 @@
+/*
+ * 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, CASE_COMMENT_SAVED_OBJECT } from '../../saved_object_types';
+import { CaseUserActionsResponseRt, CaseUserActionsResponse } from '../../../common/api';
+import { CaseClientGetUserActions, CaseClientFactoryArguments } from '../types';
+
+export const get = ({
+ savedObjectsClient,
+ userActionService,
+}: CaseClientFactoryArguments) => async ({
+ caseId,
+}: CaseClientGetUserActions): Promise => {
+ const userActions = await userActionService.getUserActions({
+ client: savedObjectsClient,
+ caseId,
+ });
+
+ return CaseUserActionsResponseRt.encode(
+ userActions.saved_objects.map((ua) => ({
+ ...ua.attributes,
+ action_id: ua.id,
+ case_id: ua.references.find((r) => r.type === CASE_SAVED_OBJECT)?.id ?? '',
+ comment_id: ua.references.find((r) => r.type === CASE_COMMENT_SAVED_OBJECT)?.id ?? null,
+ }))
+ );
+};
diff --git a/x-pack/plugins/case/server/connectors/case/index.ts b/x-pack/plugins/case/server/connectors/case/index.ts
index 01446942c33c6..9907aa5b3cd3a 100644
--- a/x-pack/plugins/case/server/connectors/case/index.ts
+++ b/x-pack/plugins/case/server/connectors/case/index.ts
@@ -7,7 +7,7 @@
import { curry } from 'lodash';
-import { KibanaRequest } from 'kibana/server';
+import { KibanaRequest, kibanaResponseFactory } from '../../../../../../src/core/server';
import { ActionTypeExecutorResult } from '../../../../actions/common';
import { CasePatchRequest, CasePostRequest } from '../../../common/api';
import { createCaseClient } from '../../client';
@@ -73,6 +73,7 @@ async function executor(
const caseClient = createCaseClient({
savedObjectsClient,
request: {} as KibanaRequest,
+ response: kibanaResponseFactory,
caseService,
caseConfigureService,
connectorMappingsService,
diff --git a/x-pack/plugins/case/server/connectors/index.ts b/x-pack/plugins/case/server/connectors/index.ts
index 100511e271b02..00809d81ca5f2 100644
--- a/x-pack/plugins/case/server/connectors/index.ts
+++ b/x-pack/plugins/case/server/connectors/index.ts
@@ -5,43 +5,14 @@
* 2.0.
*/
-import { Logger } from 'kibana/server';
-import {
- ActionTypeConfig,
- ActionTypeSecrets,
- ActionTypeParams,
- ActionType,
- // eslint-disable-next-line @kbn/eslint/no-restricted-paths
-} from '../../../actions/server/types';
-import {
- CaseServiceSetup,
- CaseConfigureServiceSetup,
- CaseUserActionServiceSetup,
- ConnectorMappingsServiceSetup,
- AlertServiceContract,
-} from '../services';
-
+import { RegisterConnectorsArgs, ExternalServiceFormatterMapper } from './types';
import { getActionType as getCaseConnector } from './case';
+import { serviceNowITSMExternalServiceFormatter } from './servicenow/itsm_formatter';
+import { serviceNowSIRExternalServiceFormatter } from './servicenow/sir_formatter';
+import { jiraExternalServiceFormatter } from './jira/external_service_formatter';
+import { resilientExternalServiceFormatter } from './resilient/external_service_formatter';
-export interface GetActionTypeParams {
- logger: Logger;
- caseService: CaseServiceSetup;
- caseConfigureService: CaseConfigureServiceSetup;
- connectorMappingsService: ConnectorMappingsServiceSetup;
- userActionService: CaseUserActionServiceSetup;
- alertsService: AlertServiceContract;
-}
-
-export interface RegisterConnectorsArgs extends GetActionTypeParams {
- actionsRegisterType<
- Config extends ActionTypeConfig = ActionTypeConfig,
- Secrets extends ActionTypeSecrets = ActionTypeSecrets,
- Params extends ActionTypeParams = ActionTypeParams,
- ExecutorResultData = void
- >(
- actionType: ActionType
- ): void;
-}
+export * from './types';
export const registerConnectors = ({
actionsRegisterType,
@@ -63,3 +34,10 @@ export const registerConnectors = ({
})
);
};
+
+export const externalServiceFormatters: ExternalServiceFormatterMapper = {
+ '.servicenow': serviceNowITSMExternalServiceFormatter,
+ '.servicenow-sir': serviceNowSIRExternalServiceFormatter,
+ '.jira': jiraExternalServiceFormatter,
+ '.resilient': resilientExternalServiceFormatter,
+};
diff --git a/x-pack/plugins/case/server/connectors/jira/external_service_formatter.test.ts b/x-pack/plugins/case/server/connectors/jira/external_service_formatter.test.ts
new file mode 100644
index 0000000000000..0bfaf7cdbd9e3
--- /dev/null
+++ b/x-pack/plugins/case/server/connectors/jira/external_service_formatter.test.ts
@@ -0,0 +1,35 @@
+/*
+ * 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 { CaseResponse } from '../../../common/api';
+import { jiraExternalServiceFormatter } from './external_service_formatter';
+
+describe('Jira formatter', () => {
+ const theCase = {
+ tags: ['tag'],
+ connector: { fields: { priority: 'High', issueType: 'Task', parent: null } },
+ } as CaseResponse;
+
+ it('it formats correctly', async () => {
+ const res = await jiraExternalServiceFormatter.format(theCase, []);
+ expect(res).toEqual({ ...theCase.connector.fields, labels: theCase.tags });
+ });
+
+ it('it formats correctly when fields do not exist ', async () => {
+ const invalidFields = { tags: ['tag'], connector: { fields: null } } as CaseResponse;
+ const res = await jiraExternalServiceFormatter.format(invalidFields, []);
+ expect(res).toEqual({ priority: null, issueType: null, parent: null, labels: theCase.tags });
+ });
+
+ it('it replace white spaces with hyphens on tags', async () => {
+ const res = await jiraExternalServiceFormatter.format(
+ { ...theCase, tags: ['a tag with spaces'] },
+ []
+ );
+ expect(res).toEqual({ ...theCase.connector.fields, labels: ['a-tag-with-spaces'] });
+ });
+});
diff --git a/x-pack/plugins/case/server/connectors/jira/external_service_formatter.ts b/x-pack/plugins/case/server/connectors/jira/external_service_formatter.ts
new file mode 100644
index 0000000000000..74376d295fea5
--- /dev/null
+++ b/x-pack/plugins/case/server/connectors/jira/external_service_formatter.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { JiraFieldsType, ConnectorJiraTypeFields } from '../../../common/api';
+import { ExternalServiceFormatter } from '../types';
+
+interface ExternalServiceParams extends JiraFieldsType {
+ labels: string[];
+}
+
+const format: ExternalServiceFormatter['format'] = (theCase) => {
+ const { priority = null, issueType = null, parent = null } =
+ (theCase.connector.fields as ConnectorJiraTypeFields['fields']) ?? {};
+ return {
+ priority,
+ // Jira do not allows empty spaces on labels. We replace white spaces with hyphens
+ labels: theCase.tags.map((tag) => tag.replace(/\s+/g, '-')),
+ issueType,
+ parent,
+ };
+};
+
+export const jiraExternalServiceFormatter: ExternalServiceFormatter = {
+ format,
+};
diff --git a/x-pack/plugins/case/server/connectors/resilient/external_service_formatter.test.ts b/x-pack/plugins/case/server/connectors/resilient/external_service_formatter.test.ts
new file mode 100644
index 0000000000000..01280e9692b5e
--- /dev/null
+++ b/x-pack/plugins/case/server/connectors/resilient/external_service_formatter.test.ts
@@ -0,0 +1,26 @@
+/*
+ * 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 { CaseResponse } from '../../../common/api';
+import { resilientExternalServiceFormatter } from './external_service_formatter';
+
+describe('IBM Resilient formatter', () => {
+ const theCase = {
+ connector: { fields: { incidentTypes: ['2'], severityCode: '2' } },
+ } as CaseResponse;
+
+ it('it formats correctly', async () => {
+ const res = await resilientExternalServiceFormatter.format(theCase, []);
+ expect(res).toEqual({ ...theCase.connector.fields });
+ });
+
+ it('it formats correctly when fields do not exist ', async () => {
+ const invalidFields = { tags: ['a tag'], connector: { fields: null } } as CaseResponse;
+ const res = await resilientExternalServiceFormatter.format(invalidFields, []);
+ expect(res).toEqual({ incidentTypes: null, severityCode: null });
+ });
+});
diff --git a/x-pack/plugins/case/server/connectors/resilient/external_service_formatter.ts b/x-pack/plugins/case/server/connectors/resilient/external_service_formatter.ts
new file mode 100644
index 0000000000000..76554dce32797
--- /dev/null
+++ b/x-pack/plugins/case/server/connectors/resilient/external_service_formatter.ts
@@ -0,0 +1,19 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { ResilientFieldsType, ConnectorResillientTypeFields } from '../../../common/api';
+import { ExternalServiceFormatter } from '../types';
+
+const format: ExternalServiceFormatter['format'] = (theCase) => {
+ const { incidentTypes = null, severityCode = null } =
+ (theCase.connector.fields as ConnectorResillientTypeFields['fields']) ?? {};
+ return { incidentTypes, severityCode };
+};
+
+export const resilientExternalServiceFormatter: ExternalServiceFormatter = {
+ format,
+};
diff --git a/x-pack/plugins/case/server/connectors/servicenow/itsm_formatter.ts b/x-pack/plugins/case/server/connectors/servicenow/itsm_formatter.ts
new file mode 100644
index 0000000000000..60faa82a9e3fa
--- /dev/null
+++ b/x-pack/plugins/case/server/connectors/servicenow/itsm_formatter.ts
@@ -0,0 +1,19 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { ServiceNowITSMFieldsType, ConnectorServiceNowITSMTypeFields } from '../../../common/api';
+import { ExternalServiceFormatter } from '../types';
+
+const format: ExternalServiceFormatter['format'] = (theCase) => {
+ const { severity = null, urgency = null, impact = null } =
+ (theCase.connector.fields as ConnectorServiceNowITSMTypeFields['fields']) ?? {};
+ return { severity, urgency, impact };
+};
+
+export const serviceNowITSMExternalServiceFormatter: ExternalServiceFormatter = {
+ format,
+};
diff --git a/x-pack/plugins/case/server/connectors/servicenow/itsm_formmater.test.ts b/x-pack/plugins/case/server/connectors/servicenow/itsm_formmater.test.ts
new file mode 100644
index 0000000000000..033f184c7e751
--- /dev/null
+++ b/x-pack/plugins/case/server/connectors/servicenow/itsm_formmater.test.ts
@@ -0,0 +1,26 @@
+/*
+ * 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 { CaseResponse } from '../../../common/api';
+import { serviceNowITSMExternalServiceFormatter } from './itsm_formatter';
+
+describe('ITSM formatter', () => {
+ const theCase = {
+ connector: { fields: { severity: '2', urgency: '2', impact: '2' } },
+ } as CaseResponse;
+
+ it('it formats correctly', async () => {
+ const res = await serviceNowITSMExternalServiceFormatter.format(theCase, []);
+ expect(res).toEqual(theCase.connector.fields);
+ });
+
+ it('it formats correctly when fields do not exist ', async () => {
+ const invalidFields = { connector: { fields: null } } as CaseResponse;
+ const res = await serviceNowITSMExternalServiceFormatter.format(invalidFields, []);
+ expect(res).toEqual({ severity: null, urgency: null, impact: null });
+ });
+});
diff --git a/x-pack/plugins/case/server/connectors/servicenow/sir_formatter.test.ts b/x-pack/plugins/case/server/connectors/servicenow/sir_formatter.test.ts
new file mode 100644
index 0000000000000..4faca62c6e706
--- /dev/null
+++ b/x-pack/plugins/case/server/connectors/servicenow/sir_formatter.test.ts
@@ -0,0 +1,164 @@
+/*
+ * 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 { CaseResponse } from '../../../common/api';
+import { serviceNowSIRExternalServiceFormatter } from './sir_formatter';
+
+describe('ITSM formatter', () => {
+ const theCase = {
+ connector: {
+ fields: {
+ destIp: true,
+ sourceIp: true,
+ category: 'Denial of Service',
+ subcategory: 'Inbound DDos',
+ malwareHash: true,
+ malwareUrl: true,
+ priority: '2 - High',
+ },
+ },
+ } as CaseResponse;
+
+ it('it formats correctly without alerts', async () => {
+ const res = await serviceNowSIRExternalServiceFormatter.format(theCase, []);
+ expect(res).toEqual({
+ dest_ip: null,
+ source_ip: null,
+ category: 'Denial of Service',
+ subcategory: 'Inbound DDos',
+ malware_hash: null,
+ malware_url: null,
+ priority: '2 - High',
+ });
+ });
+
+ it('it formats correctly when fields do not exist ', async () => {
+ const invalidFields = { connector: { fields: null } } as CaseResponse;
+ const res = await serviceNowSIRExternalServiceFormatter.format(invalidFields, []);
+ expect(res).toEqual({
+ dest_ip: null,
+ source_ip: null,
+ category: null,
+ subcategory: null,
+ malware_hash: null,
+ malware_url: null,
+ priority: null,
+ });
+ });
+
+ it('it formats correctly with alerts', async () => {
+ const alerts = [
+ {
+ id: 'alert-1',
+ index: 'index-1',
+ destination: { ip: '192.168.1.1' },
+ source: { ip: '192.168.1.2' },
+ file: {
+ hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' },
+ },
+ url: { full: 'https://attack.com' },
+ },
+ {
+ id: 'alert-2',
+ index: 'index-2',
+ destination: { ip: '192.168.1.4' },
+ source: { ip: '192.168.1.3' },
+ file: {
+ hash: { sha256: '60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752' },
+ },
+ url: { full: 'https://attack.com/api' },
+ },
+ ];
+ const res = await serviceNowSIRExternalServiceFormatter.format(theCase, alerts);
+ expect(res).toEqual({
+ dest_ip: '192.168.1.1,192.168.1.4',
+ source_ip: '192.168.1.2,192.168.1.3',
+ category: 'Denial of Service',
+ subcategory: 'Inbound DDos',
+ malware_hash:
+ '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08,60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752',
+ malware_url: 'https://attack.com,https://attack.com/api',
+ priority: '2 - High',
+ });
+ });
+
+ it('it handles duplicates correctly', async () => {
+ const alerts = [
+ {
+ id: 'alert-1',
+ index: 'index-1',
+ destination: { ip: '192.168.1.1' },
+ source: { ip: '192.168.1.2' },
+ file: {
+ hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' },
+ },
+ url: { full: 'https://attack.com' },
+ },
+ {
+ id: 'alert-2',
+ index: 'index-2',
+ destination: { ip: '192.168.1.1' },
+ source: { ip: '192.168.1.3' },
+ file: {
+ hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' },
+ },
+ url: { full: 'https://attack.com/api' },
+ },
+ ];
+ const res = await serviceNowSIRExternalServiceFormatter.format(theCase, alerts);
+ expect(res).toEqual({
+ dest_ip: '192.168.1.1',
+ source_ip: '192.168.1.2,192.168.1.3',
+ category: 'Denial of Service',
+ subcategory: 'Inbound DDos',
+ malware_hash: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08',
+ malware_url: 'https://attack.com,https://attack.com/api',
+ priority: '2 - High',
+ });
+ });
+
+ it('it formats correctly when field is not selected', async () => {
+ const alerts = [
+ {
+ id: 'alert-1',
+ index: 'index-1',
+ destination: { ip: '192.168.1.1' },
+ source: { ip: '192.168.1.2' },
+ file: {
+ hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' },
+ },
+ url: { full: 'https://attack.com' },
+ },
+ {
+ id: 'alert-2',
+ index: 'index-2',
+ destination: { ip: '192.168.1.1' },
+ source: { ip: '192.168.1.3' },
+ file: {
+ hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' },
+ },
+ url: { full: 'https://attack.com/api' },
+ },
+ ];
+
+ const newCase = {
+ ...theCase,
+ connector: { fields: { ...theCase.connector.fields, destIp: false, malwareHash: false } },
+ } as CaseResponse;
+
+ const res = await serviceNowSIRExternalServiceFormatter.format(newCase, alerts);
+ expect(res).toEqual({
+ dest_ip: null,
+ source_ip: '192.168.1.2,192.168.1.3',
+ category: 'Denial of Service',
+ subcategory: 'Inbound DDos',
+ malware_hash: null,
+ malware_url: 'https://attack.com,https://attack.com/api',
+ priority: '2 - High',
+ });
+ });
+});
diff --git a/x-pack/plugins/case/server/connectors/servicenow/sir_formatter.ts b/x-pack/plugins/case/server/connectors/servicenow/sir_formatter.ts
new file mode 100644
index 0000000000000..d2458e6c7ae53
--- /dev/null
+++ b/x-pack/plugins/case/server/connectors/servicenow/sir_formatter.ts
@@ -0,0 +1,88 @@
+/*
+ * 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 { get } from 'lodash/fp';
+import { ConnectorServiceNowSIRTypeFields } from '../../../common/api';
+import { ExternalServiceFormatter } from '../types';
+interface ExternalServiceParams {
+ dest_ip: string | null;
+ source_ip: string | null;
+ category: string | null;
+ subcategory: string | null;
+ malware_hash: string | null;
+ malware_url: string | null;
+ priority: string | null;
+}
+type SirFieldKey = 'dest_ip' | 'source_ip' | 'malware_hash' | 'malware_url';
+type AlertFieldMappingAndValues = Record<
+ string,
+ { alertPath: string; sirFieldKey: SirFieldKey; add: boolean }
+>;
+const format: ExternalServiceFormatter['format'] = (theCase, alerts) => {
+ const {
+ destIp = null,
+ sourceIp = null,
+ category = null,
+ subcategory = null,
+ malwareHash = null,
+ malwareUrl = null,
+ priority = null,
+ } = (theCase.connector.fields as ConnectorServiceNowSIRTypeFields['fields']) ?? {};
+ const alertFieldMapping: AlertFieldMappingAndValues = {
+ destIp: { alertPath: 'destination.ip', sirFieldKey: 'dest_ip', add: !!destIp },
+ sourceIp: { alertPath: 'source.ip', sirFieldKey: 'source_ip', add: !!sourceIp },
+ malwareHash: { alertPath: 'file.hash.sha256', sirFieldKey: 'malware_hash', add: !!malwareHash },
+ malwareUrl: { alertPath: 'url.full', sirFieldKey: 'malware_url', add: !!malwareUrl },
+ };
+
+ const manageDuplicate: Record> = {
+ dest_ip: new Set(),
+ source_ip: new Set(),
+ malware_hash: new Set(),
+ malware_url: new Set(),
+ };
+
+ let sirFields: Record = {
+ dest_ip: null,
+ source_ip: null,
+ malware_hash: null,
+ malware_url: null,
+ };
+
+ const fieldsToAdd = (Object.keys(alertFieldMapping) as SirFieldKey[]).filter(
+ (key) => alertFieldMapping[key].add
+ );
+
+ if (fieldsToAdd.length > 0) {
+ sirFields = alerts.reduce>((acc, alert) => {
+ fieldsToAdd.forEach((alertField) => {
+ const field = get(alertFieldMapping[alertField].alertPath, alert);
+ if (field && !manageDuplicate[alertFieldMapping[alertField].sirFieldKey].has(field)) {
+ manageDuplicate[alertFieldMapping[alertField].sirFieldKey].add(field);
+ acc = {
+ ...acc,
+ [alertFieldMapping[alertField].sirFieldKey]: `${
+ acc[alertFieldMapping[alertField].sirFieldKey] != null
+ ? `${acc[alertFieldMapping[alertField].sirFieldKey]},${field}`
+ : field
+ }`,
+ };
+ }
+ });
+ return acc;
+ }, sirFields);
+ }
+
+ return {
+ ...sirFields,
+ category,
+ subcategory,
+ priority,
+ };
+};
+export const serviceNowSIRExternalServiceFormatter: ExternalServiceFormatter = {
+ format,
+};
diff --git a/x-pack/plugins/case/server/connectors/types.ts b/x-pack/plugins/case/server/connectors/types.ts
new file mode 100644
index 0000000000000..8e7eb91ad2dc6
--- /dev/null
+++ b/x-pack/plugins/case/server/connectors/types.ts
@@ -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 { Logger } from 'kibana/server';
+import {
+ ActionTypeConfig,
+ ActionTypeSecrets,
+ ActionTypeParams,
+ ActionType,
+ // eslint-disable-next-line @kbn/eslint/no-restricted-paths
+} from '../../../actions/server/types';
+import { CaseResponse, ConnectorTypes } from '../../common/api';
+import { CaseClientGetAlertsResponse } from '../client/alerts/types';
+import {
+ CaseServiceSetup,
+ CaseConfigureServiceSetup,
+ CaseUserActionServiceSetup,
+ ConnectorMappingsServiceSetup,
+ AlertServiceContract,
+} from '../services';
+
+export interface GetActionTypeParams {
+ logger: Logger;
+ caseService: CaseServiceSetup;
+ caseConfigureService: CaseConfigureServiceSetup;
+ connectorMappingsService: ConnectorMappingsServiceSetup;
+ userActionService: CaseUserActionServiceSetup;
+ alertsService: AlertServiceContract;
+}
+
+export interface RegisterConnectorsArgs extends GetActionTypeParams {
+ actionsRegisterType<
+ Config extends ActionTypeConfig = ActionTypeConfig,
+ Secrets extends ActionTypeSecrets = ActionTypeSecrets,
+ Params extends ActionTypeParams = ActionTypeParams,
+ ExecutorResultData = void
+ >(
+ actionType: ActionType
+ ): void;
+}
+
+export type FormatterConnectorTypes = Exclude;
+
+export interface ExternalServiceFormatter {
+ format: (theCase: CaseResponse, alerts: CaseClientGetAlertsResponse) => TExternalServiceParams;
+}
+
+export type ExternalServiceFormatterMapper = {
+ [x in FormatterConnectorTypes]: ExternalServiceFormatter;
+};
diff --git a/x-pack/plugins/case/server/plugin.ts b/x-pack/plugins/case/server/plugin.ts
index 8b4fdc73dab44..5d05db165f637 100644
--- a/x-pack/plugins/case/server/plugin.ts
+++ b/x-pack/plugins/case/server/plugin.ts
@@ -5,7 +5,13 @@
* 2.0.
*/
-import { IContextProvider, KibanaRequest, Logger, PluginInitializerContext } from 'kibana/server';
+import {
+ IContextProvider,
+ KibanaRequest,
+ KibanaResponseFactory,
+ Logger,
+ PluginInitializerContext,
+} from 'kibana/server';
import { CoreSetup, CoreStart } from 'src/core/server';
import { SecurityPluginSetup } from '../../security/server';
@@ -123,11 +129,13 @@ export class CasePlugin {
const getCaseClientWithRequestAndContext = async (
context: CasesRequestHandlerContext,
- request: KibanaRequest
+ request: KibanaRequest,
+ response: KibanaResponseFactory
) => {
return createCaseClient({
savedObjectsClient: core.savedObjects.getScopedClient(request),
request,
+ response,
caseService: this.caseService!,
caseConfigureService: this.caseConfigureService!,
connectorMappingsService: this.connectorMappingsService!,
@@ -161,7 +169,7 @@ export class CasePlugin {
userActionService: CaseUserActionServiceSetup;
alertsService: AlertServiceContract;
}): IContextProvider => {
- return async (context, request) => {
+ return async (context, request, response) => {
const [{ savedObjects }] = await core.getStartServices();
return {
getCaseClient: () => {
@@ -172,8 +180,9 @@ export class CasePlugin {
connectorMappingsService,
userActionService,
alertsService,
- request,
context,
+ request,
+ response,
});
},
};
diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts
index 8dc970d235fea..18730effdf55a 100644
--- a/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts
+++ b/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts
@@ -17,6 +17,7 @@ import {
CASE_SAVED_OBJECT,
CASE_CONFIGURE_SAVED_OBJECT,
CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT,
+ CASE_USER_ACTION_SAVED_OBJECT,
} from '../../../saved_object_types';
export const createMockSavedObjectsRepository = ({
@@ -24,11 +25,13 @@ export const createMockSavedObjectsRepository = ({
caseCommentSavedObject = [],
caseConfigureSavedObject = [],
caseMappingsSavedObject = [],
+ caseUserActionsSavedObject = [],
}: {
caseSavedObject?: any[];
caseCommentSavedObject?: any[];
caseConfigureSavedObject?: any[];
caseMappingsSavedObject?: any[];
+ caseUserActionsSavedObject?: any[];
} = {}) => {
const mockSavedObjectsClientContract = ({
bulkGet: jest.fn((objects: SavedObjectsBulkGetObject[]) => {
@@ -57,6 +60,7 @@ export const createMockSavedObjectsRepository = ({
}),
};
}),
+ bulkCreate: jest.fn(),
bulkUpdate: jest.fn((objects: Array>) => {
return {
saved_objects: objects.map(({ id, type, attributes }) => {
@@ -136,6 +140,16 @@ export const createMockSavedObjectsRepository = ({
saved_objects: caseCommentSavedObject,
};
}
+
+ if (findArgs.type === CASE_USER_ACTION_SAVED_OBJECT) {
+ return {
+ page: 1,
+ per_page: 5,
+ total: caseUserActionsSavedObject.length,
+ saved_objects: caseUserActionsSavedObject,
+ };
+ }
+
return {
page: 1,
per_page: 5,
diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/index.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/index.ts
index 5e2c29f29a3e7..1abd44aec1552 100644
--- a/x-pack/plugins/case/server/routes/api/__fixtures__/index.ts
+++ b/x-pack/plugins/case/server/routes/api/__fixtures__/index.ts
@@ -10,3 +10,4 @@ export { createMockSavedObjectsRepository } from './create_mock_so_repository';
export { createRouteContext } from './route_contexts';
export { authenticationMock } from './authc_mock';
export { createRoute } from './mock_router';
+export { createActionsClient } from './mock_actions_client';
diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_actions_client.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_actions_client.ts
new file mode 100644
index 0000000000000..d153c328cbb91
--- /dev/null
+++ b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_actions_client.ts
@@ -0,0 +1,34 @@
+/*
+ * 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 { SavedObjectsErrorHelpers } from 'src/core/server';
+import { actionsClientMock } from '../../../../../actions/server/mocks';
+import {
+ getActions,
+ getActionTypes,
+ getActionExecuteResults,
+} from '../__mocks__/request_responses';
+
+export const createActionsClient = () => {
+ const actionsMock = actionsClientMock.create();
+ actionsMock.getAll.mockImplementation(() => Promise.resolve(getActions()));
+ actionsMock.listTypes.mockImplementation(() => Promise.resolve(getActionTypes()));
+ actionsMock.get.mockImplementation(({ id }) => {
+ const actions = getActions();
+ const action = actions.find((a) => a.id === id);
+ if (action) {
+ return Promise.resolve(action);
+ } else {
+ return Promise.reject(SavedObjectsErrorHelpers.createGenericNotFoundError('action', id));
+ }
+ });
+ actionsMock.execute.mockImplementation(({ actionId }) =>
+ Promise.resolve(getActionExecuteResults(actionId))
+ );
+
+ return actionsMock;
+};
diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts
index 4ac5004eb3dfd..514f77a8f953d 100644
--- a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts
+++ b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts
@@ -8,6 +8,7 @@
import { SavedObject } from 'kibana/server';
import {
CaseStatuses,
+ CaseUserActionAttributes,
CommentAttributes,
CommentType,
ConnectorMappings,
@@ -15,7 +16,10 @@ import {
ESCaseAttributes,
ESCasesConfigureAttributes,
} from '../../../../common/api';
-import { CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT } from '../../../saved_object_types';
+import {
+ CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT,
+ CASE_USER_ACTION_SAVED_OBJECT,
+} from '../../../saved_object_types';
import { mappings } from '../../../client/configure/mock';
export const mockCases: Array> = [
@@ -424,3 +428,44 @@ export const mockCaseMappings: Array> = [
references: [],
},
];
+
+export const mockUserActions: Array> = [
+ {
+ type: CASE_USER_ACTION_SAVED_OBJECT,
+ id: 'mock-user-actions-1',
+ attributes: {
+ action_field: ['description', 'status', 'tags', 'title', 'connector', 'settings'],
+ action: 'create',
+ action_at: '2021-02-03T17:41:03.771Z',
+ action_by: {
+ email: 'elastic@elastic.co',
+ full_name: 'Elastic',
+ username: 'elastic',
+ },
+ new_value:
+ '{"title":"A case","tags":["case"],"description":"Yeah!","connector":{"id":"connector-od","name":"My Connector","type":".servicenow-sir","fields":{"category":"Denial of Service","destIp":true,"malwareHash":true,"malwareUrl":true,"priority":"2","sourceIp":true,"subcategory":"45"}},"settings":{"syncAlerts":true}}',
+ old_value: null,
+ },
+ version: 'WzYsMV0=',
+ references: [],
+ },
+ {
+ type: CASE_USER_ACTION_SAVED_OBJECT,
+ id: 'mock-user-actions-2',
+ attributes: {
+ action_field: ['comment'],
+ action: 'create',
+ action_at: '2021-02-03T17:44:21.067Z',
+ action_by: {
+ email: 'elastic@elastic.co',
+ full_name: 'Elastic',
+ username: 'elastic',
+ },
+ new_value:
+ '{"type":"alert","alertId":"cec3da90fb37a44407145adf1593f3b0d5ad94c4654201f773d63b5d4706128e","index":".siem-signals-default-000008"}',
+ old_value: null,
+ },
+ version: 'WzYsMV0=',
+ references: [],
+ },
+];
diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts
index 9f7258fc7edaf..74665ffdc5b16 100644
--- a/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts
+++ b/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts
@@ -5,24 +5,25 @@
* 2.0.
*/
-import { KibanaRequest } from 'src/core/server';
-import { loggingSystemMock, elasticsearchServiceMock } from 'src/core/server/mocks';
-import { actionsClientMock } from '../../../../../actions/server/mocks';
+import { KibanaRequest, kibanaResponseFactory } from '../../../../../../../src/core/server';
+import {
+ loggingSystemMock,
+ elasticsearchServiceMock,
+} from '../../../../../../../src/core/server/mocks';
import { createCaseClient } from '../../../client';
import {
AlertService,
CaseService,
CaseConfigureService,
ConnectorMappingsService,
+ CaseUserActionService,
} from '../../../services';
-import { getActions, getActionTypes } from '../__mocks__/request_responses';
import { authenticationMock } from '../__fixtures__';
import type { CasesRequestHandlerContext } from '../../../types';
+import { createActionsClient } from './mock_actions_client';
export const createRouteContext = async (client: any, badAuth = false) => {
- const actionsMock = actionsClientMock.create();
- actionsMock.getAll.mockImplementation(() => Promise.resolve(getActions()));
- actionsMock.listTypes.mockImplementation(() => Promise.resolve(getActionTypes()));
+ const actionsMock = createActionsClient();
const log = loggingSystemMock.create().get('case');
const esClientMock = elasticsearchServiceMock.createClusterClient();
@@ -30,11 +31,13 @@ export const createRouteContext = async (client: any, badAuth = false) => {
const caseServicePlugin = new CaseService(log);
const caseConfigureServicePlugin = new CaseConfigureService(log);
const connectorMappingsServicePlugin = new ConnectorMappingsService(log);
+ const caseUserActionsServicePlugin = new CaseUserActionService(log);
const caseService = await caseServicePlugin.setup({
authentication: badAuth ? authenticationMock.createInvalid() : authenticationMock.create(),
});
const caseConfigureService = await caseConfigureServicePlugin.setup();
+ const userActionService = await caseUserActionsServicePlugin.setup();
const alertsService = new AlertService();
alertsService.initialize(esClientMock);
@@ -59,16 +62,14 @@ export const createRouteContext = async (client: any, badAuth = false) => {
const caseClient = createCaseClient({
savedObjectsClient: client,
request: {} as KibanaRequest,
+ response: kibanaResponseFactory,
caseService,
caseConfigureService,
connectorMappingsService,
- userActionService: {
- postUserActions: jest.fn(),
- getUserActions: jest.fn(),
- },
+ userActionService,
alertsService,
context,
});
- return context;
+ return { context, services: { userActionService } };
};
diff --git a/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts b/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts
index f2109167527c7..ae14b44e7dffe 100644
--- a/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts
+++ b/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts
@@ -10,11 +10,9 @@ import {
CasePostRequest,
CasesConfigureRequest,
ConnectorTypes,
- PostPushRequest,
} from '../../../../common/api';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { FindActionResult } from '../../../../../actions/server/types';
-import { params } from '../cases/configure/mock';
export const newCase: CasePostRequest = {
title: 'My new case',
@@ -74,6 +72,16 @@ export const getActions = (): FindActionResult[] => [
isPreconfigured: false,
referencedByCount: 0,
},
+ {
+ id: 'for-mock-case-id-3',
+ actionTypeId: '.jira',
+ name: 'For mock case id 3',
+ config: {
+ apiUrl: 'https://elastic.jira.com',
+ },
+ isPreconfigured: false,
+ referencedByCount: 0,
+ },
];
export const getActionTypes = (): ActionTypeConnector[] => [
@@ -119,6 +127,18 @@ export const getActionTypes = (): ActionTypeConnector[] => [
},
];
+export const getActionExecuteResults = (actionId = '123') => ({
+ status: 'ok' as const,
+ data: {
+ title: 'RJ2-200',
+ id: '10663',
+ pushedDate: '2020-12-17T00:32:40.738Z',
+ url: 'https://siem-kibana.atlassian.net/browse/RJ2-200',
+ comments: [],
+ },
+ actionId,
+});
+
export const newConfiguration: CasesConfigureRequest = {
connector: {
id: '456',
@@ -129,11 +149,6 @@ export const newConfiguration: CasesConfigureRequest = {
closure_type: 'close-by-pushing',
};
-export const newPostPushRequest: PostPushRequest = {
- params: params[ConnectorTypes.jira],
- connector_type: ConnectorTypes.jira,
-};
-
export const executePushResponse = {
status: 'ok',
data: {
diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.test.ts b/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.test.ts
index 9454f582e50c6..dcbcd7b9e246d 100644
--- a/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.test.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.test.ts
@@ -33,14 +33,14 @@ describe('DELETE comment', () => {
},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
caseCommentSavedObject: mockCaseComments,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(204);
});
it(`returns an error when thrown from deleteComment service`, async () => {
@@ -53,14 +53,14 @@ describe('DELETE comment', () => {
},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
caseCommentSavedObject: mockCaseComments,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(404);
});
});
diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.test.ts b/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.test.ts
index a1f4b8c2583cf..8ee43eaba8a82 100644
--- a/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.test.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.test.ts
@@ -34,14 +34,14 @@ describe('GET comment', () => {
},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
caseCommentSavedObject: mockCaseComments,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(200);
const myPayload = mockCaseComments.find((s) => s.id === 'mock-comment-1');
expect(myPayload).not.toBeUndefined();
@@ -59,13 +59,13 @@ describe('GET comment', () => {
},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseCommentSavedObject: mockCaseComments,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(404);
});
});
diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts
index 3bd8a688e1bba..33dc24d776c70 100644
--- a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts
@@ -41,14 +41,14 @@ describe('PATCH comment', () => {
},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
caseCommentSavedObject: mockCaseComments,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(200);
expect(response.payload.comments[response.payload.comments.length - 1].comment).toEqual(
'Update my comment'
@@ -71,14 +71,14 @@ describe('PATCH comment', () => {
},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
caseCommentSavedObject: mockCaseComments,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(200);
expect(response.payload.comments[response.payload.comments.length - 1].alertId).toEqual(
'new-id'
@@ -102,14 +102,14 @@ describe('PATCH comment', () => {
body: requestAttributes,
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
caseCommentSavedObject: mockCaseComments,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(400);
expect(response.payload.isBoom).toEqual(true);
}
@@ -130,14 +130,14 @@ describe('PATCH comment', () => {
},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
caseCommentSavedObject: mockCaseComments,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(400);
expect(response.payload.isBoom).toEqual(true);
}
@@ -161,14 +161,14 @@ describe('PATCH comment', () => {
body: requestAttributes,
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
caseCommentSavedObject: mockCaseComments,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(400);
expect(response.payload.isBoom).toEqual(true);
}
@@ -190,14 +190,14 @@ describe('PATCH comment', () => {
},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
caseCommentSavedObject: mockCaseComments,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(400);
expect(response.payload.isBoom).toEqual(true);
}
@@ -219,14 +219,14 @@ describe('PATCH comment', () => {
},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
caseCommentSavedObject: mockCaseComments,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(400);
expect(response.payload.isBoom).toEqual(true);
expect(response.payload.message).toEqual('You cannot change the type of the comment.');
@@ -247,14 +247,14 @@ describe('PATCH comment', () => {
},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
caseCommentSavedObject: mockCaseComments,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(409);
});
@@ -273,14 +273,14 @@ describe('PATCH comment', () => {
},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
caseCommentSavedObject: mockCaseComments,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(404);
expect(response.payload.isBoom).toEqual(true);
});
diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts
index 54699415cd984..0ab038a62ac77 100644
--- a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts
@@ -43,14 +43,14 @@ describe('POST comment', () => {
},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
caseCommentSavedObject: mockCaseComments,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(200);
expect(response.payload.comments[response.payload.comments.length - 1].id).toEqual(
'mock-comment'
@@ -71,14 +71,14 @@ describe('POST comment', () => {
},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
caseCommentSavedObject: mockCaseComments,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(200);
expect(response.payload.comments[response.payload.comments.length - 1].id).toEqual(
'mock-comment'
@@ -95,14 +95,14 @@ describe('POST comment', () => {
body: {},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
caseCommentSavedObject: mockCaseComments,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(400);
expect(response.payload.isBoom).toEqual(true);
});
@@ -124,14 +124,14 @@ describe('POST comment', () => {
body: requestAttributes,
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
caseCommentSavedObject: mockCaseComments,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(400);
expect(response.payload.isBoom).toEqual(true);
}
@@ -152,14 +152,14 @@ describe('POST comment', () => {
},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
caseCommentSavedObject: mockCaseComments,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(400);
expect(response.payload.isBoom).toEqual(true);
}
@@ -183,14 +183,14 @@ describe('POST comment', () => {
body: requestAttributes,
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
caseCommentSavedObject: mockCaseComments,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(400);
expect(response.payload.isBoom).toEqual(true);
}
@@ -212,14 +212,14 @@ describe('POST comment', () => {
},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
caseCommentSavedObject: mockCaseComments,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(400);
expect(response.payload.isBoom).toEqual(true);
}
@@ -238,14 +238,14 @@ describe('POST comment', () => {
},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
caseCommentSavedObject: mockCaseComments,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(404);
expect(response.payload.isBoom).toEqual(true);
});
@@ -262,14 +262,14 @@ describe('POST comment', () => {
},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
caseCommentSavedObject: mockCaseComments,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(400);
expect(response.payload.isBoom).toEqual(true);
});
@@ -289,7 +289,7 @@ describe('POST comment', () => {
},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
caseCommentSavedObject: mockCaseComments,
@@ -297,7 +297,7 @@ describe('POST comment', () => {
true
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(200);
expect(response.payload.comments[response.payload.comments.length - 1]).toEqual({
comment: 'Wow, good luck catching that bad meanie!',
diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts
index ddcbb3522f986..ff4216a05ae58 100644
--- a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts
@@ -34,7 +34,7 @@ describe('GET configuration', () => {
method: 'get',
});
- const context = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseConfigureSavedObject: mockCaseConfigure,
caseMappingsSavedObject: mockCaseMappings,
@@ -57,7 +57,7 @@ describe('GET configuration', () => {
method: 'get',
});
- const context = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseConfigureSavedObject: [{ ...mockCaseConfigure[0], version: undefined }],
caseMappingsSavedObject: mockCaseMappings,
@@ -98,7 +98,7 @@ describe('GET configuration', () => {
method: 'get',
});
- const context = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseConfigureSavedObject: [],
})
@@ -116,7 +116,7 @@ describe('GET configuration', () => {
method: 'get',
});
- const context = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseConfigureSavedObject: [{ ...mockCaseConfigure[0], id: 'throw-error-find' }],
})
@@ -133,7 +133,7 @@ describe('GET configuration', () => {
method: 'get',
});
- const context = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseConfigureSavedObject: mockCaseConfigure,
caseMappingsSavedObject: [],
diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts
index 0f74b7291dd81..17972e129a825 100644
--- a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts
@@ -33,9 +33,9 @@ export function initGetCaseConfigure({ caseConfigureService, router }: RouteDeps
throw Boom.badRequest('RouteHandlerContext is not registered for cases');
}
const caseClient = context.case.getCaseClient();
- const actionsClient = await context.actions?.getActionsClient();
+ const actionsClient = context.actions?.getActionsClient();
if (actionsClient == null) {
- throw Boom.notFound('Action client have not been found');
+ throw Boom.notFound('Action client not found');
}
try {
mappings = await caseClient.getMappings({
diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts
index 1e37918d7766a..3fa0fe2f83f79 100644
--- a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts
@@ -32,7 +32,7 @@ describe('GET connectors', () => {
method: 'get',
});
- const context = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseConfigureSavedObject: mockCaseConfigure,
caseMappingsSavedObject: mockCaseMappings,
@@ -54,7 +54,7 @@ describe('GET connectors', () => {
method: 'get',
});
- const context = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseConfigureSavedObject: mockCaseConfigure,
caseMappingsSavedObject: mockCaseMappings,
@@ -106,6 +106,16 @@ describe('GET connectors', () => {
isPreconfigured: false,
referencedByCount: 0,
},
+ {
+ id: 'for-mock-case-id-3',
+ actionTypeId: '.jira',
+ name: 'For mock case id 3',
+ config: {
+ apiUrl: 'https://elastic.jira.com',
+ },
+ isPreconfigured: false,
+ referencedByCount: 0,
+ },
]);
});
@@ -115,7 +125,7 @@ describe('GET connectors', () => {
method: 'get',
});
- const context = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseConfigureSavedObject: mockCaseConfigure,
caseMappingsSavedObject: mockCaseMappings,
diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts
index fb0595f858d4e..0a368e0276bb5 100644
--- a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts
@@ -14,18 +14,15 @@ import { FindActionResult } from '../../../../../../actions/server/types';
import {
CASE_CONFIGURE_CONNECTORS_URL,
- SERVICENOW_ACTION_TYPE_ID,
- JIRA_ACTION_TYPE_ID,
- RESILIENT_ACTION_TYPE_ID,
+ SUPPORTED_CONNECTORS,
} from '../../../../../common/constants';
const isConnectorSupported = (
action: FindActionResult,
actionTypes: Record
): boolean =>
- [SERVICENOW_ACTION_TYPE_ID, JIRA_ACTION_TYPE_ID, RESILIENT_ACTION_TYPE_ID].includes(
- action.actionTypeId
- ) && actionTypes[action.actionTypeId]?.enabledInLicense;
+ SUPPORTED_CONNECTORS.includes(action.actionTypeId) &&
+ actionTypes[action.actionTypeId]?.enabledInLicense;
/*
* Be aware that this api will only return 20 connectors
@@ -39,10 +36,10 @@ export function initCaseConfigureGetActionConnector({ router }: RouteDeps) {
},
async (context, request, response) => {
try {
- const actionsClient = await context.actions?.getActionsClient();
+ const actionsClient = context.actions?.getActionsClient();
if (actionsClient == null) {
- throw Boom.notFound('Action client have not been found');
+ throw Boom.notFound('Action client not found');
}
const actionTypes = (await actionsClient.listTypes()).reduce(
diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/mock.ts b/x-pack/plugins/case/server/routes/api/cases/configure/mock.ts
deleted file mode 100644
index 9959a3e4acee6..0000000000000
--- a/x-pack/plugins/case/server/routes/api/cases/configure/mock.ts
+++ /dev/null
@@ -1,76 +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 {
- ServiceConnectorCaseParams,
- ServiceConnectorCommentParams,
- ConnectorMappingsAttributes,
- ConnectorTypes,
-} from '../../../../../common/api/connectors';
-export const updateUser = {
- updatedAt: '2020-03-13T08:34:53.450Z',
- updatedBy: { fullName: 'Another User', username: 'another' },
-};
-const entity = {
- createdAt: '2020-03-13T08:34:53.450Z',
- createdBy: { fullName: 'Elastic User', username: 'elastic' },
- updatedAt: null,
- updatedBy: null,
-};
-export const comment: ServiceConnectorCommentParams = {
- comment: 'first comment',
- commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631',
- ...entity,
-};
-export const defaultPipes = ['informationCreated'];
-const basicParams = {
- comments: [comment],
- description: 'a description',
- title: 'a title',
- savedObjectId: '1231231231232',
- externalId: null,
-};
-export const params = {
- [ConnectorTypes.jira]: {
- ...basicParams,
- issueType: '10003',
- priority: 'Highest',
- parent: '5002',
- ...entity,
- } as ServiceConnectorCaseParams,
- [ConnectorTypes.resilient]: {
- ...basicParams,
- incidentTypes: ['10003'],
- severityCode: '1',
- ...entity,
- } as ServiceConnectorCaseParams,
- [ConnectorTypes.servicenow]: {
- ...basicParams,
- impact: '3',
- severity: '1',
- urgency: '2',
- ...entity,
- } as ServiceConnectorCaseParams,
- [ConnectorTypes.none]: {},
-};
-export const mappings: ConnectorMappingsAttributes[] = [
- {
- source: 'title',
- target: 'short_description',
- action_type: 'overwrite',
- },
- {
- source: 'description',
- target: 'description',
- action_type: 'append',
- },
- {
- source: 'comments',
- target: 'comments',
- action_type: 'append',
- },
-];
diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.test.ts
index c67a1c064a82f..f43f561e30e10 100644
--- a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.test.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.test.ts
@@ -42,7 +42,7 @@ describe('PATCH configuration', () => {
},
});
- const context = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseConfigureSavedObject: mockCaseConfigure,
caseMappingsSavedObject: mockCaseMappings,
@@ -76,7 +76,7 @@ describe('PATCH configuration', () => {
},
});
- const context = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseConfigureSavedObject: mockCaseConfigure,
caseMappingsSavedObject: mockCaseMappings,
@@ -115,7 +115,7 @@ describe('PATCH configuration', () => {
},
});
- const context = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseConfigureSavedObject: mockCaseConfigure,
caseMappingsSavedObject: mockCaseMappings,
@@ -153,7 +153,7 @@ describe('PATCH configuration', () => {
},
});
- const context = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseConfigureSavedObject: mockCaseConfigure,
caseMappingsSavedObject: [],
@@ -193,7 +193,7 @@ describe('PATCH configuration', () => {
},
});
- const context = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseConfigureSavedObject: [],
})
@@ -215,7 +215,7 @@ describe('PATCH configuration', () => {
},
});
- const context = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseConfigureSavedObject: mockCaseConfigure,
caseMappingsSavedObject: mockCaseMappings,
@@ -243,7 +243,7 @@ describe('PATCH configuration', () => {
},
});
- const context = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseConfigureSavedObject: mockCaseConfigure,
caseMappingsSavedObject: mockCaseMappings,
diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts
index f847c4f776bf0..6925f116136b3 100644
--- a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts
@@ -66,7 +66,7 @@ export function initPatchCaseConfigure({ caseConfigureService, caseService, rout
throw Boom.badRequest('RouteHandlerContext is not registered for cases');
}
const caseClient = context.case.getCaseClient();
- const actionsClient = await context.actions?.getActionsClient();
+ const actionsClient = context.actions?.getActionsClient();
if (actionsClient == null) {
throw Boom.notFound('Action client have not been found');
}
diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.test.ts
index 0a7f3ef488fce..7dcb7d1fa12ca 100644
--- a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.test.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.test.ts
@@ -40,7 +40,7 @@ describe('POST configuration', () => {
body: newConfiguration,
});
- const context = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseConfigureSavedObject: mockCaseConfigure,
caseMappingsSavedObject: mockCaseMappings,
@@ -73,7 +73,7 @@ describe('POST configuration', () => {
body: newConfiguration,
});
- const context = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseConfigureSavedObject: mockCaseConfigure,
caseMappingsSavedObject: [],
@@ -113,7 +113,7 @@ describe('POST configuration', () => {
body: newConfiguration,
});
- const context = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseConfigureSavedObject: mockCaseConfigure,
caseMappingsSavedObject: mockCaseMappings,
@@ -154,7 +154,7 @@ describe('POST configuration', () => {
},
});
- const context = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseConfigureSavedObject: mockCaseConfigure,
caseMappingsSavedObject: mockCaseMappings,
@@ -180,7 +180,7 @@ describe('POST configuration', () => {
},
});
- const context = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseConfigureSavedObject: mockCaseConfigure,
caseMappingsSavedObject: mockCaseMappings,
@@ -206,7 +206,7 @@ describe('POST configuration', () => {
},
});
- const context = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseConfigureSavedObject: mockCaseConfigure,
caseMappingsSavedObject: mockCaseMappings,
@@ -232,7 +232,7 @@ describe('POST configuration', () => {
},
});
- const context = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseConfigureSavedObject: mockCaseConfigure,
caseMappingsSavedObject: mockCaseMappings,
@@ -258,7 +258,7 @@ describe('POST configuration', () => {
},
});
- const context = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseConfigureSavedObject: mockCaseConfigure,
caseMappingsSavedObject: mockCaseMappings,
@@ -282,7 +282,7 @@ describe('POST configuration', () => {
caseMappingsSavedObject: mockCaseMappings,
});
- const context = await createRouteContext(savedObjectRepository);
+ const { context } = await createRouteContext(savedObjectRepository);
const res = await routeHandler(context, req, kibanaResponseFactory);
@@ -302,7 +302,7 @@ describe('POST configuration', () => {
caseMappingsSavedObject: mockCaseMappings,
});
- const context = await createRouteContext(savedObjectRepository);
+ const { context } = await createRouteContext(savedObjectRepository);
const res = await routeHandler(context, req, kibanaResponseFactory);
@@ -325,7 +325,7 @@ describe('POST configuration', () => {
caseMappingsSavedObject: mockCaseMappings,
});
- const context = await createRouteContext(savedObjectRepository);
+ const { context } = await createRouteContext(savedObjectRepository);
const res = await routeHandler(context, req, kibanaResponseFactory);
@@ -341,7 +341,7 @@ describe('POST configuration', () => {
body: newConfiguration,
});
- const context = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseConfigureSavedObject: [{ ...mockCaseConfigure[0], id: 'throw-error-find' }],
})
@@ -359,7 +359,7 @@ describe('POST configuration', () => {
body: newConfiguration,
});
- const context = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseConfigureSavedObject: [{ ...mockCaseConfigure[0], id: 'throw-error-delete' }],
})
@@ -384,7 +384,7 @@ describe('POST configuration', () => {
},
});
- const context = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseConfigureSavedObject: mockCaseConfigure,
caseMappingsSavedObject: mockCaseMappings,
@@ -411,7 +411,7 @@ describe('POST configuration', () => {
},
});
- const context = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseConfigureSavedObject: mockCaseConfigure,
caseMappingsSavedObject: mockCaseMappings,
@@ -437,7 +437,7 @@ describe('POST configuration', () => {
},
});
- const context = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseConfigureSavedObject: mockCaseConfigure,
caseMappingsSavedObject: mockCaseMappings,
@@ -459,7 +459,7 @@ describe('POST configuration', () => {
},
});
- const context = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseConfigureSavedObject: mockCaseConfigure,
caseMappingsSavedObject: mockCaseMappings,
diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts
index 8e5fd95facc3d..0bcf2ac18740f 100644
--- a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts
@@ -39,9 +39,9 @@ export function initPostCaseConfigure({ caseConfigureService, caseService, route
throw Boom.badRequest('RouteHandlerContext is not registered for cases');
}
const caseClient = context.case.getCaseClient();
- const actionsClient = await context.actions?.getActionsClient();
+ const actionsClient = context.actions?.getActionsClient();
if (actionsClient == null) {
- throw Boom.notFound('Action client have not been found');
+ throw Boom.notFound('Action client not found');
}
const client = context.core.savedObjects.client;
const query = pipe(
diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/post_push_to_service.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/post_push_to_service.test.ts
deleted file mode 100644
index e382813dbf0c5..0000000000000
--- a/x-pack/plugins/case/server/routes/api/cases/configure/post_push_to_service.test.ts
+++ /dev/null
@@ -1,106 +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 { kibanaResponseFactory, RequestHandler } from 'src/core/server';
-import { httpServerMock } from 'src/core/server/mocks';
-
-import {
- createMockSavedObjectsRepository,
- createRoute,
- createRouteContext,
- mockCaseMappings,
-} from '../../__fixtures__';
-
-import { initPostPushToService } from './post_push_to_service';
-import { executePushResponse, newPostPushRequest } from '../../__mocks__/request_responses';
-import { CASE_CONFIGURE_PUSH_URL } from '../../../../../common/constants';
-import type { CasesRequestHandlerContext } from '../../../../types';
-
-describe('Post push to service', () => {
- let routeHandler: RequestHandler;
- const req = httpServerMock.createKibanaRequest({
- path: `${CASE_CONFIGURE_PUSH_URL}`,
- method: 'post',
- params: {
- connector_id: '666',
- },
- body: newPostPushRequest,
- });
- let context: CasesRequestHandlerContext;
- beforeAll(async () => {
- routeHandler = await createRoute(initPostPushToService, 'post');
- const spyOnDate = jest.spyOn(global, 'Date') as jest.SpyInstance<{}, []>;
- spyOnDate.mockImplementation(() => ({
- toISOString: jest.fn().mockReturnValue('2020-04-09T09:43:51.778Z'),
- }));
- context = await createRouteContext(
- createMockSavedObjectsRepository({
- caseMappingsSavedObject: mockCaseMappings,
- })
- );
- });
-
- it('Happy path - posts success', async () => {
- const betterContext = ({
- ...context,
- actions: {
- ...context.actions,
- getActionsClient: () => {
- const actions = context!.actions!.getActionsClient();
- return {
- ...actions,
- execute: jest.fn().mockImplementation(({ actionId }) => {
- return {
- status: 'ok',
- data: {
- title: 'RJ2-200',
- id: '10663',
- pushedDate: '2020-12-17T00:32:40.738Z',
- url: 'https://siem-kibana.atlassian.net/browse/RJ2-200',
- comments: [],
- },
- actionId,
- };
- }),
- };
- },
- },
- } as unknown) as CasesRequestHandlerContext;
-
- const res = await routeHandler(betterContext, req, kibanaResponseFactory);
-
- expect(res.status).toEqual(200);
- expect(res.payload).toEqual({
- ...executePushResponse,
- actionId: '666',
- });
- });
- it('Unhappy path - context case missing', async () => {
- const betterContext = ({
- ...context,
- case: null,
- } as unknown) as CasesRequestHandlerContext;
-
- const res = await routeHandler(betterContext, req, kibanaResponseFactory);
- expect(res.status).toEqual(400);
- expect(res.payload.isBoom).toBeTruthy();
- expect(res.payload.output.payload.message).toEqual(
- 'RouteHandlerContext is not registered for cases'
- );
- });
- it('Unhappy path - context actions missing', async () => {
- const betterContext = ({
- ...context,
- actions: null,
- } as unknown) as CasesRequestHandlerContext;
-
- const res = await routeHandler(betterContext, req, kibanaResponseFactory);
- expect(res.status).toEqual(404);
- expect(res.payload.isBoom).toBeTruthy();
- expect(res.payload.output.payload.message).toEqual('Action client have not been found');
- });
-});
diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/post_push_to_service.ts b/x-pack/plugins/case/server/routes/api/cases/configure/post_push_to_service.ts
deleted file mode 100644
index b8ba1a9ccb6ef..0000000000000
--- a/x-pack/plugins/case/server/routes/api/cases/configure/post_push_to_service.ts
+++ /dev/null
@@ -1,81 +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 { pipe } from 'fp-ts/lib/pipeable';
-import { fold } from 'fp-ts/lib/Either';
-import { identity } from 'fp-ts/lib/function';
-import Boom from '@hapi/boom';
-import { RouteDeps } from '../../types';
-import { escapeHatch, wrapError } from '../../utils';
-
-import { CASE_CONFIGURE_PUSH_URL } from '../../../../../common/constants';
-import {
- ConnectorRequestParamsRt,
- PostPushRequestRt,
- throwErrors,
-} from '../../../../../common/api';
-import { mapIncident } from './utils';
-
-export function initPostPushToService({ router }: RouteDeps) {
- router.post(
- {
- path: CASE_CONFIGURE_PUSH_URL,
- validate: {
- params: escapeHatch,
- body: escapeHatch,
- },
- },
- async (context, request, response) => {
- try {
- if (!context.case) {
- throw Boom.badRequest('RouteHandlerContext is not registered for cases');
- }
- const caseClient = context.case.getCaseClient();
- const actionsClient = await context.actions?.getActionsClient();
- if (actionsClient == null) {
- throw Boom.notFound('Action client have not been found');
- }
- const params = pipe(
- ConnectorRequestParamsRt.decode(request.params),
- fold(throwErrors(Boom.badRequest), identity)
- );
- const body = pipe(
- PostPushRequestRt.decode(request.body),
- fold(throwErrors(Boom.badRequest), identity)
- );
-
- const myConnectorMappings = await caseClient.getMappings({
- actionsClient,
- caseClient,
- connectorId: params.connector_id,
- connectorType: body.connector_type,
- });
-
- const res = await mapIncident(
- actionsClient,
- params.connector_id,
- body.connector_type,
- myConnectorMappings,
- body.params
- );
- const pushRes = await actionsClient.execute({
- actionId: params.connector_id,
- params: {
- subAction: 'pushToService',
- subActionParams: res,
- },
- });
-
- return response.ok({
- body: pushRes,
- });
- } catch (error) {
- return response.customError(wrapError(error));
- }
- }
- );
-}
diff --git a/x-pack/plugins/case/server/routes/api/cases/delete_cases.test.ts b/x-pack/plugins/case/server/routes/api/cases/delete_cases.test.ts
index 84e452ea8e871..d588950bec9aa 100644
--- a/x-pack/plugins/case/server/routes/api/cases/delete_cases.test.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/delete_cases.test.ts
@@ -33,14 +33,14 @@ describe('DELETE case', () => {
},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
caseCommentSavedObject: mockCaseComments,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(204);
});
it(`returns an error when thrown from deleteCase service`, async () => {
@@ -52,14 +52,14 @@ describe('DELETE case', () => {
},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
caseCommentSavedObject: mockCaseComments,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(404);
});
it(`returns an error when thrown from getAllCaseComments service`, async () => {
@@ -71,14 +71,14 @@ describe('DELETE case', () => {
},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCasesErrorTriggerData,
caseCommentSavedObject: mockCaseComments,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(400);
});
it(`returns an error when thrown from deleteComment service`, async () => {
@@ -90,14 +90,14 @@ describe('DELETE case', () => {
},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCasesErrorTriggerData,
caseCommentSavedObject: mockCasesErrorTriggerData,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(400);
});
});
diff --git a/x-pack/plugins/case/server/routes/api/cases/find_cases.test.ts b/x-pack/plugins/case/server/routes/api/cases/find_cases.test.ts
index acd7de1e8643e..ca9f731ca5010 100644
--- a/x-pack/plugins/case/server/routes/api/cases/find_cases.test.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/find_cases.test.ts
@@ -30,13 +30,13 @@ describe('FIND all cases', () => {
method: 'get',
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(200);
expect(response.payload.cases).toHaveLength(4);
// mockSavedObjectsRepository do not support filters and returns all cases every time.
@@ -51,13 +51,13 @@ describe('FIND all cases', () => {
method: 'get',
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(200);
expect(response.payload.cases[2].connector.id).toEqual('123');
});
@@ -68,13 +68,13 @@ describe('FIND all cases', () => {
method: 'get',
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: [mockCaseNoConnectorId],
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(200);
expect(response.payload.cases[0].connector.id).toEqual('none');
});
@@ -85,14 +85,14 @@ describe('FIND all cases', () => {
method: 'get',
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: [mockCaseNoConnectorId],
caseConfigureSavedObject: mockCaseConfigure,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(200);
expect(response.payload.cases[0].connector.id).toEqual('none');
});
diff --git a/x-pack/plugins/case/server/routes/api/cases/get_case.test.ts b/x-pack/plugins/case/server/routes/api/cases/get_case.test.ts
index 7aa6f110a0079..968dd0424fe3f 100644
--- a/x-pack/plugins/case/server/routes/api/cases/get_case.test.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/get_case.test.ts
@@ -40,13 +40,13 @@ describe('GET case', () => {
},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
const savedObject = (mockCases.find(
(s) => s.id === 'mock-id-1'
) as unknown) as SavedObject;
@@ -71,13 +71,13 @@ describe('GET case', () => {
},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(404);
expect(response.payload.isBoom).toEqual(true);
@@ -95,14 +95,14 @@ describe('GET case', () => {
},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
caseCommentSavedObject: mockCaseComments,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(200);
expect(response.payload.comments).toHaveLength(5);
@@ -120,13 +120,13 @@ describe('GET case', () => {
},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCasesErrorTriggerData,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(400);
});
@@ -143,13 +143,13 @@ describe('GET case', () => {
},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: [mockCaseNoConnectorId],
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(200);
expect(response.payload.connector).toEqual({
@@ -172,14 +172,14 @@ describe('GET case', () => {
},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: [mockCaseNoConnectorId],
caseConfigureSavedObject: mockCaseConfigure,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(200);
expect(response.payload.connector).toEqual({
@@ -202,14 +202,14 @@ describe('GET case', () => {
},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
caseConfigureSavedObject: mockCaseConfigure,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(200);
expect(response.payload.connector).toEqual({
diff --git a/x-pack/plugins/case/server/routes/api/cases/get_case.ts b/x-pack/plugins/case/server/routes/api/cases/get_case.ts
index f563fc274b18b..55377d93e528d 100644
--- a/x-pack/plugins/case/server/routes/api/cases/get_case.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/get_case.ts
@@ -7,9 +7,8 @@
import { schema } from '@kbn/config-schema';
-import { CaseResponseRt } from '../../../../common/api';
import { RouteDeps } from '../types';
-import { flattenCaseSavedObject, wrapError } from '../utils';
+import { wrapError } from '../utils';
import { CASE_DETAILS_URL } from '../../../../common/constants';
export function initGetCaseApi({ caseConfigureService, caseService, router }: RouteDeps) {
@@ -26,44 +25,17 @@ export function initGetCaseApi({ caseConfigureService, caseService, router }: Ro
},
},
async (context, request, response) => {
- try {
- const client = context.core.savedObjects.client;
- const includeComments = JSON.parse(request.query.includeComments);
-
- const [theCase] = await Promise.all([
- caseService.getCase({
- client,
- caseId: request.params.case_id,
- }),
- ]);
-
- if (!includeComments) {
- return response.ok({
- body: CaseResponseRt.encode(
- flattenCaseSavedObject({
- savedObject: theCase,
- })
- ),
- });
- }
+ if (!context.case) {
+ return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' });
+ }
- const theComments = await caseService.getAllCaseComments({
- client,
- caseId: request.params.case_id,
- options: {
- sortField: 'created_at',
- sortOrder: 'asc',
- },
- });
+ const caseClient = context.case.getCaseClient();
+ const includeComments = JSON.parse(request.query.includeComments);
+ const id = request.params.case_id;
+ try {
return response.ok({
- body: CaseResponseRt.encode(
- flattenCaseSavedObject({
- savedObject: theCase,
- comments: theComments.saved_objects,
- totalComment: theComments.total,
- })
- ),
+ body: await caseClient.get({ id, includeComments }),
});
} catch (error) {
return response.customError(wrapError(error));
diff --git a/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts b/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts
index 95f7e5bb19a01..6d1134b15b65e 100644
--- a/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts
@@ -44,13 +44,13 @@ describe('PATCH cases', () => {
},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(200);
expect(response.payload).toEqual([
{
@@ -97,14 +97,14 @@ describe('PATCH cases', () => {
},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
caseConfigureSavedObject: mockCaseConfigure,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(200);
expect(response.payload).toEqual([
{
@@ -151,13 +151,13 @@ describe('PATCH cases', () => {
},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(200);
expect(response.payload).toEqual([
{
@@ -204,13 +204,13 @@ describe('PATCH cases', () => {
},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: [mockCaseNoConnectorId],
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(200);
expect(response.payload[0].connector.id).toEqual('none');
});
@@ -230,13 +230,13 @@ describe('PATCH cases', () => {
},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(200);
expect(response.payload[0].connector.id).toEqual('123');
});
@@ -261,13 +261,13 @@ describe('PATCH cases', () => {
},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(200);
expect(response.payload[0].connector).toEqual({
id: '456',
@@ -292,13 +292,13 @@ describe('PATCH cases', () => {
},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(409);
});
@@ -317,14 +317,14 @@ describe('PATCH cases', () => {
},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
caseCommentSavedObject: mockCaseComments,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(406);
});
@@ -343,13 +343,13 @@ describe('PATCH cases', () => {
},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(404);
expect(response.payload.isBoom).toEqual(true);
});
diff --git a/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts b/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts
index 997516d2e30b6..292e2c6775a80 100644
--- a/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts
@@ -49,13 +49,13 @@ describe('POST cases', () => {
},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(200);
expect(response.payload.id).toEqual('mock-it');
expect(response.payload.status).toEqual('open');
@@ -88,14 +88,14 @@ describe('POST cases', () => {
},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
caseConfigureSavedObject: mockCaseConfigure,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(200);
expect(response.payload.connector).toEqual({
id: '123',
@@ -121,13 +121,13 @@ describe('POST cases', () => {
},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(400);
});
@@ -146,13 +146,13 @@ describe('POST cases', () => {
},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(400);
expect(response.payload.isBoom).toEqual(true);
});
@@ -179,7 +179,7 @@ describe('POST cases', () => {
},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
caseConfigureSavedObject: mockCaseConfigure,
@@ -187,7 +187,7 @@ describe('POST cases', () => {
true
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(200);
expect(response.payload).toEqual({
closed_at: null,
diff --git a/x-pack/plugins/case/server/routes/api/cases/push_case.test.ts b/x-pack/plugins/case/server/routes/api/cases/push_case.test.ts
index 549195966b2a7..49801ea4e2f3e 100644
--- a/x-pack/plugins/case/server/routes/api/cases/push_case.test.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/push_case.test.ts
@@ -13,63 +13,187 @@ import {
createRoute,
createRouteContext,
mockCases,
+ mockCaseConfigure,
+ mockCaseMappings,
+ mockUserActions,
+ mockCaseComments,
} from '../__fixtures__';
-import { initPushCaseUserActionApi } from './push_case';
-import { CASE_DETAILS_URL } from '../../../../common/constants';
-import { mockCaseConfigure } from '../__fixtures__/mock_saved_objects';
+import { initPushCaseApi } from './push_case';
+import { CasesRequestHandlerContext } from '../../../types';
+import { getCasePushUrl } from '../../../../common/api/helpers';
describe('Push case', () => {
let routeHandler: RequestHandler;
const mockDate = '2019-11-25T21:54:48.952Z';
- const caseExternalServiceRequestBody = {
- connector_id: 'connector_id',
- connector_name: 'connector_name',
- external_id: 'external_id',
- external_title: 'external_title',
- external_url: 'external_url',
- };
+ const caseId = 'mock-id-3';
+ const connectorId = '123';
+ const path = getCasePushUrl(caseId, connectorId);
+
beforeAll(async () => {
- routeHandler = await createRoute(initPushCaseUserActionApi, 'post');
+ routeHandler = await createRoute(initPushCaseApi, 'post');
const spyOnDate = jest.spyOn(global, 'Date') as jest.SpyInstance<{}, []>;
spyOnDate.mockImplementation(() => ({
toISOString: jest.fn().mockReturnValue(mockDate),
}));
});
+
it(`Pushes a case`, async () => {
const request = httpServerMock.createKibanaRequest({
- path: `${CASE_DETAILS_URL}/_push`,
+ path,
+ method: 'post',
+ params: {
+ case_id: caseId,
+ connector_id: connectorId,
+ },
+ body: {},
+ });
+
+ const { context } = await createRouteContext(
+ createMockSavedObjectsRepository({
+ caseSavedObject: mockCases,
+ caseMappingsSavedObject: mockCaseMappings,
+ caseConfigureSavedObject: mockCaseConfigure,
+ caseUserActionsSavedObject: mockUserActions,
+ })
+ );
+
+ const response = await routeHandler(context, request, kibanaResponseFactory);
+ expect(response.status).toEqual(200);
+ expect(response.payload.external_service).toEqual({
+ connector_id: connectorId,
+ connector_name: 'ServiceNow',
+ external_id: '10663',
+ external_title: 'RJ2-200',
+ external_url: 'https://siem-kibana.atlassian.net/browse/RJ2-200',
+ pushed_at: mockDate,
+ pushed_by: {
+ email: 'd00d@awesome.com',
+ full_name: 'Awesome D00d',
+ username: 'awesome',
+ },
+ });
+ });
+
+ it(`Pushes a case with comments`, async () => {
+ const request = httpServerMock.createKibanaRequest({
+ path,
+ method: 'post',
+ params: {
+ case_id: caseId,
+ connector_id: connectorId,
+ },
+ body: {},
+ });
+
+ const { context } = await createRouteContext(
+ createMockSavedObjectsRepository({
+ caseSavedObject: mockCases,
+ caseMappingsSavedObject: mockCaseMappings,
+ caseConfigureSavedObject: mockCaseConfigure,
+ caseUserActionsSavedObject: mockUserActions,
+ caseCommentSavedObject: [mockCaseComments[0]],
+ })
+ );
+
+ const response = await routeHandler(context, request, kibanaResponseFactory);
+ expect(response.status).toEqual(200);
+ expect(response.payload.comments[0].pushed_at).toEqual(mockDate);
+ expect(response.payload.comments[0].pushed_by).toEqual({
+ email: 'd00d@awesome.com',
+ full_name: 'Awesome D00d',
+ username: 'awesome',
+ });
+ });
+
+ it(`Filters comments with type alert correctly`, async () => {
+ const request = httpServerMock.createKibanaRequest({
+ path,
method: 'post',
params: {
- case_id: 'mock-id-3',
+ case_id: caseId,
+ connector_id: connectorId,
},
- body: caseExternalServiceRequestBody,
+ body: {},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
+ caseMappingsSavedObject: mockCaseMappings,
+ caseConfigureSavedObject: mockCaseConfigure,
+ caseUserActionsSavedObject: mockUserActions,
+ caseCommentSavedObject: [mockCaseComments[0], mockCaseComments[3]],
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const caseClient = context.case.getCaseClient();
+ caseClient.getAlerts = jest.fn().mockResolvedValue([]);
+
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(200);
- expect(response.payload.external_service.pushed_at).toEqual(mockDate);
- expect(response.payload.external_service.connector_id).toEqual('connector_id');
- expect(response.payload.closed_at).toEqual(null);
+ expect(caseClient.getAlerts).toHaveBeenCalledWith({ ids: ['test-id'] });
+ });
+
+ it(`Calls execute with correct arguments`, async () => {
+ const request = httpServerMock.createKibanaRequest({
+ path,
+ method: 'post',
+ params: {
+ case_id: caseId,
+ connector_id: 'for-mock-case-id-3',
+ },
+ body: {},
+ });
+
+ const { context } = await createRouteContext(
+ createMockSavedObjectsRepository({
+ caseSavedObject: mockCases,
+ caseMappingsSavedObject: mockCaseMappings,
+ caseConfigureSavedObject: mockCaseConfigure,
+ caseUserActionsSavedObject: mockUserActions,
+ })
+ );
+
+ const actionsClient = context.actions.getActionsClient();
+
+ await routeHandler(context, request, kibanaResponseFactory);
+ expect(actionsClient.execute).toHaveBeenCalledWith({
+ actionId: 'for-mock-case-id-3',
+ params: {
+ subAction: 'pushToService',
+ subActionParams: {
+ incident: {
+ issueType: 'Task',
+ parent: null,
+ priority: 'High',
+ labels: ['LOLBins'],
+ summary: 'Another bad one (created at 2019-11-25T22:32:17.947Z by elastic)',
+ description:
+ 'Oh no, a bad meanie going LOLBins all over the place! (created at 2019-11-25T22:32:17.947Z by elastic)',
+ externalId: null,
+ },
+ comments: [],
+ },
+ },
+ });
});
+
it(`Pushes a case and closes when closure_type: 'close-by-pushing'`, async () => {
const request = httpServerMock.createKibanaRequest({
- path: `${CASE_DETAILS_URL}/_push`,
+ path,
method: 'post',
params: {
- case_id: 'mock-id-3',
+ case_id: caseId,
+ connector_id: connectorId,
},
- body: caseExternalServiceRequestBody,
+ body: {},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
+ caseMappingsSavedObject: mockCaseMappings,
+ caseUserActionsSavedObject: mockUserActions,
caseConfigureSavedObject: [
{
...mockCaseConfigure[0],
@@ -82,30 +206,259 @@ describe('Push case', () => {
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(200);
- expect(response.payload.external_service.pushed_at).toEqual(mockDate);
- expect(response.payload.external_service.connector_id).toEqual('connector_id');
expect(response.payload.closed_at).toEqual(mockDate);
});
- it(`Returns an error if pushCaseUserAction throws`, async () => {
+ it(`post the correct user action`, async () => {
+ const request = httpServerMock.createKibanaRequest({
+ path,
+ method: 'post',
+ params: {
+ case_id: caseId,
+ connector_id: connectorId,
+ },
+ body: {},
+ });
+
+ const { context, services } = await createRouteContext(
+ createMockSavedObjectsRepository({
+ caseSavedObject: mockCases,
+ caseMappingsSavedObject: mockCaseMappings,
+ caseConfigureSavedObject: mockCaseConfigure,
+ caseUserActionsSavedObject: mockUserActions,
+ })
+ );
+
+ services.userActionService.postUserActions = jest.fn();
+ const postUserActions = services.userActionService.postUserActions as jest.Mock;
+
+ const response = await routeHandler(context, request, kibanaResponseFactory);
+ expect(response.status).toEqual(200);
+ expect(postUserActions.mock.calls[0][0].actions[0].attributes).toEqual({
+ action: 'push-to-service',
+ action_at: '2019-11-25T21:54:48.952Z',
+ action_by: {
+ email: 'd00d@awesome.com',
+ full_name: 'Awesome D00d',
+ username: 'awesome',
+ },
+ action_field: ['pushed'],
+ new_value:
+ '{"pushed_at":"2019-11-25T21:54:48.952Z","pushed_by":{"username":"awesome","full_name":"Awesome D00d","email":"d00d@awesome.com"},"connector_id":"123","connector_name":"ServiceNow","external_id":"10663","external_title":"RJ2-200","external_url":"https://siem-kibana.atlassian.net/browse/RJ2-200"}',
+ old_value: null,
+ });
+ });
+
+ it('Unhappy path - case id is missing', async () => {
const request = httpServerMock.createKibanaRequest({
- path: `${CASE_DETAILS_URL}/_push`,
+ path,
method: 'post',
- body: {
- notagoodbody: 'Throw an error',
+ params: {
+ connector_id: connectorId,
+ },
+ body: {},
+ });
+
+ const { context } = await createRouteContext(
+ createMockSavedObjectsRepository({
+ caseSavedObject: mockCases,
+ caseMappingsSavedObject: mockCaseMappings,
+ caseConfigureSavedObject: mockCaseConfigure,
+ caseUserActionsSavedObject: mockUserActions,
+ })
+ );
+
+ const res = await routeHandler(context, request, kibanaResponseFactory);
+ expect(res.status).toEqual(400);
+ });
+
+ it('Unhappy path - connector id is missing', async () => {
+ const request = httpServerMock.createKibanaRequest({
+ path,
+ method: 'post',
+ params: {
+ case_id: caseId,
},
+ body: {},
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
+ caseMappingsSavedObject: mockCaseMappings,
+ caseConfigureSavedObject: mockCaseConfigure,
+ caseUserActionsSavedObject: mockUserActions,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
- expect(response.status).toEqual(400);
- expect(response.payload.isBoom).toEqual(true);
+ const res = await routeHandler(context, request, kibanaResponseFactory);
+ expect(res.status).toEqual(400);
+ });
+
+ it('Unhappy path - case does not exists', async () => {
+ const request = httpServerMock.createKibanaRequest({
+ path,
+ method: 'post',
+ params: {
+ case_id: 'not-exist',
+ connector_id: connectorId,
+ },
+ body: {},
+ });
+
+ const { context } = await createRouteContext(
+ createMockSavedObjectsRepository({
+ caseSavedObject: mockCases,
+ caseMappingsSavedObject: mockCaseMappings,
+ caseConfigureSavedObject: mockCaseConfigure,
+ caseUserActionsSavedObject: mockUserActions,
+ })
+ );
+
+ const res = await routeHandler(context, request, kibanaResponseFactory);
+ expect(res.status).toEqual(404);
+ });
+
+ it('Unhappy path - connector does not exists', async () => {
+ const request = httpServerMock.createKibanaRequest({
+ path,
+ method: 'post',
+ params: {
+ case_id: caseId,
+ connector_id: 'not-exists',
+ },
+ body: {},
+ });
+
+ const { context } = await createRouteContext(
+ createMockSavedObjectsRepository({
+ caseSavedObject: mockCases,
+ caseMappingsSavedObject: mockCaseMappings,
+ caseConfigureSavedObject: mockCaseConfigure,
+ caseUserActionsSavedObject: mockUserActions,
+ })
+ );
+
+ const res = await routeHandler(context, request, kibanaResponseFactory);
+ expect(res.status).toEqual(404);
+ });
+
+ it('Unhappy path - cannot push to a closed case', async () => {
+ const request = httpServerMock.createKibanaRequest({
+ path,
+ method: 'post',
+ params: {
+ case_id: 'mock-id-4',
+ connector_id: connectorId,
+ },
+ body: {},
+ });
+
+ const { context } = await createRouteContext(
+ createMockSavedObjectsRepository({
+ caseSavedObject: mockCases,
+ caseMappingsSavedObject: mockCaseMappings,
+ caseConfigureSavedObject: mockCaseConfigure,
+ caseUserActionsSavedObject: mockUserActions,
+ })
+ );
+
+ const res = await routeHandler(context, request, kibanaResponseFactory);
+ expect(res.status).toEqual(409);
+ expect(res.payload.output.payload.message).toBe(
+ 'This case Another bad one is closed. You can not pushed if the case is closed.'
+ );
+ });
+
+ it('Unhappy path - throws when external service returns an error', async () => {
+ const request = httpServerMock.createKibanaRequest({
+ path,
+ method: 'post',
+ params: {
+ case_id: caseId,
+ connector_id: connectorId,
+ },
+ body: {},
+ });
+
+ const { context } = await createRouteContext(
+ createMockSavedObjectsRepository({
+ caseSavedObject: mockCases,
+ caseMappingsSavedObject: mockCaseMappings,
+ caseConfigureSavedObject: mockCaseConfigure,
+ caseUserActionsSavedObject: mockUserActions,
+ })
+ );
+
+ const actionsClient = context.actions.getActionsClient();
+ (actionsClient.execute as jest.Mock).mockResolvedValue({
+ status: 'error',
+ });
+
+ const res = await routeHandler(context, request, kibanaResponseFactory);
+ expect(res.status).toEqual(424);
+ expect(res.payload.output.payload.message).toBe('Error pushing to service');
+ });
+
+ it('Unhappy path - context case missing', async () => {
+ const request = httpServerMock.createKibanaRequest({
+ path,
+ method: 'post',
+ params: {
+ case_id: caseId,
+ connector_id: connectorId,
+ },
+ body: {},
+ });
+
+ const { context } = await createRouteContext(
+ createMockSavedObjectsRepository({
+ caseSavedObject: mockCases,
+ caseMappingsSavedObject: mockCaseMappings,
+ caseConfigureSavedObject: mockCaseConfigure,
+ caseUserActionsSavedObject: mockUserActions,
+ })
+ );
+
+ const betterContext = ({
+ ...context,
+ case: null,
+ } as unknown) as CasesRequestHandlerContext;
+
+ const res = await routeHandler(betterContext, request, kibanaResponseFactory);
+ expect(res.status).toEqual(400);
+ expect(res.payload).toEqual('RouteHandlerContext is not registered for cases');
+ });
+
+ it('Unhappy path - context actions missing', async () => {
+ const request = httpServerMock.createKibanaRequest({
+ path,
+ method: 'post',
+ params: {
+ case_id: caseId,
+ connector_id: connectorId,
+ },
+ body: {},
+ });
+
+ const { context } = await createRouteContext(
+ createMockSavedObjectsRepository({
+ caseSavedObject: mockCases,
+ caseMappingsSavedObject: mockCaseMappings,
+ caseConfigureSavedObject: mockCaseConfigure,
+ caseUserActionsSavedObject: mockUserActions,
+ })
+ );
+
+ const betterContext = ({
+ ...context,
+ actions: null,
+ } as unknown) as CasesRequestHandlerContext;
+
+ const res = await routeHandler(betterContext, request, kibanaResponseFactory);
+ expect(res.status).toEqual(400);
+ expect(res.payload).toEqual('Action client not found');
});
});
diff --git a/x-pack/plugins/case/server/routes/api/cases/push_case.ts b/x-pack/plugins/case/server/routes/api/cases/push_case.ts
index 218b1f16b9aab..6d670c38bbf85 100644
--- a/x-pack/plugins/case/server/routes/api/cases/push_case.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/push_case.ts
@@ -5,204 +5,51 @@
* 2.0.
*/
-import { schema } from '@kbn/config-schema';
import Boom from '@hapi/boom';
-import isEmpty from 'lodash/isEmpty';
import { pipe } from 'fp-ts/lib/pipeable';
import { fold } from 'fp-ts/lib/Either';
import { identity } from 'fp-ts/lib/function';
-import {
- flattenCaseSavedObject,
- wrapError,
- escapeHatch,
- getCommentContextFromAttributes,
-} from '../utils';
+import { wrapError, escapeHatch } from '../utils';
-import {
- CaseExternalServiceRequestRt,
- CaseResponseRt,
- throwErrors,
- CaseStatuses,
-} from '../../../../common/api';
-import { buildCaseUserActionItem } from '../../../services/user_actions/helpers';
+import { throwErrors, CasePushRequestParamsRt } from '../../../../common/api';
import { RouteDeps } from '../types';
-import { CASE_DETAILS_URL } from '../../../../common/constants';
+import { CASE_PUSH_URL } from '../../../../common/constants';
-export function initPushCaseUserActionApi({
- caseConfigureService,
- caseService,
- router,
- userActionService,
-}: RouteDeps) {
+export function initPushCaseApi({ router }: RouteDeps) {
router.post(
{
- path: `${CASE_DETAILS_URL}/_push`,
+ path: CASE_PUSH_URL,
validate: {
- params: schema.object({
- case_id: schema.string(),
- }),
+ params: escapeHatch,
body: escapeHatch,
},
},
async (context, request, response) => {
- try {
- const client = context.core.savedObjects.client;
- const actionsClient = await context.actions?.getActionsClient();
-
- const caseId = request.params.case_id;
- const query = pipe(
- CaseExternalServiceRequestRt.decode(request.body),
- fold(throwErrors(Boom.badRequest), identity)
- );
-
- if (actionsClient == null) {
- throw Boom.notFound('Action client have not been found');
- }
-
- // eslint-disable-next-line @typescript-eslint/naming-convention
- const { username, full_name, email } = await caseService.getUser({ request, response });
-
- const pushedDate = new Date().toISOString();
-
- const [myCase, myCaseConfigure, totalCommentsFindByCases, connectors] = await Promise.all([
- caseService.getCase({
- client,
- caseId: request.params.case_id,
- }),
- caseConfigureService.find({ client }),
- caseService.getAllCaseComments({
- client,
- caseId,
- options: {
- fields: [],
- page: 1,
- perPage: 1,
- },
- }),
- actionsClient.getAll(),
- ]);
-
- if (myCase.attributes.status === CaseStatuses.closed) {
- throw Boom.conflict(
- `This case ${myCase.attributes.title} is closed. You can not pushed if the case is closed.`
- );
- }
-
- const comments = await caseService.getAllCaseComments({
- client,
- caseId,
- options: {
- fields: [],
- page: 1,
- perPage: totalCommentsFindByCases.total,
- },
- });
-
- const externalService = {
- pushed_at: pushedDate,
- pushed_by: { username, full_name, email },
- ...query,
- };
+ if (!context.case) {
+ return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' });
+ }
- const updateConnector = myCase.attributes.connector;
+ const caseClient = context.case.getCaseClient();
+ const actionsClient = context.actions?.getActionsClient();
- if (
- isEmpty(updateConnector) ||
- (updateConnector != null && updateConnector.id === 'none') ||
- !connectors.some((connector) => connector.id === updateConnector.id)
- ) {
- throw Boom.notFound('Connector not found or set to none');
- }
+ if (actionsClient == null) {
+ return response.badRequest({ body: 'Action client not found' });
+ }
- const [updatedCase, updatedComments] = await Promise.all([
- caseService.patchCase({
- client,
- caseId,
- updatedAttributes: {
- ...(myCaseConfigure.total > 0 &&
- myCaseConfigure.saved_objects[0].attributes.closure_type === 'close-by-pushing'
- ? {
- status: CaseStatuses.closed,
- closed_at: pushedDate,
- closed_by: { email, full_name, username },
- }
- : {}),
- external_service: externalService,
- updated_at: pushedDate,
- updated_by: { username, full_name, email },
- },
- version: myCase.version,
- }),
- caseService.patchComments({
- client,
- comments: comments.saved_objects
- .filter((comment) => comment.attributes.pushed_at == null)
- .map((comment) => ({
- commentId: comment.id,
- updatedAttributes: {
- pushed_at: pushedDate,
- pushed_by: { username, full_name, email },
- },
- version: comment.version,
- })),
- }),
- userActionService.postUserActions({
- client,
- actions: [
- ...(myCaseConfigure.total > 0 &&
- myCaseConfigure.saved_objects[0].attributes.closure_type === 'close-by-pushing'
- ? [
- buildCaseUserActionItem({
- action: 'update',
- actionAt: pushedDate,
- actionBy: { username, full_name, email },
- caseId,
- fields: ['status'],
- newValue: CaseStatuses.closed,
- oldValue: myCase.attributes.status,
- }),
- ]
- : []),
- buildCaseUserActionItem({
- action: 'push-to-service',
- actionAt: pushedDate,
- actionBy: { username, full_name, email },
- caseId,
- fields: ['pushed'],
- newValue: JSON.stringify(externalService),
- }),
- ],
- }),
- ]);
+ try {
+ const params = pipe(
+ CasePushRequestParamsRt.decode(request.params),
+ fold(throwErrors(Boom.badRequest), identity)
+ );
return response.ok({
- body: CaseResponseRt.encode(
- flattenCaseSavedObject({
- savedObject: {
- ...myCase,
- ...updatedCase,
- attributes: { ...myCase.attributes, ...updatedCase?.attributes },
- references: myCase.references,
- },
- comments: comments.saved_objects.map((origComment) => {
- const updatedComment = updatedComments.saved_objects.find(
- (c) => c.id === origComment.id
- );
- return {
- ...origComment,
- ...updatedComment,
- attributes: {
- ...origComment.attributes,
- ...updatedComment?.attributes,
- ...getCommentContextFromAttributes(origComment.attributes),
- },
- version: updatedComment?.version ?? origComment.version,
- references: origComment?.references ?? [],
- };
- }),
- })
- ),
+ body: await caseClient.push({
+ caseClient,
+ actionsClient,
+ caseId: params.case_id,
+ connectorId: params.connector_id,
+ }),
});
} catch (error) {
return response.customError(wrapError(error));
diff --git a/x-pack/plugins/case/server/routes/api/cases/status/get_status.test.ts b/x-pack/plugins/case/server/routes/api/cases/status/get_status.test.ts
index e8761ad69dcca..9644162629f24 100644
--- a/x-pack/plugins/case/server/routes/api/cases/status/get_status.test.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/status/get_status.test.ts
@@ -36,24 +36,24 @@ describe('GET status', () => {
method: 'get',
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
- expect(theContext.core.savedObjects.client.find).toHaveBeenNthCalledWith(1, {
+ const response = await routeHandler(context, request, kibanaResponseFactory);
+ expect(context.core.savedObjects.client.find).toHaveBeenNthCalledWith(1, {
...findArgs,
filter: 'cases.attributes.status: open',
});
- expect(theContext.core.savedObjects.client.find).toHaveBeenNthCalledWith(2, {
+ expect(context.core.savedObjects.client.find).toHaveBeenNthCalledWith(2, {
...findArgs,
filter: 'cases.attributes.status: in-progress',
});
- expect(theContext.core.savedObjects.client.find).toHaveBeenNthCalledWith(3, {
+ expect(context.core.savedObjects.client.find).toHaveBeenNthCalledWith(3, {
...findArgs,
filter: 'cases.attributes.status: closed',
});
@@ -71,13 +71,13 @@ describe('GET status', () => {
method: 'get',
});
- const theContext = await createRouteContext(
+ const { context } = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: [{ ...mockCases[0], id: 'throw-error-find' }],
})
);
- const response = await routeHandler(theContext, request, kibanaResponseFactory);
+ const response = await routeHandler(context, request, kibanaResponseFactory);
expect(response.status).toEqual(404);
});
});
diff --git a/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts b/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts
index 346eec3dde752..06e929cc40e6b 100644
--- a/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts
@@ -7,13 +7,11 @@
import { schema } from '@kbn/config-schema';
-import { CaseUserActionsResponseRt } from '../../../../../common/api';
-import { CASE_SAVED_OBJECT, CASE_COMMENT_SAVED_OBJECT } from '../../../../saved_object_types';
import { RouteDeps } from '../../types';
import { wrapError } from '../../utils';
import { CASE_USER_ACTIONS_URL } from '../../../../../common/constants';
-export function initGetAllUserActionsApi({ userActionService, router }: RouteDeps) {
+export function initGetAllUserActionsApi({ router }: RouteDeps) {
router.get(
{
path: CASE_USER_ACTIONS_URL,
@@ -24,22 +22,16 @@ export function initGetAllUserActionsApi({ userActionService, router }: RouteDep
},
},
async (context, request, response) => {
+ if (!context.case) {
+ return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' });
+ }
+
+ const caseClient = context.case.getCaseClient();
+ const caseId = request.params.case_id;
+
try {
- const client = context.core.savedObjects.client;
- const userActions = await userActionService.getUserActions({
- client,
- caseId: request.params.case_id,
- });
return response.ok({
- body: CaseUserActionsResponseRt.encode(
- userActions.saved_objects.map((ua) => ({
- ...ua.attributes,
- action_id: ua.id,
- case_id: ua.references.find((r) => r.type === CASE_SAVED_OBJECT)?.id ?? '',
- comment_id:
- ua.references.find((r) => r.type === CASE_COMMENT_SAVED_OBJECT)?.id ?? null,
- }))
- ),
+ body: await caseClient.getUserActions({ caseId }),
});
} catch (error) {
return response.customError(wrapError(error));
diff --git a/x-pack/plugins/case/server/routes/api/index.ts b/x-pack/plugins/case/server/routes/api/index.ts
index c399364ea35ec..00660e08bbd83 100644
--- a/x-pack/plugins/case/server/routes/api/index.ts
+++ b/x-pack/plugins/case/server/routes/api/index.ts
@@ -10,7 +10,7 @@ import { initFindCasesApi } from '././cases/find_cases';
import { initGetCaseApi } from './cases/get_case';
import { initPatchCasesApi } from './cases/patch_cases';
import { initPostCaseApi } from './cases/post_case';
-import { initPushCaseUserActionApi } from './cases/push_case';
+import { initPushCaseApi } from './cases/push_case';
import { initGetReportersApi } from './cases/reporters/get_reporters';
import { initGetCasesStatusApi } from './cases/status/get_status';
import { initGetTagsApi } from './cases/tags/get_tags';
@@ -28,7 +28,6 @@ import { initCaseConfigureGetActionConnector } from './cases/configure/get_conne
import { initGetCaseConfigure } from './cases/configure/get_configure';
import { initPatchCaseConfigure } from './cases/configure/patch_configure';
import { initPostCaseConfigure } from './cases/configure/post_configure';
-import { initPostPushToService } from './cases/configure/post_push_to_service';
import { RouteDeps } from './types';
@@ -39,7 +38,7 @@ export function initCaseApi(deps: RouteDeps) {
initGetCaseApi(deps);
initPatchCasesApi(deps);
initPostCaseApi(deps);
- initPushCaseUserActionApi(deps);
+ initPushCaseApi(deps);
initGetAllUserActionsApi(deps);
// Comments
initDeleteCommentApi(deps);
@@ -54,7 +53,6 @@ export function initCaseApi(deps: RouteDeps) {
initGetCaseConfigure(deps);
initPatchCaseConfigure(deps);
initPostCaseConfigure(deps);
- initPostPushToService(deps);
// Reporters
initGetReportersApi(deps);
// Status
diff --git a/x-pack/plugins/case/server/routes/api/utils.ts b/x-pack/plugins/case/server/routes/api/utils.ts
index b7e556daffbd9..e2751c05d880a 100644
--- a/x-pack/plugins/case/server/routes/api/utils.ts
+++ b/x-pack/plugins/case/server/routes/api/utils.ts
@@ -191,11 +191,11 @@ export const sortToSnake = (sortField: string): SortFieldCase => {
export const escapeHatch = schema.object({}, { unknowns: 'allow' });
-const isUserContext = (context: CommentRequest): context is CommentRequestUserType => {
+export const isUserContext = (context: CommentRequest): context is CommentRequestUserType => {
return context.type === CommentType.user;
};
-const isAlertContext = (context: CommentRequest): context is CommentRequestAlertType => {
+export const isAlertContext = (context: CommentRequest): context is CommentRequestAlertType => {
return context.type === CommentType.alert;
};
@@ -206,17 +206,3 @@ export const decodeComment = (comment: CommentRequest) => {
pipe(excess(ContextTypeAlertRt).decode(comment), fold(throwErrors(badRequest), identity));
}
};
-
-export const getCommentContextFromAttributes = (
- attributes: CommentAttributes
-): CommentRequestUserType | CommentRequestAlertType =>
- isUserContext(attributes)
- ? {
- type: CommentType.user,
- comment: attributes.comment,
- }
- : {
- type: CommentType.alert,
- alertId: attributes.alertId,
- index: attributes.index,
- };
diff --git a/x-pack/plugins/case/server/services/alerts/index.ts b/x-pack/plugins/case/server/services/alerts/index.ts
index 4f0d415f23b50..2776d6b40761e 100644
--- a/x-pack/plugins/case/server/services/alerts/index.ts
+++ b/x-pack/plugins/case/server/services/alerts/index.ts
@@ -19,6 +19,24 @@ interface UpdateAlertsStatusArgs {
index: string;
}
+interface GetAlertsArgs {
+ request: KibanaRequest;
+ ids: string[];
+ index: string;
+}
+
+interface Alert {
+ _id: string;
+ _index: string;
+ _source: Record;
+}
+
+interface AlertsResponse {
+ hits: {
+ hits: Alert[];
+ };
+}
+
export class AlertService {
private isInitialized = false;
private esClient?: IClusterClient;
@@ -55,4 +73,30 @@ export class AlertService {
return result;
}
+
+ public async getAlerts({ request, ids, index }: GetAlertsArgs): Promise {
+ if (!this.isInitialized) {
+ throw new Error('AlertService not initialized');
+ }
+
+ // The above check makes sure that esClient is defined.
+ const result = await this.esClient!.asScoped(request).asCurrentUser.search({
+ index,
+ body: {
+ query: {
+ bool: {
+ filter: {
+ bool: {
+ should: ids.map((_id) => ({ match: { _id } })),
+ minimum_should_match: 1,
+ },
+ },
+ },
+ },
+ },
+ ignore_unavailable: true,
+ });
+
+ return result.body;
+ }
}
diff --git a/x-pack/plugins/case/server/services/mocks.ts b/x-pack/plugins/case/server/services/mocks.ts
index 7c8b44b297362..0b3615793ef85 100644
--- a/x-pack/plugins/case/server/services/mocks.ts
+++ b/x-pack/plugins/case/server/services/mocks.ts
@@ -59,4 +59,5 @@ export const createUserActionServiceMock = (): CaseUserActionServiceMock => ({
export const createAlertServiceMock = (): AlertServiceMock => ({
initialize: jest.fn(),
updateAlertsStatus: jest.fn(),
+ getAlerts: jest.fn(),
});
diff --git a/x-pack/plugins/cross_cluster_replication/tsconfig.json b/x-pack/plugins/cross_cluster_replication/tsconfig.json
new file mode 100644
index 0000000000000..9c7590b9c2553
--- /dev/null
+++ b/x-pack/plugins/cross_cluster_replication/tsconfig.json
@@ -0,0 +1,31 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "compilerOptions": {
+ "composite": true,
+ "outDir": "./target/types",
+ "emitDeclarationOnly": true,
+ "declaration": true,
+ "declarationMap": true
+ },
+ "include": [
+ "common/**/*",
+ "public/**/*",
+ "server/**/*",
+ ],
+ "references": [
+ { "path": "../../../src/core/tsconfig.json" },
+ // required plugins
+ { "path": "../../../src/plugins/home/tsconfig.json" },
+ { "path": "../licensing/tsconfig.json" },
+ { "path": "../../../src/plugins/management/tsconfig.json" },
+ { "path": "../remote_clusters/tsconfig.json" },
+ { "path": "../index_management/tsconfig.json" },
+ { "path": "../features/tsconfig.json" },
+ // optional plugins
+ { "path": "../../../src/plugins/usage_collection/tsconfig.json" },
+ // required bundles
+ { "path": "../../../src/plugins/kibana_react/tsconfig.json" },
+ { "path": "../../../src/plugins/es_ui_shared/tsconfig.json" },
+ { "path": "../../../src/plugins/data/tsconfig.json" },
+ ]
+}
diff --git a/x-pack/plugins/data_enhanced/public/plugin.ts b/x-pack/plugins/data_enhanced/public/plugin.ts
index b7d7b7c0e20d1..0a116545e6e36 100644
--- a/x-pack/plugins/data_enhanced/public/plugin.ts
+++ b/x-pack/plugins/data_enhanced/public/plugin.ts
@@ -6,6 +6,7 @@
*/
import React from 'react';
+import moment from 'moment';
import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public';
import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../../src/plugins/data/public';
import { BfetchPublicSetup } from '../../../../src/plugins/bfetch/public';
@@ -86,6 +87,9 @@ export class DataEnhancedPlugin
application: core.application,
timeFilter: plugins.data.query.timefilter.timefilter,
storage: this.storage,
+ disableSaveAfterSessionCompletesTimeout: moment
+ .duration(this.config.search.sessions.notTouchedTimeout)
+ .asMilliseconds(),
})
)
),
diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/extend_button.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/extend_button.tsx
index 1e2678912ce99..381c44b1bf7be 100644
--- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/extend_button.tsx
+++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/extend_button.tsx
@@ -38,7 +38,7 @@ const ExtendConfirm = ({
defaultMessage: 'Extend search session expiration',
});
const confirm = i18n.translate('xpack.data.mgmt.searchSessions.extendModal.extendButton', {
- defaultMessage: 'Extend',
+ defaultMessage: 'Extend expiration',
});
const extend = i18n.translate('xpack.data.mgmt.searchSessions.extendModal.dontExtendButton', {
defaultMessage: 'Cancel',
@@ -58,7 +58,9 @@ const ExtendConfirm = ({
onCancel={onConfirmDismiss}
onConfirm={async () => {
setIsLoading(true);
- await api.sendExtend(id, `${extendByDuration.asMilliseconds()}ms`);
+ await api.sendExtend(id, `${newExpiration.toISOString()}`);
+ setIsLoading(false);
+ onConfirmDismiss();
onActionComplete();
}}
confirmButtonText={confirm}
diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/get_action.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/get_action.tsx
index edc5037f1dbec..1a2b2cfb4ecec 100644
--- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/get_action.tsx
+++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/get_action.tsx
@@ -12,15 +12,24 @@ import { SearchSessionsMgmtAPI } from '../../lib/api';
import { UISession } from '../../types';
import { DeleteButton } from './delete_button';
import { ExtendButton } from './extend_button';
+import { InspectButton } from './inspect_button';
import { ACTION, OnActionComplete } from './types';
export const getAction = (
api: SearchSessionsMgmtAPI,
actionType: string,
- { id, name, expires }: UISession,
+ uiSession: UISession,
onActionComplete: OnActionComplete
): IClickActionDescriptor | null => {
+ const { id, name, expires } = uiSession;
switch (actionType) {
+ case ACTION.INSPECT:
+ return {
+ iconType: 'document',
+ textColor: 'default',
+ label: ,
+ };
+
case ACTION.DELETE:
return {
iconType: 'crossInACircleFilled',
diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/inspect_button.scss b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/inspect_button.scss
new file mode 100644
index 0000000000000..a43bb65927ed4
--- /dev/null
+++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/inspect_button.scss
@@ -0,0 +1,6 @@
+.searchSessionsFlyout .euiFlyoutBody__overflowContent {
+ height: 100%;
+ > div {
+ height: 100%;
+ }
+}
\ No newline at end of file
diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/inspect_button.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/inspect_button.tsx
new file mode 100644
index 0000000000000..86dca64909b55
--- /dev/null
+++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/inspect_button.tsx
@@ -0,0 +1,134 @@
+/*
+ * 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 {
+ EuiFlyout,
+ EuiFlyoutBody,
+ EuiFlyoutHeader,
+ EuiPortal,
+ EuiSpacer,
+ EuiText,
+ EuiTitle,
+} from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import React, { Component, Fragment } from 'react';
+import { UISession } from '../../types';
+import { TableText } from '..';
+import { CodeEditor } from '../../../../../../../../src/plugins/kibana_react/public';
+import './inspect_button.scss';
+
+interface Props {
+ searchSession: UISession;
+}
+
+interface State {
+ isFlyoutVisible: boolean;
+}
+
+export class InspectButton extends Component {
+ constructor(props: Props) {
+ super(props);
+
+ this.state = {
+ isFlyoutVisible: false,
+ };
+
+ this.closeFlyout = this.closeFlyout.bind(this);
+ this.showFlyout = this.showFlyout.bind(this);
+ }
+
+ public renderInfo() {
+ return (
+
+