[Security Solution][Case] ServiceNow SIR Connector (#88655)
Co-authored-by: Xavier Mouligneau <189600+XavierM@users.noreply.github.com>
This commit is contained in:
parent
7b5d62fd55
commit
a0d4b04155
|
@ -16,6 +16,7 @@ describe('api', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
externalService = externalServiceMock.create();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('create incident', () => {
|
||||
|
@ -26,6 +27,7 @@ describe('api', () => {
|
|||
params,
|
||||
secrets: {},
|
||||
logger: mockedLogger,
|
||||
commentFieldKey: 'comments',
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
|
@ -57,6 +59,7 @@ describe('api', () => {
|
|||
params,
|
||||
secrets: {},
|
||||
logger: mockedLogger,
|
||||
commentFieldKey: 'comments',
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
|
@ -77,6 +80,7 @@ describe('api', () => {
|
|||
params,
|
||||
secrets: { username: 'elastic', password: 'elastic' },
|
||||
logger: mockedLogger,
|
||||
commentFieldKey: 'comments',
|
||||
});
|
||||
|
||||
expect(externalService.createIncident).toHaveBeenCalledWith({
|
||||
|
@ -99,6 +103,7 @@ describe('api', () => {
|
|||
params,
|
||||
secrets: {},
|
||||
logger: mockedLogger,
|
||||
commentFieldKey: 'comments',
|
||||
});
|
||||
expect(externalService.updateIncident).toHaveBeenCalledTimes(2);
|
||||
expect(externalService.updateIncident).toHaveBeenNthCalledWith(1, {
|
||||
|
@ -125,6 +130,41 @@ describe('api', () => {
|
|||
incidentId: 'incident-1',
|
||||
});
|
||||
});
|
||||
|
||||
test('it post comments to different comment field key', async () => {
|
||||
const params = { ...apiParams, incident: { ...apiParams.incident, externalId: null } };
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
params,
|
||||
secrets: {},
|
||||
logger: mockedLogger,
|
||||
commentFieldKey: 'work_notes',
|
||||
});
|
||||
expect(externalService.updateIncident).toHaveBeenCalledTimes(2);
|
||||
expect(externalService.updateIncident).toHaveBeenNthCalledWith(1, {
|
||||
incident: {
|
||||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
work_notes: 'A comment',
|
||||
description: 'Incident description',
|
||||
short_description: 'Incident title',
|
||||
},
|
||||
incidentId: 'incident-1',
|
||||
});
|
||||
|
||||
expect(externalService.updateIncident).toHaveBeenNthCalledWith(2, {
|
||||
incident: {
|
||||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
work_notes: 'Another comment',
|
||||
description: 'Incident description',
|
||||
short_description: 'Incident title',
|
||||
},
|
||||
incidentId: 'incident-1',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('update incident', () => {
|
||||
|
@ -134,6 +174,7 @@ describe('api', () => {
|
|||
params: apiParams,
|
||||
secrets: {},
|
||||
logger: mockedLogger,
|
||||
commentFieldKey: 'comments',
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
|
@ -161,6 +202,7 @@ describe('api', () => {
|
|||
params,
|
||||
secrets: {},
|
||||
logger: mockedLogger,
|
||||
commentFieldKey: 'comments',
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
|
@ -178,6 +220,7 @@ describe('api', () => {
|
|||
params,
|
||||
secrets: {},
|
||||
logger: mockedLogger,
|
||||
commentFieldKey: 'comments',
|
||||
});
|
||||
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
|
@ -200,6 +243,7 @@ describe('api', () => {
|
|||
params,
|
||||
secrets: {},
|
||||
logger: mockedLogger,
|
||||
commentFieldKey: 'comments',
|
||||
});
|
||||
expect(externalService.updateIncident).toHaveBeenCalledTimes(3);
|
||||
expect(externalService.updateIncident).toHaveBeenNthCalledWith(1, {
|
||||
|
@ -225,6 +269,40 @@ describe('api', () => {
|
|||
incidentId: 'incident-2',
|
||||
});
|
||||
});
|
||||
|
||||
test('it post comments to different comment field key', async () => {
|
||||
const params = { ...apiParams };
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
params,
|
||||
secrets: {},
|
||||
logger: mockedLogger,
|
||||
commentFieldKey: 'work_notes',
|
||||
});
|
||||
expect(externalService.updateIncident).toHaveBeenCalledTimes(3);
|
||||
expect(externalService.updateIncident).toHaveBeenNthCalledWith(1, {
|
||||
incident: {
|
||||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
description: 'Incident description',
|
||||
short_description: 'Incident title',
|
||||
},
|
||||
incidentId: 'incident-3',
|
||||
});
|
||||
|
||||
expect(externalService.updateIncident).toHaveBeenNthCalledWith(2, {
|
||||
incident: {
|
||||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
work_notes: 'A comment',
|
||||
description: 'Incident description',
|
||||
short_description: 'Incident title',
|
||||
},
|
||||
incidentId: 'incident-2',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFields', () => {
|
||||
|
|
|
@ -25,6 +25,7 @@ const pushToServiceHandler = async ({
|
|||
externalService,
|
||||
params,
|
||||
secrets,
|
||||
commentFieldKey,
|
||||
}: PushToServiceApiHandlerArgs): Promise<PushToServiceResponse> => {
|
||||
const { comments } = params;
|
||||
let res: PushToServiceResponse;
|
||||
|
@ -53,7 +54,7 @@ const pushToServiceHandler = async ({
|
|||
incidentId: res.id,
|
||||
incident: {
|
||||
...incident,
|
||||
comments: currentComment.comment,
|
||||
[commentFieldKey]: currentComment.comment,
|
||||
},
|
||||
});
|
||||
res.comments = [
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* 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 { actionsMock } from '../../mocks';
|
||||
import { createActionTypeRegistry } from '../index.test';
|
||||
import {
|
||||
ServiceNowPublicConfigurationType,
|
||||
ServiceNowSecretConfigurationType,
|
||||
ExecutorParams,
|
||||
PushToServiceResponse,
|
||||
} from './types';
|
||||
import {
|
||||
ServiceNowActionType,
|
||||
ServiceNowITSMActionTypeId,
|
||||
ServiceNowSIRActionTypeId,
|
||||
ServiceNowActionTypeExecutorOptions,
|
||||
} from '.';
|
||||
import { api } from './api';
|
||||
|
||||
jest.mock('./api', () => ({
|
||||
api: {
|
||||
getChoices: jest.fn(),
|
||||
getFields: jest.fn(),
|
||||
getIncident: jest.fn(),
|
||||
handshake: jest.fn(),
|
||||
pushToService: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const services = actionsMock.createServices();
|
||||
|
||||
describe('ServiceNow', () => {
|
||||
const config = { apiUrl: 'https://instance.com' };
|
||||
const secrets = { username: 'username', password: 'password' };
|
||||
const params = {
|
||||
subAction: 'pushToService',
|
||||
subActionParams: {
|
||||
incident: {
|
||||
short_description: 'An incident',
|
||||
description: 'This is serious',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
(api.pushToService as jest.Mock).mockResolvedValue({ id: 'some-id' });
|
||||
});
|
||||
|
||||
describe('ServiceNow ITSM', () => {
|
||||
let actionType: ServiceNowActionType;
|
||||
|
||||
beforeAll(() => {
|
||||
const { actionTypeRegistry } = createActionTypeRegistry();
|
||||
actionType = actionTypeRegistry.get<
|
||||
ServiceNowPublicConfigurationType,
|
||||
ServiceNowSecretConfigurationType,
|
||||
ExecutorParams,
|
||||
PushToServiceResponse | {}
|
||||
>(ServiceNowITSMActionTypeId);
|
||||
});
|
||||
|
||||
describe('execute()', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('it pass the correct comment field key', async () => {
|
||||
const actionId = 'some-action-id';
|
||||
const executorOptions = ({
|
||||
actionId,
|
||||
config,
|
||||
secrets,
|
||||
params,
|
||||
services,
|
||||
} as unknown) as ServiceNowActionTypeExecutorOptions;
|
||||
await actionType.executor(executorOptions);
|
||||
expect((api.pushToService as jest.Mock).mock.calls[0][0].commentFieldKey).toBe('comments');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ServiceNow SIR', () => {
|
||||
let actionType: ServiceNowActionType;
|
||||
|
||||
beforeAll(() => {
|
||||
const { actionTypeRegistry } = createActionTypeRegistry();
|
||||
actionType = actionTypeRegistry.get<
|
||||
ServiceNowPublicConfigurationType,
|
||||
ServiceNowSecretConfigurationType,
|
||||
ExecutorParams,
|
||||
PushToServiceResponse | {}
|
||||
>(ServiceNowSIRActionTypeId);
|
||||
});
|
||||
|
||||
describe('execute()', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('it pass the correct comment field key', async () => {
|
||||
const actionId = 'some-action-id';
|
||||
const executorOptions = ({
|
||||
actionId,
|
||||
config,
|
||||
secrets,
|
||||
params,
|
||||
services,
|
||||
} as unknown) as ServiceNowActionTypeExecutorOptions;
|
||||
await actionType.executor(executorOptions);
|
||||
expect((api.pushToService as jest.Mock).mock.calls[0][0].commentFieldKey).toBe(
|
||||
'work_notes'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -47,15 +47,21 @@ const serviceNowSIRTable = 'sn_si_incident';
|
|||
export const ServiceNowITSMActionTypeId = '.servicenow';
|
||||
export const ServiceNowSIRActionTypeId = '.servicenow-sir';
|
||||
|
||||
// action type definition
|
||||
export function getServiceNowITSMActionType(
|
||||
params: GetActionTypeParams
|
||||
): ActionType<
|
||||
export type ServiceNowActionType = ActionType<
|
||||
ServiceNowPublicConfigurationType,
|
||||
ServiceNowSecretConfigurationType,
|
||||
ExecutorParams,
|
||||
PushToServiceResponse | {}
|
||||
> {
|
||||
>;
|
||||
|
||||
export type ServiceNowActionTypeExecutorOptions = ActionTypeExecutorOptions<
|
||||
ServiceNowPublicConfigurationType,
|
||||
ServiceNowSecretConfigurationType,
|
||||
ExecutorParams
|
||||
>;
|
||||
|
||||
// action type definition
|
||||
export function getServiceNowITSMActionType(params: GetActionTypeParams): ServiceNowActionType {
|
||||
const { logger, configurationUtilities } = params;
|
||||
return {
|
||||
id: ServiceNowITSMActionTypeId,
|
||||
|
@ -74,14 +80,7 @@ export function getServiceNowITSMActionType(
|
|||
};
|
||||
}
|
||||
|
||||
export function getServiceNowSIRActionType(
|
||||
params: GetActionTypeParams
|
||||
): ActionType<
|
||||
ServiceNowPublicConfigurationType,
|
||||
ServiceNowSecretConfigurationType,
|
||||
ExecutorParams,
|
||||
PushToServiceResponse | {}
|
||||
> {
|
||||
export function getServiceNowSIRActionType(params: GetActionTypeParams): ServiceNowActionType {
|
||||
const { logger, configurationUtilities } = params;
|
||||
return {
|
||||
id: ServiceNowSIRActionTypeId,
|
||||
|
@ -96,7 +95,12 @@ export function getServiceNowSIRActionType(
|
|||
}),
|
||||
params: ExecutorParamsSchemaSIR,
|
||||
},
|
||||
executor: curry(executor)({ logger, configurationUtilities, table: serviceNowSIRTable }),
|
||||
executor: curry(executor)({
|
||||
logger,
|
||||
configurationUtilities,
|
||||
table: serviceNowSIRTable,
|
||||
commentFieldKey: 'work_notes',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -107,12 +111,14 @@ async function executor(
|
|||
logger,
|
||||
configurationUtilities,
|
||||
table,
|
||||
}: { logger: Logger; configurationUtilities: ActionsConfigurationUtilities; table: string },
|
||||
execOptions: ActionTypeExecutorOptions<
|
||||
ServiceNowPublicConfigurationType,
|
||||
ServiceNowSecretConfigurationType,
|
||||
ExecutorParams
|
||||
>
|
||||
commentFieldKey = 'comments',
|
||||
}: {
|
||||
logger: Logger;
|
||||
configurationUtilities: ActionsConfigurationUtilities;
|
||||
table: string;
|
||||
commentFieldKey?: string;
|
||||
},
|
||||
execOptions: ServiceNowActionTypeExecutorOptions
|
||||
): Promise<ActionTypeExecutorResult<ServiceNowExecutorResultData | {}>> {
|
||||
const { actionId, config, params, secrets } = execOptions;
|
||||
const { subAction, subActionParams } = params;
|
||||
|
@ -147,6 +153,7 @@ async function executor(
|
|||
params: pushToServiceParams,
|
||||
secrets,
|
||||
logger,
|
||||
commentFieldKey,
|
||||
});
|
||||
|
||||
logger.debug(`response push to service for incident id: ${data.id}`);
|
||||
|
|
|
@ -16,7 +16,7 @@ export const SERVICENOW_ITSM = i18n.translate('xpack.actions.builtin.serviceNowI
|
|||
});
|
||||
|
||||
export const SERVICENOW_SIR = i18n.translate('xpack.actions.builtin.serviceNowSIRTitle', {
|
||||
defaultMessage: 'ServiceNow SIR',
|
||||
defaultMessage: 'ServiceNow SecOps',
|
||||
});
|
||||
|
||||
export const ALLOWED_HOSTS_ERROR = (message: string) =>
|
||||
|
|
|
@ -121,6 +121,7 @@ export interface PushToServiceApiHandlerArgs extends ExternalServiceApiHandlerAr
|
|||
params: PushToServiceApiParams;
|
||||
secrets: Record<string, unknown>;
|
||||
logger: Logger;
|
||||
commentFieldKey: string;
|
||||
}
|
||||
|
||||
export interface GetIncidentApiHandlerArgs extends ExternalServiceApiHandlerArgs {
|
||||
|
|
|
@ -80,8 +80,6 @@ export const CasePostRequestRt = rt.type({
|
|||
settings: SettingsRt,
|
||||
});
|
||||
|
||||
export const CaseExternalServiceRequestRt = CaseExternalServiceBasicRt;
|
||||
|
||||
export const CasesFindRequestRt = rt.partial({
|
||||
tags: rt.union([rt.array(rt.string), rt.string]),
|
||||
status: CaseStatusRt,
|
||||
|
@ -126,6 +124,31 @@ export const CasePatchRequestRt = rt.intersection([
|
|||
export const CasesPatchRequestRt = rt.type({ cases: rt.array(CasePatchRequestRt) });
|
||||
export const CasesResponseRt = rt.array(CaseResponseRt);
|
||||
|
||||
export const CasePushRequestParamsRt = rt.type({
|
||||
case_id: rt.string,
|
||||
connector_id: rt.string,
|
||||
});
|
||||
|
||||
export const ExternalServiceResponseRt = 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 CaseAttributes = rt.TypeOf<typeof CaseAttributesRt>;
|
||||
export type CasePostRequest = rt.TypeOf<typeof CasePostRequestRt>;
|
||||
export type CaseResponse = rt.TypeOf<typeof CaseResponseRt>;
|
||||
|
@ -133,8 +156,8 @@ export type CasesResponse = rt.TypeOf<typeof CasesResponseRt>;
|
|||
export type CasesFindResponse = rt.TypeOf<typeof CasesFindResponseRt>;
|
||||
export type CasePatchRequest = rt.TypeOf<typeof CasePatchRequestRt>;
|
||||
export type CasesPatchRequest = rt.TypeOf<typeof CasesPatchRequestRt>;
|
||||
export type CaseExternalServiceRequest = rt.TypeOf<typeof CaseExternalServiceRequestRt>;
|
||||
export type CaseFullExternalService = rt.TypeOf<typeof CaseFullExternalServiceRt>;
|
||||
export type ExternalServiceResponse = rt.TypeOf<typeof ExternalServiceResponseRt>;
|
||||
|
||||
export type ESCaseAttributes = Omit<CaseAttributes, 'connector'> & { connector: ESCaseConnector };
|
||||
export type ESCasePatchRequest = Omit<CasePatchRequest, 'connector'> & {
|
||||
|
|
|
@ -45,6 +45,14 @@ export const CommentResponseRt = rt.intersection([
|
|||
}),
|
||||
]);
|
||||
|
||||
export const CommentResponseTypeAlertsRt = rt.intersection([
|
||||
AttributesTypeAlertsRt,
|
||||
rt.type({
|
||||
id: rt.string,
|
||||
version: rt.string,
|
||||
}),
|
||||
]);
|
||||
|
||||
export const AllCommentsResponseRT = rt.array(CommentResponseRt);
|
||||
|
||||
export const CommentPatchRequestRt = rt.intersection([
|
||||
|
@ -84,6 +92,7 @@ export const AllCommentsResponseRt = rt.array(CommentResponseRt);
|
|||
export type CommentAttributes = rt.TypeOf<typeof CommentAttributesRt>;
|
||||
export type CommentRequest = rt.TypeOf<typeof CommentRequestRt>;
|
||||
export type CommentResponse = rt.TypeOf<typeof CommentResponseRt>;
|
||||
export type CommentResponseAlertsType = rt.TypeOf<typeof CommentResponseTypeAlertsRt>;
|
||||
export type AllCommentsResponse = rt.TypeOf<typeof AllCommentsResponseRt>;
|
||||
export type CommentsResponse = rt.TypeOf<typeof CommentsResponseRt>;
|
||||
export type CommentPatchRequest = rt.TypeOf<typeof CommentPatchRequestRt>;
|
||||
|
|
|
@ -7,13 +7,9 @@
|
|||
|
||||
import * as rt from 'io-ts';
|
||||
|
||||
import { ActionResult, ActionType } from '../../../../actions/common';
|
||||
import { UserRT } from '../user';
|
||||
import { CaseConnectorRt, ConnectorMappingsRt, ESCaseConnector } from '../connectors';
|
||||
|
||||
export type ActionConnector = ActionResult;
|
||||
export type ActionTypeConnector = ActionType;
|
||||
|
||||
// TODO: we will need to add this type rt.literal('close-by-third-party')
|
||||
const ClosureTypeRT = rt.union([rt.literal('close-by-user'), rt.literal('close-by-pushing')]);
|
||||
|
||||
|
|
|
@ -7,25 +7,34 @@
|
|||
|
||||
import * as rt from 'io-ts';
|
||||
|
||||
import { ActionResult, ActionType } from '../../../../actions/common';
|
||||
import { JiraFieldsRT } from './jira';
|
||||
import { ResilientFieldsRT } from './resilient';
|
||||
import { ServiceNowFieldsRT } from './servicenow';
|
||||
import { ServiceNowITSMFieldsRT } from './servicenow_itsm';
|
||||
import { ServiceNowSIRFieldsRT } from './servicenow_sir';
|
||||
|
||||
export * from './jira';
|
||||
export * from './servicenow';
|
||||
export * from './servicenow_itsm';
|
||||
export * from './servicenow_sir';
|
||||
export * from './resilient';
|
||||
export * from './mappings';
|
||||
|
||||
export type ActionConnector = ActionResult;
|
||||
export type ActionTypeConnector = ActionType;
|
||||
|
||||
export const ConnectorFieldsRt = rt.union([
|
||||
JiraFieldsRT,
|
||||
ResilientFieldsRT,
|
||||
ServiceNowFieldsRT,
|
||||
ServiceNowITSMFieldsRT,
|
||||
ServiceNowSIRFieldsRT,
|
||||
rt.null,
|
||||
]);
|
||||
|
||||
export enum ConnectorTypes {
|
||||
jira = '.jira',
|
||||
resilient = '.resilient',
|
||||
servicenow = '.servicenow',
|
||||
serviceNowITSM = '.servicenow',
|
||||
serviceNowSIR = '.servicenow-sir',
|
||||
none = '.none',
|
||||
}
|
||||
|
||||
|
@ -39,9 +48,14 @@ const ConnectorResillientTypeFieldsRt = rt.type({
|
|||
fields: rt.union([ResilientFieldsRT, rt.null]),
|
||||
});
|
||||
|
||||
const ConnectorServiceNowTypeFieldsRt = rt.type({
|
||||
type: rt.literal(ConnectorTypes.servicenow),
|
||||
fields: rt.union([ServiceNowFieldsRT, rt.null]),
|
||||
const ConnectorServiceNowITSMTypeFieldsRt = rt.type({
|
||||
type: rt.literal(ConnectorTypes.serviceNowITSM),
|
||||
fields: rt.union([ServiceNowITSMFieldsRT, rt.null]),
|
||||
});
|
||||
|
||||
const ConnectorServiceNowSIRTypeFieldsRt = rt.type({
|
||||
type: rt.literal(ConnectorTypes.serviceNowSIR),
|
||||
fields: rt.union([ServiceNowSIRFieldsRT, rt.null]),
|
||||
});
|
||||
|
||||
const ConnectorNoneTypeFieldsRt = rt.type({
|
||||
|
@ -52,7 +66,8 @@ const ConnectorNoneTypeFieldsRt = rt.type({
|
|||
export const ConnectorTypeFieldsRt = rt.union([
|
||||
ConnectorJiraTypeFieldsRt,
|
||||
ConnectorResillientTypeFieldsRt,
|
||||
ConnectorServiceNowTypeFieldsRt,
|
||||
ConnectorServiceNowITSMTypeFieldsRt,
|
||||
ConnectorServiceNowSIRTypeFieldsRt,
|
||||
ConnectorNoneTypeFieldsRt,
|
||||
]);
|
||||
|
||||
|
@ -66,6 +81,12 @@ export const CaseConnectorRt = rt.intersection([
|
|||
|
||||
export type CaseConnector = rt.TypeOf<typeof CaseConnectorRt>;
|
||||
export type ConnectorTypeFields = rt.TypeOf<typeof ConnectorTypeFieldsRt>;
|
||||
export type ConnectorJiraTypeFields = rt.TypeOf<typeof ConnectorJiraTypeFieldsRt>;
|
||||
export type ConnectorResillientTypeFields = rt.TypeOf<typeof ConnectorResillientTypeFieldsRt>;
|
||||
export type ConnectorServiceNowITSMTypeFields = rt.TypeOf<
|
||||
typeof ConnectorServiceNowITSMTypeFieldsRt
|
||||
>;
|
||||
export type ConnectorServiceNowSIRTypeFields = rt.TypeOf<typeof ConnectorServiceNowSIRTypeFieldsRt>;
|
||||
|
||||
// we need to change these types back and forth for storing in ES (arrays overwrite, objects merge)
|
||||
export type ConnectorFields = rt.TypeOf<typeof ConnectorFieldsRt>;
|
||||
|
|
|
@ -5,42 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
/* eslint-disable @kbn/eslint/no-restricted-paths */
|
||||
|
||||
import * as rt from 'io-ts';
|
||||
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,
|
||||
ServiceNowITSMIncident,
|
||||
} from '../../../../actions/server/builtin_action_types/servicenow/types';
|
||||
import { ResilientFieldsRT } from './resilient';
|
||||
import { ServiceNowFieldsRT } from './servicenow';
|
||||
import { JiraFieldsRT } from './jira';
|
||||
|
||||
// Formerly imported from security_solution
|
||||
export interface ElasticUser {
|
||||
readonly email?: string | null;
|
||||
readonly fullName?: string | null;
|
||||
readonly username?: string | null;
|
||||
}
|
||||
|
||||
export {
|
||||
JiraPushToServiceApiParams,
|
||||
ResilientPushToServiceApiParams,
|
||||
ServiceNowITSMPushToServiceApiParams,
|
||||
};
|
||||
export type Incident = JiraIncident | ResilientIncident | ServiceNowITSMIncident;
|
||||
export type PushToServiceApiParams =
|
||||
| JiraPushToServiceApiParams
|
||||
| ResilientPushToServiceApiParams
|
||||
| ServiceNowITSMPushToServiceApiParams;
|
||||
|
||||
const ActionTypeRT = rt.union([
|
||||
rt.literal('append'),
|
||||
|
@ -52,6 +17,7 @@ const CaseFieldRT = rt.union([
|
|||
rt.literal('description'),
|
||||
rt.literal('comments'),
|
||||
]);
|
||||
|
||||
const ThirdPartyFieldRT = rt.union([rt.string, rt.literal('not_mapped')]);
|
||||
export type ActionType = rt.TypeOf<typeof ActionTypeRT>;
|
||||
export type CaseField = rt.TypeOf<typeof CaseFieldRT>;
|
||||
|
@ -62,9 +28,11 @@ export const ConnectorMappingsAttributesRT = rt.type({
|
|||
source: CaseFieldRT,
|
||||
target: ThirdPartyFieldRT,
|
||||
});
|
||||
|
||||
export const ConnectorMappingsRt = rt.type({
|
||||
mappings: rt.array(ConnectorMappingsAttributesRT),
|
||||
});
|
||||
|
||||
export type ConnectorMappingsAttributes = rt.TypeOf<typeof ConnectorMappingsAttributesRT>;
|
||||
export type ConnectorMappings = rt.TypeOf<typeof ConnectorMappingsRt>;
|
||||
|
||||
|
@ -76,125 +44,12 @@ const ConnectorFieldRt = rt.type({
|
|||
required: rt.boolean,
|
||||
type: FieldTypeRT,
|
||||
});
|
||||
|
||||
export type ConnectorField = rt.TypeOf<typeof ConnectorFieldRt>;
|
||||
export const ConnectorRequestParamsRt = rt.type({
|
||||
connector_id: rt.string,
|
||||
});
|
||||
export const GetFieldsRequestQueryRt = rt.type({
|
||||
connector_type: rt.string,
|
||||
});
|
||||
|
||||
const GetFieldsResponseRt = rt.type({
|
||||
defaultMappings: rt.array(ConnectorMappingsAttributesRT),
|
||||
fields: rt.array(ConnectorFieldRt),
|
||||
});
|
||||
|
||||
export type GetFieldsResponse = rt.TypeOf<typeof GetFieldsResponseRt>;
|
||||
|
||||
export type ExternalServiceParams = Record<string, unknown>;
|
||||
|
||||
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<P, S> {
|
||||
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<typeof ServiceConnectorBasicCaseParamsRt>;
|
||||
export type ServiceConnectorCaseParams = rt.TypeOf<typeof ServiceConnectorCaseParamsRt>;
|
||||
export type ServiceConnectorCaseResponse = rt.TypeOf<typeof ServiceConnectorCaseResponseRt>;
|
||||
export type ServiceConnectorCommentParams = rt.TypeOf<typeof ServiceConnectorCommentParamsRt>;
|
||||
|
||||
export const PostPushRequestRt = rt.type({
|
||||
connector_type: rt.string,
|
||||
params: ServiceConnectorCaseParamsRt,
|
||||
});
|
||||
|
||||
export type PostPushRequest = rt.TypeOf<typeof PostPushRequestRt>;
|
||||
|
||||
export interface SimpleComment {
|
||||
comment: string;
|
||||
commentId: string;
|
||||
}
|
||||
|
||||
export interface MapIncident {
|
||||
incident: ExternalServiceParams;
|
||||
comments: SimpleComment[];
|
||||
}
|
||||
|
|
|
@ -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<typeof ServiceNowFieldsRT>;
|
||||
export type ServiceNowITSMFieldsType = rt.TypeOf<typeof ServiceNowITSMFieldsRT>;
|
20
x-pack/plugins/case/common/api/connectors/servicenow_sir.ts
Normal file
20
x-pack/plugins/case/common/api/connectors/servicenow_sir.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import * as rt from 'io-ts';
|
||||
|
||||
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<typeof ServiceNowSIRFieldsRT>;
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
];
|
||||
|
|
31
x-pack/plugins/case/server/client/alerts/get.ts
Normal file
31
x-pack/plugins/case/server/client/alerts/get.ts
Normal file
|
@ -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<CaseClientGetAlertsResponse> => {
|
||||
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,
|
||||
}));
|
||||
};
|
19
x-pack/plugins/case/server/client/alerts/types.ts
Normal file
19
x-pack/plugins/case/server/client/alerts/types.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
interface Alert {
|
||||
id: string;
|
||||
index: string;
|
||||
destination?: {
|
||||
ip: string;
|
||||
};
|
||||
source?: {
|
||||
ip: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type CaseClientGetAlertsResponse = Alert[];
|
45
x-pack/plugins/case/server/client/cases/get.ts
Normal file
45
x-pack/plugins/case/server/client/cases/get.ts
Normal file
|
@ -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<CaseResponse> => {
|
||||
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,
|
||||
})
|
||||
);
|
||||
};
|
191
x-pack/plugins/case/server/client/cases/mock.ts
Normal file
191
x-pack/plugins/case/server/client/cases/mock.ts
Normal file
|
@ -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',
|
||||
},
|
||||
];
|
266
x-pack/plugins/case/server/client/cases/push.ts
Normal file
266
x-pack/plugins/case/server/client/cases/push.ts
Normal file
|
@ -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<CaseResponse> => {
|
||||
/* 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<ESCaseAttributes>;
|
||||
let updatedComments: SavedObjectsBulkUpdateResponse<CommentAttributes>;
|
||||
|
||||
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 ?? [],
|
||||
};
|
||||
}),
|
||||
})
|
||||
);
|
||||
};
|
81
x-pack/plugins/case/server/client/cases/types.ts
Normal file
81
x-pack/plugins/case/server/client/cases/types.ts
Normal file
|
@ -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<string, unknown>;
|
||||
|
||||
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<P, S> {
|
||||
currentIncident?: S;
|
||||
fields: PipedField[];
|
||||
params: P;
|
||||
}
|
||||
|
||||
export interface ExternalServiceComment {
|
||||
comment: string;
|
||||
commentId: string;
|
||||
}
|
||||
|
||||
export interface MapIncident {
|
||||
incident: ExternalServiceParams;
|
||||
comments: ExternalServiceComment[];
|
||||
}
|
|
@ -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 {
|
||||
mapIncident,
|
||||
comment as commentObj,
|
||||
mappings,
|
||||
defaultPipes,
|
||||
basicParams,
|
||||
userActions,
|
||||
commentAlert,
|
||||
} from './mock';
|
||||
|
||||
import {
|
||||
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<ServiceConnectorCaseParams, ExternalServiceParams, Incident>({
|
||||
params: serviceNowParams,
|
||||
const res = transformFields<BasicParams, ExternalServiceParams, Incident>({
|
||||
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<ServiceConnectorCaseParams, ExternalServiceParams, Incident>({
|
||||
const res = transformFields<BasicParams, ExternalServiceParams, Incident>({
|
||||
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<ServiceConnectorCaseParams, ExternalServiceParams, Incident>({
|
||||
params: serviceNowParams,
|
||||
const res = transformFields<BasicParams, ExternalServiceParams, Incident>({
|
||||
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<ServiceConnectorCaseParams, ExternalServiceParams, Incident>({
|
||||
const res = transformFields<BasicParams, ExternalServiceParams, Incident>({
|
||||
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<ServiceConnectorCaseParams, ExternalServiceParams, Incident>({
|
||||
const res = transformFields<BasicParams, ExternalServiceParams, Incident>({
|
||||
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',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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<MapIncident> => {
|
||||
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<MapIncident> => {
|
||||
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<PushToServiceApiParams['incident']> = service.incident;
|
||||
|
||||
const externalServiceFields = externalServiceFormatters[connector.actionTypeId].format(
|
||||
theCase,
|
||||
alerts
|
||||
);
|
||||
let incident: Partial<PushToServiceApiParams['incident']> = { ...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<BasicParams, ExternalServiceParams, Incident>({
|
||||
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<PushToServiceApiParams['incident']> } | 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<string, Transformer> = {
|
||||
informationCreated: ({ value, date, user, ...rest }: TransformerArgs): TransformerArgs => ({
|
||||
value: `${value} ${FIELD_INFORMATION('create', date, user)}`,
|
||||
|
@ -178,6 +227,7 @@ export const transformers: Record<string, Transformer> = {
|
|||
...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: '',
|
||||
};
|
||||
}
|
||||
};
|
|
@ -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 = {
|
||||
|
|
|
@ -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 };
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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<CaseClient>;
|
||||
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<AlertServiceContract>;
|
||||
};
|
||||
}> => {
|
||||
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,
|
||||
|
|
|
@ -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<CaseResponse>;
|
||||
create: (args: CaseClientCreate) => Promise<CaseResponse>;
|
||||
get: (args: CaseClientGet) => Promise<CaseResponse>;
|
||||
getAlerts: (args: CaseClientGetAlerts) => Promise<CaseClientGetAlertsResponse>;
|
||||
getFields: (args: ConfigureFields) => Promise<GetFieldsResponse>;
|
||||
getMappings: (args: MappingsClient) => Promise<ConnectorMappingsAttributes[]>;
|
||||
getUserActions: (args: CaseClientGetUserActions) => Promise<CaseUserActionsResponse>;
|
||||
push: (args: CaseClientPush) => Promise<CaseResponse>;
|
||||
update: (args: CaseClientUpdate) => Promise<CasesResponse>;
|
||||
updateAlertsStatus: (args: CaseClientUpdateAlertsStatus) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface MappingsClient {
|
||||
actionsClient: ActionsClient;
|
||||
caseClient: CaseClient;
|
||||
connectorId: string;
|
||||
connectorType: string;
|
||||
}
|
||||
export type CaseClientFactoryMethod = (
|
||||
factoryArgs: CaseClientFactoryArguments
|
||||
) => (methodArgs: any) => Promise<any>;
|
||||
|
||||
export type CaseClientMethods = keyof CaseClient;
|
||||
|
||||
export type CaseClientFactoryMethods = {
|
||||
[K in CaseClientMethods]: CaseClientFactoryMethod;
|
||||
};
|
||||
|
|
31
x-pack/plugins/case/server/client/user_actions/get.ts
Normal file
31
x-pack/plugins/case/server/client/user_actions/get.ts
Normal file
|
@ -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<CaseUserActionsResponse> => {
|
||||
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,
|
||||
}))
|
||||
);
|
||||
};
|
|
@ -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,
|
||||
|
|
|
@ -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<Config, Secrets, Params, ExecutorResultData>
|
||||
): 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,
|
||||
};
|
||||
|
|
|
@ -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'] });
|
||||
});
|
||||
});
|
|
@ -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<ExternalServiceParams>['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<ExternalServiceParams> = {
|
||||
format,
|
||||
};
|
|
@ -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 });
|
||||
});
|
||||
});
|
|
@ -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<ResilientFieldsType>['format'] = (theCase) => {
|
||||
const { incidentTypes = null, severityCode = null } =
|
||||
(theCase.connector.fields as ConnectorResillientTypeFields['fields']) ?? {};
|
||||
return { incidentTypes, severityCode };
|
||||
};
|
||||
|
||||
export const resilientExternalServiceFormatter: ExternalServiceFormatter<ResilientFieldsType> = {
|
||||
format,
|
||||
};
|
|
@ -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<ServiceNowITSMFieldsType>['format'] = (theCase) => {
|
||||
const { severity = null, urgency = null, impact = null } =
|
||||
(theCase.connector.fields as ConnectorServiceNowITSMTypeFields['fields']) ?? {};
|
||||
return { severity, urgency, impact };
|
||||
};
|
||||
|
||||
export const serviceNowITSMExternalServiceFormatter: ExternalServiceFormatter<ServiceNowITSMFieldsType> = {
|
||||
format,
|
||||
};
|
|
@ -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 });
|
||||
});
|
||||
});
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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<ExternalServiceParams>['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<SirFieldKey, Set<string>> = {
|
||||
dest_ip: new Set(),
|
||||
source_ip: new Set(),
|
||||
malware_hash: new Set(),
|
||||
malware_url: new Set(),
|
||||
};
|
||||
|
||||
let sirFields: Record<SirFieldKey, string | null> = {
|
||||
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<Record<SirFieldKey, string | null>>((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<ExternalServiceParams> = {
|
||||
format,
|
||||
};
|
54
x-pack/plugins/case/server/connectors/types.ts
Normal file
54
x-pack/plugins/case/server/connectors/types.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { 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<Config, Secrets, Params, ExecutorResultData>
|
||||
): void;
|
||||
}
|
||||
|
||||
export type FormatterConnectorTypes = Exclude<ConnectorTypes, ConnectorTypes.none>;
|
||||
|
||||
export interface ExternalServiceFormatter<TExternalServiceParams = {}> {
|
||||
format: (theCase: CaseResponse, alerts: CaseClientGetAlertsResponse) => TExternalServiceParams;
|
||||
}
|
||||
|
||||
export type ExternalServiceFormatterMapper = {
|
||||
[x in FormatterConnectorTypes]: ExternalServiceFormatter;
|
||||
};
|
|
@ -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<CasesRequestHandlerContext, 'case'> => {
|
||||
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,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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<SavedObjectsBulkUpdateObject<unknown>>) => {
|
||||
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,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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<SavedObject<ESCaseAttributes>> = [
|
||||
|
@ -424,3 +428,44 @@ export const mockCaseMappings: Array<SavedObject<ConnectorMappings>> = [
|
|||
references: [],
|
||||
},
|
||||
];
|
||||
|
||||
export const mockUserActions: Array<SavedObject<CaseUserActionAttributes>> = [
|
||||
{
|
||||
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: [],
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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 } };
|
||||
};
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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!',
|
||||
|
|
|
@ -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: [],
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<string, ActionType>
|
||||
): 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(
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
];
|
|
@ -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,
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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<any, any, any>;
|
||||
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');
|
||||
});
|
||||
});
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
|
|
|
@ -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<ESCaseAttributes>;
|
||||
|
@ -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({
|
||||
|
|
|
@ -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) => {
|
||||
if (!context.case) {
|
||||
return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' });
|
||||
}
|
||||
|
||||
const caseClient = context.case.getCaseClient();
|
||||
const includeComments = JSON.parse(request.query.includeComments);
|
||||
const id = request.params.case_id;
|
||||
|
||||
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,
|
||||
})
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
const theComments = await caseService.getAllCaseComments({
|
||||
client,
|
||||
caseId: request.params.case_id,
|
||||
options: {
|
||||
sortField: 'created_at',
|
||||
sortOrder: 'asc',
|
||||
},
|
||||
});
|
||||
|
||||
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));
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<any, any, any>;
|
||||
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: '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,
|
||||
})
|
||||
);
|
||||
|
||||
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(null);
|
||||
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 and closes when closure_type: 'close-by-pushing'`, async () => {
|
||||
|
||||
it(`Pushes a case with comments`, 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,
|
||||
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: caseId,
|
||||
connector_id: connectorId,
|
||||
},
|
||||
body: {},
|
||||
});
|
||||
|
||||
const { context } = await createRouteContext(
|
||||
createMockSavedObjectsRepository({
|
||||
caseSavedObject: mockCases,
|
||||
caseMappingsSavedObject: mockCaseMappings,
|
||||
caseConfigureSavedObject: mockCaseConfigure,
|
||||
caseUserActionsSavedObject: mockUserActions,
|
||||
caseCommentSavedObject: [mockCaseComments[0], mockCaseComments[3]],
|
||||
})
|
||||
);
|
||||
|
||||
const caseClient = context.case.getCaseClient();
|
||||
caseClient.getAlerts = jest.fn().mockResolvedValue([]);
|
||||
|
||||
const response = await routeHandler(context, request, kibanaResponseFactory);
|
||||
expect(response.status).toEqual(200);
|
||||
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,
|
||||
method: 'post',
|
||||
params: {
|
||||
case_id: caseId,
|
||||
connector_id: connectorId,
|
||||
},
|
||||
body: {},
|
||||
});
|
||||
|
||||
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: `${CASE_DETAILS_URL}/_push`,
|
||||
path,
|
||||
method: 'post',
|
||||
body: {
|
||||
notagoodbody: 'Throw an error',
|
||||
params: {
|
||||
case_id: caseId,
|
||||
connector_id: connectorId,
|
||||
},
|
||||
body: {},
|
||||
});
|
||||
|
||||
const theContext = await createRouteContext(
|
||||
const { context, services } = 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);
|
||||
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,
|
||||
method: 'post',
|
||||
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 { 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 - 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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
if (!context.case) {
|
||||
return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' });
|
||||
}
|
||||
|
||||
const caseId = request.params.case_id;
|
||||
const query = pipe(
|
||||
CaseExternalServiceRequestRt.decode(request.body),
|
||||
const caseClient = context.case.getCaseClient();
|
||||
const actionsClient = context.actions?.getActionsClient();
|
||||
|
||||
if (actionsClient == null) {
|
||||
return response.badRequest({ body: 'Action client not found' });
|
||||
}
|
||||
|
||||
try {
|
||||
const params = pipe(
|
||||
CasePushRequestParamsRt.decode(request.params),
|
||||
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,
|
||||
};
|
||||
|
||||
const updateConnector = myCase.attributes.connector;
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
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),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
]);
|
||||
|
||||
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));
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -19,6 +19,24 @@ interface UpdateAlertsStatusArgs {
|
|||
index: string;
|
||||
}
|
||||
|
||||
interface GetAlertsArgs {
|
||||
request: KibanaRequest;
|
||||
ids: string[];
|
||||
index: string;
|
||||
}
|
||||
|
||||
interface Alert {
|
||||
_id: string;
|
||||
_index: string;
|
||||
_source: Record<string, unknown>;
|
||||
}
|
||||
|
||||
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<AlertsResponse> {
|
||||
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<AlertsResponse>({
|
||||
index,
|
||||
body: {
|
||||
query: {
|
||||
bool: {
|
||||
filter: {
|
||||
bool: {
|
||||
should: ids.map((_id) => ({ match: { _id } })),
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ignore_unavailable: true,
|
||||
});
|
||||
|
||||
return result.body;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,4 +59,5 @@ export const createUserActionServiceMock = (): CaseUserActionServiceMock => ({
|
|||
export const createAlertServiceMock = (): AlertServiceMock => ({
|
||||
initialize: jest.fn(),
|
||||
updateAlertsStatus: jest.fn(),
|
||||
getAlerts: jest.fn(),
|
||||
});
|
||||
|
|
|
@ -31,6 +31,13 @@ describe('Cases connector incident fields', () => {
|
|||
beforeEach(() => {
|
||||
cleanKibana();
|
||||
cy.intercept('GET', '/api/cases/configure/connectors/_find', mockConnectorsResponse);
|
||||
cy.intercept('POST', `/api/actions/action/${connectorIds.sn}/_execute`, (req) => {
|
||||
const response =
|
||||
req.body.params.subAction === 'getChoices'
|
||||
? executeResponses.servicenow.choices
|
||||
: { status: 'ok', data: [] };
|
||||
req.reply(response);
|
||||
});
|
||||
cy.intercept('POST', `/api/actions/action/${connectorIds.jira}/_execute`, (req) => {
|
||||
const response =
|
||||
req.body.params.subAction === 'issueTypes'
|
||||
|
|
|
@ -113,6 +113,77 @@ export const mockConnectorsResponse = [
|
|||
},
|
||||
];
|
||||
export const executeResponses = {
|
||||
servicenow: {
|
||||
choices: {
|
||||
status: 'ok',
|
||||
data: [
|
||||
{
|
||||
dependent_value: '',
|
||||
label: 'Priviledge Escalation',
|
||||
value: 'Priviledge Escalation',
|
||||
element: 'category',
|
||||
},
|
||||
{
|
||||
dependent_value: '',
|
||||
label: 'Criminal activity/investigation',
|
||||
value: 'Criminal activity/investigation',
|
||||
element: 'category',
|
||||
},
|
||||
{
|
||||
dependent_value: '',
|
||||
label: 'Denial of Service',
|
||||
value: 'Denial of Service',
|
||||
element: 'category',
|
||||
},
|
||||
{
|
||||
dependent_value: 'Denial of Service',
|
||||
label: 'Inbound or outbound',
|
||||
value: '12',
|
||||
element: 'subcategory',
|
||||
},
|
||||
{
|
||||
dependent_value: 'Denial of Service',
|
||||
label: 'Single or distributed (DoS or DDoS)',
|
||||
value: '26',
|
||||
element: 'subcategory',
|
||||
},
|
||||
{
|
||||
dependent_value: 'Denial of Service',
|
||||
label: 'Inbound DDos',
|
||||
value: 'inbound_ddos',
|
||||
element: 'subcategory',
|
||||
},
|
||||
...['severity', 'urgency', 'impact', 'priority']
|
||||
.map((element) => [
|
||||
{
|
||||
dependent_value: '',
|
||||
label: '1 - Critical',
|
||||
value: '1',
|
||||
element,
|
||||
},
|
||||
{
|
||||
dependent_value: '',
|
||||
label: '2 - High',
|
||||
value: '2',
|
||||
element,
|
||||
},
|
||||
{
|
||||
dependent_value: '',
|
||||
label: '3 - Moderate',
|
||||
value: '3',
|
||||
element,
|
||||
},
|
||||
{
|
||||
dependent_value: '',
|
||||
label: '4 - Low',
|
||||
value: '4',
|
||||
element,
|
||||
},
|
||||
])
|
||||
.flat(),
|
||||
],
|
||||
},
|
||||
},
|
||||
jira: {
|
||||
issueTypes: {
|
||||
status: 'ok',
|
||||
|
|
|
@ -30,9 +30,9 @@ export const CASE_DETAILS_USER_ACTION_DESCRIPTION_USERNAME =
|
|||
|
||||
export const CASE_DETAILS_USERNAMES = '[data-test-subj="case-view-username"]';
|
||||
|
||||
export const CONNECTOR_CARD_DETAILS = '[data-test-subj="settings-connector-card"]';
|
||||
export const CONNECTOR_CARD_DETAILS = '[data-test-subj="connector-card"]';
|
||||
|
||||
export const CONNECTOR_TITLE = '[data-test-subj="settings-connector-card"] span.euiTitle';
|
||||
export const CONNECTOR_TITLE = '[data-test-subj="connector-card"] span.euiTitle';
|
||||
|
||||
export const DELETE_CASE_BTN = '[data-test-subj="property-actions-trash"]';
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { connectorIds } from '../objects/case';
|
||||
|
||||
export const CONNECTOR_RESILIENT = `[data-test-subj="connector-settings-resilient"]`;
|
||||
export const CONNECTOR_RESILIENT = `[data-test-subj="connector-fields-resilient"]`;
|
||||
|
||||
export const CONNECTOR_SELECTOR = '[data-test-subj="dropdown-connectors"]';
|
||||
|
||||
|
|
|
@ -107,7 +107,7 @@ describe('CaseView ', () => {
|
|||
const fetchCaseUserActions = jest.fn();
|
||||
const fetchCase = jest.fn();
|
||||
const updateCase = jest.fn();
|
||||
const postPushToService = jest.fn();
|
||||
const pushCaseToExternalService = jest.fn();
|
||||
|
||||
const data = caseProps.caseData;
|
||||
const defaultGetCase = {
|
||||
|
@ -144,7 +144,10 @@ describe('CaseView ', () => {
|
|||
|
||||
jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation);
|
||||
useGetCaseUserActionsMock.mockImplementation(() => defaultUseGetCaseUserActions);
|
||||
usePostPushToServiceMock.mockImplementation(() => ({ isLoading: false, postPushToService }));
|
||||
usePostPushToServiceMock.mockImplementation(() => ({
|
||||
isLoading: false,
|
||||
pushCaseToExternalService,
|
||||
}));
|
||||
useConnectorsMock.mockImplementation(() => ({ connectors: connectorsMock, loading: false }));
|
||||
useQueryAlertsMock.mockImplementation(() => ({
|
||||
loading: false,
|
||||
|
@ -378,7 +381,7 @@ describe('CaseView ', () => {
|
|||
|
||||
wrapper.update();
|
||||
|
||||
expect(postPushToService).toHaveBeenCalled();
|
||||
expect(pushCaseToExternalService).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -508,7 +511,7 @@ describe('CaseView ', () => {
|
|||
connector: {
|
||||
id: 'servicenow-1',
|
||||
name: 'SN 1',
|
||||
type: ConnectorTypes.servicenow,
|
||||
type: ConnectorTypes.serviceNowITSM,
|
||||
fields: null,
|
||||
},
|
||||
}}
|
||||
|
@ -556,7 +559,7 @@ describe('CaseView ', () => {
|
|||
connector: {
|
||||
id: 'servicenow-1',
|
||||
name: 'SN 1',
|
||||
type: ConnectorTypes.servicenow,
|
||||
type: ConnectorTypes.serviceNowITSM,
|
||||
fields: null,
|
||||
},
|
||||
}}
|
||||
|
|
|
@ -297,7 +297,6 @@ export const CaseComponent = React.memo<CaseProps>(
|
|||
updateCase: handleUpdateCase,
|
||||
userCanCrud,
|
||||
isValidConnector: isLoadingConnectors ? true : isValidConnector,
|
||||
alerts,
|
||||
});
|
||||
|
||||
const onSubmitConnector = useCallback(
|
||||
|
@ -397,7 +396,6 @@ export const CaseComponent = React.memo<CaseProps>(
|
|||
);
|
||||
}
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeaderWrapper>
|
||||
|
|
|
@ -72,7 +72,7 @@ describe('Connectors', () => {
|
|||
const newWrapper = mount(
|
||||
<Connectors
|
||||
{...props}
|
||||
selectedConnector={{ id: 'servicenow-1', type: ConnectorTypes.servicenow }}
|
||||
selectedConnector={{ id: 'servicenow-1', type: ConnectorTypes.serviceNowITSM }}
|
||||
/>,
|
||||
{
|
||||
wrappingComponent: TestProviders,
|
||||
|
@ -99,7 +99,7 @@ describe('Connectors', () => {
|
|||
const newWrapper = mount(
|
||||
<Connectors
|
||||
{...props}
|
||||
selectedConnector={{ id: 'servicenow-1', type: ConnectorTypes.servicenow }}
|
||||
selectedConnector={{ id: 'servicenow-1', type: ConnectorTypes.serviceNowITSM }}
|
||||
/>,
|
||||
{
|
||||
wrappingComponent: TestProviders,
|
||||
|
|
|
@ -186,14 +186,14 @@ describe('ConfigureCases', () => {
|
|||
connector: {
|
||||
id: 'servicenow-1',
|
||||
name: 'unchanged',
|
||||
type: ConnectorTypes.servicenow,
|
||||
type: ConnectorTypes.serviceNowITSM,
|
||||
fields: null,
|
||||
},
|
||||
currentConfiguration: {
|
||||
connector: {
|
||||
id: 'servicenow-1',
|
||||
name: 'unchanged',
|
||||
type: ConnectorTypes.servicenow,
|
||||
type: ConnectorTypes.serviceNowITSM,
|
||||
fields: null,
|
||||
},
|
||||
closureType: 'close-by-user',
|
||||
|
@ -271,7 +271,7 @@ describe('ConfigureCases', () => {
|
|||
connector: {
|
||||
id: 'servicenow-1',
|
||||
name: 'unchanged',
|
||||
type: ConnectorTypes.servicenow,
|
||||
type: ConnectorTypes.serviceNowITSM,
|
||||
fields: null,
|
||||
},
|
||||
closureType: 'close-by-user',
|
||||
|
@ -331,7 +331,7 @@ describe('ConfigureCases', () => {
|
|||
connector: {
|
||||
id: 'servicenow-1',
|
||||
name: 'SN',
|
||||
type: ConnectorTypes.servicenow,
|
||||
type: ConnectorTypes.serviceNowITSM,
|
||||
fields: null,
|
||||
},
|
||||
persistLoading: true,
|
||||
|
@ -450,7 +450,7 @@ describe('ConfigureCases', () => {
|
|||
connector: {
|
||||
id: 'servicenow-1',
|
||||
name: 'My connector',
|
||||
type: ConnectorTypes.servicenow,
|
||||
type: ConnectorTypes.serviceNowITSM,
|
||||
fields: null,
|
||||
},
|
||||
}))
|
||||
|
@ -493,7 +493,7 @@ describe('closure options', () => {
|
|||
connector: {
|
||||
id: 'servicenow-1',
|
||||
name: 'My connector',
|
||||
type: ConnectorTypes.servicenow,
|
||||
type: ConnectorTypes.serviceNowITSM,
|
||||
fields: null,
|
||||
},
|
||||
currentConfiguration: {
|
||||
|
@ -522,7 +522,7 @@ describe('closure options', () => {
|
|||
connector: {
|
||||
id: 'servicenow-1',
|
||||
name: 'My connector',
|
||||
type: ConnectorTypes.servicenow,
|
||||
type: ConnectorTypes.serviceNowITSM,
|
||||
fields: null,
|
||||
},
|
||||
closureType: 'close-by-pushing',
|
||||
|
@ -546,7 +546,7 @@ describe('user interactions', () => {
|
|||
connector: {
|
||||
id: 'resilient-2',
|
||||
name: 'unchanged',
|
||||
type: ConnectorTypes.servicenow,
|
||||
type: ConnectorTypes.serviceNowITSM,
|
||||
fields: null,
|
||||
},
|
||||
closureType: 'close-by-user',
|
||||
|
|
|
@ -5,13 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import { EuiFormRow } from '@elastic/eui';
|
||||
|
||||
import { FieldHook, getFieldValidityAndErrorMessage } from '../../../shared_imports';
|
||||
import { ConnectorsDropdown } from '../configure_cases/connectors_dropdown';
|
||||
import { ActionConnector } from '../../../../../case/common/api/cases';
|
||||
import { ActionConnector } from '../../../../../case/common/api';
|
||||
|
||||
interface ConnectorSelectorProps {
|
||||
connectors: ActionConnector[];
|
||||
|
@ -21,6 +21,7 @@ interface ConnectorSelectorProps {
|
|||
idAria: string;
|
||||
isEdit: boolean;
|
||||
isLoading: boolean;
|
||||
handleChange?: (newValue: string) => void;
|
||||
}
|
||||
export const ConnectorSelector = ({
|
||||
connectors,
|
||||
|
@ -30,8 +31,19 @@ export const ConnectorSelector = ({
|
|||
idAria,
|
||||
isEdit = true,
|
||||
isLoading = false,
|
||||
handleChange,
|
||||
}: ConnectorSelectorProps) => {
|
||||
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
|
||||
const onChange = useCallback(
|
||||
(val: string) => {
|
||||
if (handleChange) {
|
||||
handleChange(val);
|
||||
}
|
||||
field.setValue(val);
|
||||
},
|
||||
[handleChange, field]
|
||||
);
|
||||
|
||||
return isEdit ? (
|
||||
<EuiFormRow
|
||||
data-test-subj={dataTestSubj}
|
||||
|
@ -47,7 +59,7 @@ export const ConnectorSelector = ({
|
|||
connectors={connectors}
|
||||
disabled={disabled}
|
||||
isLoading={isLoading}
|
||||
onChange={field.setValue}
|
||||
onChange={onChange}
|
||||
selectedConnector={isEmpty(field.value) ? 'none' : field.value}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
|
|
@ -9,7 +9,7 @@ import React, { memo, useMemo } from 'react';
|
|||
import { EuiCard, EuiIcon, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { connectorsConfiguration } from '../connectors';
|
||||
import { connectorsConfiguration } from '.';
|
||||
import { ConnectorTypes } from '../../../../../case/common/api/connectors';
|
||||
|
||||
interface ConnectorCardProps {
|
||||
|
@ -51,10 +51,10 @@ const ConnectorCardDisplay: React.FC<ConnectorCardProps> = ({
|
|||
);
|
||||
return (
|
||||
<>
|
||||
{isLoading && <EuiLoadingSpinner data-test-subj="settings-connector-card-loading" />}
|
||||
{isLoading && <EuiLoadingSpinner data-test-subj="connector-card-loading" />}
|
||||
{!isLoading && (
|
||||
<EuiCard
|
||||
data-test-subj={`settings-connector-card`}
|
||||
data-test-subj={`connector-card`}
|
||||
description={description}
|
||||
display="plain"
|
||||
icon={icon}
|
|
@ -37,6 +37,6 @@ export function getActionType(): ActionTypeModel {
|
|||
validateConnector: () => ({ config: { errors: {} }, secrets: { errors: {} } }),
|
||||
validateParams,
|
||||
actionConnectorFields: null,
|
||||
actionParamsFields: lazy(() => import('./fields')),
|
||||
actionParamsFields: lazy(() => import('./alert_fields')),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,17 +5,35 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
/* eslint-disable @kbn/eslint/no-restricted-paths */
|
||||
|
||||
import {
|
||||
ServiceNowITSMConnectorConfiguration,
|
||||
JiraConnectorConfiguration,
|
||||
ResilientConnectorConfiguration,
|
||||
getResilientActionType,
|
||||
getServiceNowITSMActionType,
|
||||
getServiceNowSIRActionType,
|
||||
getJiraActionType,
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
} from '../../../../../triggers_actions_ui/public/common';
|
||||
import { ConnectorConfiguration } from './types';
|
||||
|
||||
const resilient = getResilientActionType();
|
||||
const serviceNowITSM = getServiceNowITSMActionType();
|
||||
const serviceNowSIR = getServiceNowSIRActionType();
|
||||
const jira = getJiraActionType();
|
||||
|
||||
export const connectorsConfiguration: Record<string, ConnectorConfiguration> = {
|
||||
'.servicenow': ServiceNowITSMConnectorConfiguration as ConnectorConfiguration,
|
||||
'.jira': JiraConnectorConfiguration as ConnectorConfiguration,
|
||||
'.resilient': ResilientConnectorConfiguration as ConnectorConfiguration,
|
||||
'.servicenow': {
|
||||
name: serviceNowITSM.actionTypeTitle ?? '',
|
||||
logo: serviceNowITSM.iconClass,
|
||||
},
|
||||
'.servicenow-sir': {
|
||||
name: serviceNowSIR.actionTypeTitle ?? '',
|
||||
logo: serviceNowSIR.iconClass,
|
||||
},
|
||||
'.jira': {
|
||||
name: jira.actionTypeTitle ?? '',
|
||||
logo: jira.iconClass,
|
||||
},
|
||||
'.resilient': {
|
||||
name: resilient.actionTypeTitle ?? '',
|
||||
logo: resilient.iconClass,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { CaseConnector, CaseConnectorsRegistry } from './types';
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
export const createCaseConnectorsRegistry = (): CaseConnectorsRegistry => {
|
||||
const connectors: Map<string, CaseConnector<any>> = new Map();
|
||||
|
||||
const registry: CaseConnectorsRegistry = {
|
||||
has: (id: string) => connectors.has(id),
|
||||
register: <UIProps>(connector: CaseConnector<UIProps>) => {
|
||||
if (connectors.has(connector.id)) {
|
||||
throw new Error(
|
||||
i18n.translate(
|
||||
'xpack.securitySolution.caseConnectorsRegistry.register.duplicateCaseConnectorErrorMessage',
|
||||
{
|
||||
defaultMessage: 'Object type "{id}" is already registered.',
|
||||
values: {
|
||||
id: connector.id,
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
connectors.set(connector.id, connector);
|
||||
},
|
||||
get: <UIProps>(id: string): CaseConnector<UIProps> => {
|
||||
if (!connectors.has(id)) {
|
||||
throw new Error(
|
||||
i18n.translate(
|
||||
'xpack.securitySolution.caseConnectorsRegistry.get.missingCaseConnectorErrorMessage',
|
||||
{
|
||||
defaultMessage: 'Object type "{id}" is not registered.',
|
||||
values: {
|
||||
id,
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
return connectors.get(id)!;
|
||||
},
|
||||
list: () => {
|
||||
return Array.from(connectors).map(([id, connector]) => connector);
|
||||
},
|
||||
};
|
||||
|
||||
return registry;
|
||||
};
|
|
@ -8,24 +8,22 @@
|
|||
import React, { memo, Suspense } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
|
||||
|
||||
import { CaseSettingsConnector, SettingFieldsProps } from './types';
|
||||
import { getCaseSettings } from '.';
|
||||
import { CaseActionConnector, ConnectorFieldsProps } from './types';
|
||||
import { getCaseConnectors } from '.';
|
||||
import { ConnectorTypeFields } from '../../../../../case/common/api/connectors';
|
||||
|
||||
interface Props extends Omit<SettingFieldsProps<ConnectorTypeFields['fields']>, 'connector'> {
|
||||
connector: CaseSettingsConnector | null;
|
||||
interface Props extends Omit<ConnectorFieldsProps<ConnectorTypeFields['fields']>, 'connector'> {
|
||||
connector: CaseActionConnector | null;
|
||||
}
|
||||
|
||||
const SettingFieldsFormComponent: React.FC<Props> = ({ connector, isEdit, onChange, fields }) => {
|
||||
const { caseSettingsRegistry } = getCaseSettings();
|
||||
const ConnectorFieldsFormComponent: React.FC<Props> = ({ connector, isEdit, onChange, fields }) => {
|
||||
const { caseConnectorsRegistry } = getCaseConnectors();
|
||||
|
||||
if (connector == null || connector.actionTypeId == null || connector.actionTypeId === '.none') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { caseSettingFieldsComponent: FieldsComponent } = caseSettingsRegistry.get(
|
||||
connector.actionTypeId
|
||||
);
|
||||
const { fieldsComponent: FieldsComponent } = caseConnectorsRegistry.get(connector.actionTypeId);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -39,7 +37,7 @@ const SettingFieldsFormComponent: React.FC<Props> = ({ connector, isEdit, onChan
|
|||
</EuiFlexGroup>
|
||||
}
|
||||
>
|
||||
<div data-test-subj={'connector-settings'}>
|
||||
<div data-test-subj={'connector-fields'}>
|
||||
<FieldsComponent
|
||||
isEdit={isEdit}
|
||||
fields={fields}
|
||||
|
@ -53,4 +51,4 @@ const SettingFieldsFormComponent: React.FC<Props> = ({ connector, isEdit, onChan
|
|||
);
|
||||
};
|
||||
|
||||
export const SettingFieldsForm = memo(SettingFieldsFormComponent);
|
||||
export const ConnectorFieldsForm = memo(ConnectorFieldsFormComponent);
|
|
@ -5,7 +5,53 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { CaseConnectorsRegistry } from './types';
|
||||
import { createCaseConnectorsRegistry } from './connectors_registry';
|
||||
import { getCaseConnector as getJiraCaseConnector } from './jira';
|
||||
import { getCaseConnector as getResilientCaseConnector } from './resilient';
|
||||
import { getServiceNowITSMCaseConnector, getServiceNowSIRCaseConnector } from './servicenow';
|
||||
import {
|
||||
JiraFieldsType,
|
||||
ServiceNowITSMFieldsType,
|
||||
ServiceNowSIRFieldsType,
|
||||
ResilientFieldsType,
|
||||
} from '../../../../../case/common/api/connectors';
|
||||
|
||||
export { getActionType as getCaseConnectorUI } from './case';
|
||||
|
||||
export * from './config';
|
||||
export * from './types';
|
||||
|
||||
interface GetCaseConnectorsReturn {
|
||||
caseConnectorsRegistry: CaseConnectorsRegistry;
|
||||
}
|
||||
|
||||
class CaseConnectors {
|
||||
private caseConnectorsRegistry: CaseConnectorsRegistry;
|
||||
|
||||
constructor() {
|
||||
this.caseConnectorsRegistry = createCaseConnectorsRegistry();
|
||||
this.init();
|
||||
}
|
||||
|
||||
private init() {
|
||||
this.caseConnectorsRegistry.register<JiraFieldsType>(getJiraCaseConnector());
|
||||
this.caseConnectorsRegistry.register<ResilientFieldsType>(getResilientCaseConnector());
|
||||
this.caseConnectorsRegistry.register<ServiceNowITSMFieldsType>(
|
||||
getServiceNowITSMCaseConnector()
|
||||
);
|
||||
this.caseConnectorsRegistry.register<ServiceNowSIRFieldsType>(getServiceNowSIRCaseConnector());
|
||||
}
|
||||
|
||||
registry(): CaseConnectorsRegistry {
|
||||
return this.caseConnectorsRegistry;
|
||||
}
|
||||
}
|
||||
|
||||
const caseConnectors = new CaseConnectors();
|
||||
|
||||
export const getCaseConnectors = (): GetCaseConnectorsReturn => {
|
||||
return {
|
||||
caseConnectorsRegistry: caseConnectors.registry(),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -12,7 +12,7 @@ import { omit } from 'lodash/fp';
|
|||
import { connector, issues } from '../mock';
|
||||
import { useGetIssueTypes } from './use_get_issue_types';
|
||||
import { useGetFieldsByIssueType } from './use_get_fields_by_issue_type';
|
||||
import Fields from './fields';
|
||||
import Fields from './case_fields';
|
||||
import { waitFor } from '@testing-library/dom';
|
||||
import { useGetSingleIssue } from './use_get_single_issue';
|
||||
import { useGetIssues } from './use_get_issues';
|
|
@ -5,25 +5,26 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import React, { useCallback, useMemo, useEffect, useRef } from 'react';
|
||||
import { map } from 'lodash/fp';
|
||||
import { EuiFormRow, EuiSelect, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||
import * as i18n from './translations';
|
||||
|
||||
import { ConnectorTypes, JiraFieldsType } from '../../../../../../case/common/api/connectors';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { SettingFieldsProps } from '../types';
|
||||
import { ConnectorFieldsProps } from '../types';
|
||||
import { useGetIssueTypes } from './use_get_issue_types';
|
||||
import { useGetFieldsByIssueType } from './use_get_fields_by_issue_type';
|
||||
import { SearchIssues } from './search_issues';
|
||||
import { ConnectorCard } from '../card';
|
||||
|
||||
const JiraSettingFieldsComponent: React.FunctionComponent<SettingFieldsProps<JiraFieldsType>> = ({
|
||||
const JiraFieldsComponent: React.FunctionComponent<ConnectorFieldsProps<JiraFieldsType>> = ({
|
||||
connector,
|
||||
fields,
|
||||
isEdit = true,
|
||||
onChange,
|
||||
}) => {
|
||||
const init = useRef(true);
|
||||
const { issueType = null, priority = null, parent = null } = fields ?? {};
|
||||
const { http, notifications } = useKibana().services;
|
||||
|
||||
|
@ -138,8 +139,16 @@ const JiraSettingFieldsComponent: React.FunctionComponent<SettingFieldsProps<Jir
|
|||
[currentIssueType, fields, onChange, parent, priority]
|
||||
);
|
||||
|
||||
// Set field at initialization
|
||||
useEffect(() => {
|
||||
if (init.current) {
|
||||
init.current = false;
|
||||
onChange({ issueType, priority, parent });
|
||||
}
|
||||
}, [issueType, onChange, parent, priority]);
|
||||
|
||||
return isEdit ? (
|
||||
<div data-test-subj={'connector-settings-jira'}>
|
||||
<div data-test-subj={'connector-fields-jira'}>
|
||||
<EuiFormRow fullWidth label={i18n.ISSUE_TYPE}>
|
||||
<EuiSelect
|
||||
data-test-subj="issueTypeSelect"
|
||||
|
@ -202,4 +211,4 @@ const JiraSettingFieldsComponent: React.FunctionComponent<SettingFieldsProps<Jir
|
|||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { JiraSettingFieldsComponent as default };
|
||||
export { JiraFieldsComponent as default };
|
|
@ -7,16 +7,16 @@
|
|||
|
||||
import { lazy } from 'react';
|
||||
|
||||
import { CaseSetting } from '../types';
|
||||
import { CaseConnector } from '../types';
|
||||
import { JiraFieldsType } from '../../../../../../case/common/api/connectors';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export * from './types';
|
||||
|
||||
export const getCaseSetting = (): CaseSetting<JiraFieldsType> => {
|
||||
export const getCaseConnector = (): CaseConnector<JiraFieldsType> => {
|
||||
return {
|
||||
id: '.jira',
|
||||
caseSettingFieldsComponent: lazy(() => import('./fields')),
|
||||
fieldsComponent: lazy(() => import('./case_fields')),
|
||||
};
|
||||
};
|
||||
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue