mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Security Solution] [Cases] Move field mappings from actions to cases (#84587)
This commit is contained in:
parent
d4a631cf8e
commit
335cd1f6fc
173 changed files with 3846 additions and 6241 deletions
|
@ -42,6 +42,7 @@ const allowedList: CircularDepList = new Set([
|
|||
'src/plugins/vis_default_editor -> src/plugins/visualize',
|
||||
'src/plugins/visualizations -> src/plugins/visualize',
|
||||
'x-pack/plugins/actions -> x-pack/plugins/case',
|
||||
'x-pack/plugins/case -> x-pack/plugins/security_solution',
|
||||
'x-pack/plugins/apm -> x-pack/plugins/infra',
|
||||
'x-pack/plugins/lists -> x-pack/plugins/security_solution',
|
||||
'x-pack/plugins/security -> x-pack/plugins/spaces',
|
||||
|
|
|
@ -553,7 +553,6 @@ The ServiceNow action uses the [V2 Table API](https://developer.servicenow.com/a
|
|||
| Property | Description | Type |
|
||||
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------- |
|
||||
| apiUrl | ServiceNow instance URL. | string |
|
||||
| incidentConfiguration | Optional property and specific to **Cases only**. If defined, the object should contain an attribute called `mapping`. A `mapping` is an array of objects. Each mapping object should be of the form `{ source: string, target: string, actionType: string }`. `source` is the Case field. `target` is the ServiceNow field where `source` will be mapped to. `actionType` can be one of `nothing`, `overwrite` or `append`. For example the `{ source: 'title', target: 'short_description', actionType: 'overwrite' }` record, inside mapping array, means that the title of a case will be mapped to the short description of an incident in ServiceNow and will be overwrite on each update. | object _(optional)_ |
|
||||
|
||||
### `secrets`
|
||||
|
||||
|
@ -600,7 +599,6 @@ The Jira action uses the [V2 API](https://developer.atlassian.com/cloud/jira/pla
|
|||
| Property | Description | Type |
|
||||
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- |
|
||||
| apiUrl | Jira instance URL. | string |
|
||||
| incidentConfiguration | Optional property and specific to **Cases only**. if defined, the object should contain an attribute called `mapping`. A `mapping` is an array of objects. Each mapping object should be of the form `{ source: string, target: string, actionType: string }`. `source` is the Case field. `target` is the Jira field where `source` will be mapped to. `actionType` can be one of `nothing`, `overwrite` or `append`. For example the `{ source: 'title', target: 'summary', actionType: 'overwrite' }` record, inside mapping array, means that the title of a case will be mapped to the short description of an incident in Jira and will be overwrite on each update. | object _(optional)_ |
|
||||
|
||||
### `secrets`
|
||||
|
||||
|
@ -653,7 +651,6 @@ ID: `.resilient`
|
|||
| Property | Description | Type |
|
||||
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ |
|
||||
| apiUrl | IBM Resilient instance URL. | string |
|
||||
| incidentConfiguration | Optional property and specific to **Cases only**. If defined, the object should contain an attribute called `mapping`. A `mapping` is an array of objects. Each mapping object should be of the form `{ source: string, target: string, actionType: string }`. `source` is the Case field. `target` is the Jira field where `source` will be mapped to. `actionType` can be one of `nothing`, `overwrite` or `append`. For example the `{ source: 'title', target: 'summary', actionType: 'overwrite' }` record, inside mapping array, means that the title of a case will be mapped to the short description of an incident in IBM Resilient and will be overwrite on each update. | object |
|
||||
|
||||
### `secrets`
|
||||
|
||||
|
|
|
@ -1,7 +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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const SUPPORTED_SOURCE_FIELDS = ['title', 'comments', 'description'];
|
|
@ -1,43 +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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
|
||||
export const MappingActionType = schema.oneOf([
|
||||
schema.literal('nothing'),
|
||||
schema.literal('overwrite'),
|
||||
schema.literal('append'),
|
||||
]);
|
||||
|
||||
export const MapRecordSchema = schema.object({
|
||||
source: schema.string(),
|
||||
target: schema.string(),
|
||||
actionType: MappingActionType,
|
||||
});
|
||||
|
||||
export const IncidentConfigurationSchema = schema.object({
|
||||
mapping: schema.arrayOf(MapRecordSchema),
|
||||
});
|
||||
|
||||
export const UserSchema = schema.object({
|
||||
fullName: schema.nullable(schema.string()),
|
||||
username: schema.nullable(schema.string()),
|
||||
});
|
||||
|
||||
export const EntityInformation = {
|
||||
createdAt: schema.nullable(schema.string()),
|
||||
createdBy: schema.nullable(UserSchema),
|
||||
updatedAt: schema.nullable(schema.string()),
|
||||
updatedBy: schema.nullable(UserSchema),
|
||||
};
|
||||
|
||||
export const EntityInformationSchema = schema.object(EntityInformation);
|
||||
|
||||
export const CommentSchema = schema.object({
|
||||
commentId: schema.string(),
|
||||
comment: schema.string(),
|
||||
...EntityInformation,
|
||||
});
|
|
@ -1,131 +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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { transformers } from './transformers';
|
||||
|
||||
const { informationCreated, informationUpdated, informationAdded, append } = transformers;
|
||||
|
||||
describe('informationCreated', () => {
|
||||
test('transforms correctly', () => {
|
||||
const res = informationCreated({
|
||||
value: 'a value',
|
||||
date: '2020-04-15T08:19:27.400Z',
|
||||
user: 'elastic',
|
||||
});
|
||||
expect(res).toEqual({ value: 'a value (created at 2020-04-15T08:19:27.400Z by elastic)' });
|
||||
});
|
||||
|
||||
test('transforms correctly without optional fields', () => {
|
||||
const res = informationCreated({
|
||||
value: 'a value',
|
||||
});
|
||||
expect(res).toEqual({ value: 'a value (created at by )' });
|
||||
});
|
||||
|
||||
test('returns correctly rest fields', () => {
|
||||
const res = informationCreated({
|
||||
value: 'a value',
|
||||
date: '2020-04-15T08:19:27.400Z',
|
||||
user: 'elastic',
|
||||
previousValue: 'previous value',
|
||||
});
|
||||
expect(res).toEqual({
|
||||
value: 'a value (created at 2020-04-15T08:19:27.400Z by elastic)',
|
||||
previousValue: 'previous value',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('informationUpdated', () => {
|
||||
test('transforms correctly', () => {
|
||||
const res = informationUpdated({
|
||||
value: 'a value',
|
||||
date: '2020-04-15T08:19:27.400Z',
|
||||
user: 'elastic',
|
||||
});
|
||||
expect(res).toEqual({ value: 'a value (updated at 2020-04-15T08:19:27.400Z by elastic)' });
|
||||
});
|
||||
|
||||
test('transforms correctly without optional fields', () => {
|
||||
const res = informationUpdated({
|
||||
value: 'a value',
|
||||
});
|
||||
expect(res).toEqual({ value: 'a value (updated at by )' });
|
||||
});
|
||||
|
||||
test('returns correctly rest fields', () => {
|
||||
const res = informationUpdated({
|
||||
value: 'a value',
|
||||
date: '2020-04-15T08:19:27.400Z',
|
||||
user: 'elastic',
|
||||
previousValue: 'previous value',
|
||||
});
|
||||
expect(res).toEqual({
|
||||
value: 'a value (updated at 2020-04-15T08:19:27.400Z by elastic)',
|
||||
previousValue: 'previous value',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('informationAdded', () => {
|
||||
test('transforms correctly', () => {
|
||||
const res = informationAdded({
|
||||
value: 'a value',
|
||||
date: '2020-04-15T08:19:27.400Z',
|
||||
user: 'elastic',
|
||||
});
|
||||
expect(res).toEqual({ value: 'a value (added at 2020-04-15T08:19:27.400Z by elastic)' });
|
||||
});
|
||||
|
||||
test('transforms correctly without optional fields', () => {
|
||||
const res = informationAdded({
|
||||
value: 'a value',
|
||||
});
|
||||
expect(res).toEqual({ value: 'a value (added at by )' });
|
||||
});
|
||||
|
||||
test('returns correctly rest fields', () => {
|
||||
const res = informationAdded({
|
||||
value: 'a value',
|
||||
date: '2020-04-15T08:19:27.400Z',
|
||||
user: 'elastic',
|
||||
previousValue: 'previous value',
|
||||
});
|
||||
expect(res).toEqual({
|
||||
value: 'a value (added at 2020-04-15T08:19:27.400Z by elastic)',
|
||||
previousValue: 'previous value',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('append', () => {
|
||||
test('transforms correctly', () => {
|
||||
const res = append({
|
||||
value: 'a value',
|
||||
previousValue: 'previous value',
|
||||
});
|
||||
expect(res).toEqual({ value: 'previous value \r\na value' });
|
||||
});
|
||||
|
||||
test('transforms correctly without optional fields', () => {
|
||||
const res = append({
|
||||
value: 'a value',
|
||||
});
|
||||
expect(res).toEqual({ value: 'a value' });
|
||||
});
|
||||
|
||||
test('returns correctly rest fields', () => {
|
||||
const res = append({
|
||||
value: 'a value',
|
||||
user: 'elastic',
|
||||
previousValue: 'previous value',
|
||||
});
|
||||
expect(res).toEqual({
|
||||
value: 'previous value \r\na value',
|
||||
user: 'elastic',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,29 +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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { TransformerArgs } from './types';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export type Transformer = (args: TransformerArgs) => TransformerArgs;
|
||||
|
||||
export const transformers: Record<string, Transformer> = {
|
||||
informationCreated: ({ value, date, user, ...rest }: TransformerArgs): TransformerArgs => ({
|
||||
value: `${value} ${i18n.FIELD_INFORMATION('create', date, user)}`,
|
||||
...rest,
|
||||
}),
|
||||
informationUpdated: ({ value, date, user, ...rest }: TransformerArgs): TransformerArgs => ({
|
||||
value: `${value} ${i18n.FIELD_INFORMATION('update', date, user)}`,
|
||||
...rest,
|
||||
}),
|
||||
informationAdded: ({ value, date, user, ...rest }: TransformerArgs): TransformerArgs => ({
|
||||
value: `${value} ${i18n.FIELD_INFORMATION('add', date, user)}`,
|
||||
...rest,
|
||||
}),
|
||||
append: ({ value, previousValue, ...rest }: TransformerArgs): TransformerArgs => ({
|
||||
value: previousValue ? `${previousValue} \r\n${value}` : `${value}`,
|
||||
...rest,
|
||||
}),
|
||||
};
|
|
@ -1,55 +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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const API_URL_REQUIRED = i18n.translate('xpack.actions.builtin.case.connectorApiNullError', {
|
||||
defaultMessage: 'connector [apiUrl] is required',
|
||||
});
|
||||
|
||||
export const FIELD_INFORMATION = (
|
||||
mode: string,
|
||||
date: string | undefined,
|
||||
user: string | undefined
|
||||
) => {
|
||||
switch (mode) {
|
||||
case 'create':
|
||||
return i18n.translate('xpack.actions.builtin.case.common.externalIncidentCreated', {
|
||||
values: { date, user },
|
||||
defaultMessage: '(created at {date} by {user})',
|
||||
});
|
||||
case 'update':
|
||||
return i18n.translate('xpack.actions.builtin.case.common.externalIncidentUpdated', {
|
||||
values: { date, user },
|
||||
defaultMessage: '(updated at {date} by {user})',
|
||||
});
|
||||
case 'add':
|
||||
return i18n.translate('xpack.actions.builtin.case.common.externalIncidentAdded', {
|
||||
values: { date, user },
|
||||
defaultMessage: '(added at {date} by {user})',
|
||||
});
|
||||
default:
|
||||
return i18n.translate('xpack.actions.builtin.case.common.externalIncidentDefault', {
|
||||
values: { date, user },
|
||||
defaultMessage: '(created at {date} by {user})',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const MAPPING_EMPTY = i18n.translate(
|
||||
'xpack.actions.builtin.case.configuration.emptyMapping',
|
||||
{
|
||||
defaultMessage: '[casesConfiguration.mapping]: expected non-empty but got empty',
|
||||
}
|
||||
);
|
||||
|
||||
export const WHITE_LISTED_ERROR = (message: string) =>
|
||||
i18n.translate('xpack.actions.builtin.case.configuration.apiWhitelistError', {
|
||||
defaultMessage: 'error configuring connector action: {message}',
|
||||
values: {
|
||||
message,
|
||||
},
|
||||
});
|
|
@ -1,54 +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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { TypeOf } from '@kbn/config-schema';
|
||||
import {
|
||||
IncidentConfigurationSchema,
|
||||
MapRecordSchema,
|
||||
CommentSchema,
|
||||
EntityInformationSchema,
|
||||
} from './schema';
|
||||
|
||||
export type IncidentConfiguration = TypeOf<typeof IncidentConfigurationSchema>;
|
||||
export type MapRecord = TypeOf<typeof MapRecordSchema>;
|
||||
export type Comment = TypeOf<typeof CommentSchema>;
|
||||
export type EntityInformation = TypeOf<typeof EntityInformationSchema>;
|
||||
|
||||
export interface ExternalServiceCommentResponse {
|
||||
commentId: string;
|
||||
pushedDate: string;
|
||||
externalCommentId?: string;
|
||||
}
|
||||
|
||||
export interface PipedField {
|
||||
key: string;
|
||||
value: string;
|
||||
actionType: string;
|
||||
pipes: string[];
|
||||
}
|
||||
|
||||
export interface TransformFieldsArgs<P, S> {
|
||||
params: P;
|
||||
fields: PipedField[];
|
||||
currentIncident?: S;
|
||||
}
|
||||
|
||||
export interface TransformerArgs {
|
||||
value: string;
|
||||
date?: string;
|
||||
user?: string;
|
||||
previousValue?: string;
|
||||
}
|
||||
|
||||
export interface AnyParams {
|
||||
[index: string]: string | number | object | undefined | null;
|
||||
}
|
||||
|
||||
export interface PrepareFieldsForTransformArgs {
|
||||
externalCase: Record<string, string>;
|
||||
mapping: Map<string, MapRecord>;
|
||||
defaultPipes?: string[];
|
||||
}
|
|
@ -1,494 +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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import {
|
||||
normalizeMapping,
|
||||
buildMap,
|
||||
mapParams,
|
||||
prepareFieldsForTransformation,
|
||||
transformFields,
|
||||
transformComments,
|
||||
} from './utils';
|
||||
|
||||
import { SUPPORTED_SOURCE_FIELDS } from './constants';
|
||||
import { Comment, MapRecord } from './types';
|
||||
|
||||
interface Entity {
|
||||
createdAt: string | null;
|
||||
createdBy: { fullName: string; username: string } | null;
|
||||
updatedAt: string | null;
|
||||
updatedBy: { fullName: string; username: string } | null;
|
||||
}
|
||||
|
||||
interface PushToServiceApiParams extends Entity {
|
||||
savedObjectId: string;
|
||||
title: string;
|
||||
description: string | null;
|
||||
externalId: string | null;
|
||||
externalObject: Record<string, any>;
|
||||
comments: Comment[];
|
||||
}
|
||||
|
||||
const mapping: MapRecord[] = [
|
||||
{ source: 'title', target: 'short_description', actionType: 'overwrite' },
|
||||
{ source: 'description', target: 'description', actionType: 'append' },
|
||||
{ source: 'comments', target: 'comments', actionType: 'append' },
|
||||
];
|
||||
|
||||
const finalMapping: Map<string, any> = new Map();
|
||||
|
||||
finalMapping.set('title', {
|
||||
target: 'short_description',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
finalMapping.set('description', {
|
||||
target: 'description',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
finalMapping.set('comments', {
|
||||
target: 'comments',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
finalMapping.set('short_description', {
|
||||
target: 'title',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
const maliciousMapping: MapRecord[] = [
|
||||
{ source: '__proto__', target: 'short_description', actionType: 'nothing' },
|
||||
{ source: 'description', target: '__proto__', actionType: 'nothing' },
|
||||
{ source: 'comments', target: 'comments', actionType: 'nothing' },
|
||||
{ source: 'unsupportedSource', target: 'comments', actionType: 'nothing' },
|
||||
];
|
||||
|
||||
const fullParams: PushToServiceApiParams = {
|
||||
savedObjectId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa',
|
||||
title: 'a title',
|
||||
description: 'a description',
|
||||
createdAt: '2020-03-13T08:34:53.450Z',
|
||||
createdBy: { fullName: 'Elastic User', username: 'elastic' },
|
||||
updatedAt: null,
|
||||
updatedBy: null,
|
||||
externalId: null,
|
||||
externalObject: {
|
||||
short_description: 'a title',
|
||||
description: 'a description',
|
||||
},
|
||||
comments: [
|
||||
{
|
||||
commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631',
|
||||
comment: 'first comment',
|
||||
createdAt: '2020-03-13T08:34:53.450Z',
|
||||
createdBy: { fullName: 'Elastic User', username: 'elastic' },
|
||||
updatedAt: null,
|
||||
updatedBy: null,
|
||||
},
|
||||
{
|
||||
commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631',
|
||||
comment: 'second comment',
|
||||
createdAt: '2020-03-13T08:34:53.450Z',
|
||||
createdBy: { fullName: 'Elastic User', username: 'elastic' },
|
||||
updatedAt: null,
|
||||
updatedBy: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('normalizeMapping', () => {
|
||||
test('remove malicious fields', () => {
|
||||
const sanitizedMapping = normalizeMapping(SUPPORTED_SOURCE_FIELDS, maliciousMapping);
|
||||
expect(
|
||||
sanitizedMapping.every((m) => m.source !== '__proto__' && m.target !== '__proto__')
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('remove unsuppported source fields', () => {
|
||||
const normalizedMapping = normalizeMapping(SUPPORTED_SOURCE_FIELDS, maliciousMapping);
|
||||
expect(normalizedMapping).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
source: 'unsupportedSource',
|
||||
target: 'comments',
|
||||
actionType: 'nothing',
|
||||
}),
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildMap', () => {
|
||||
test('builds sanitized Map', () => {
|
||||
const finalMap = buildMap(maliciousMapping);
|
||||
expect(finalMap.get('__proto__')).not.toBeDefined();
|
||||
});
|
||||
|
||||
test('builds Map correct', () => {
|
||||
const final = buildMap(mapping);
|
||||
expect(final).toEqual(finalMapping);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapParams', () => {
|
||||
test('maps params correctly', () => {
|
||||
const params = {
|
||||
savedObjectId: '123',
|
||||
incidentId: '456',
|
||||
title: 'Incident title',
|
||||
description: 'Incident description',
|
||||
};
|
||||
|
||||
const fields = mapParams(params, finalMapping);
|
||||
|
||||
expect(fields).toEqual({
|
||||
short_description: 'Incident title',
|
||||
description: 'Incident description',
|
||||
});
|
||||
});
|
||||
|
||||
test('do not add fields not in mapping', () => {
|
||||
const params = {
|
||||
savedObjectId: '123',
|
||||
incidentId: '456',
|
||||
title: 'Incident title',
|
||||
description: 'Incident description',
|
||||
};
|
||||
const fields = mapParams(params, finalMapping);
|
||||
|
||||
const { title, description, ...unexpectedFields } = params;
|
||||
|
||||
expect(fields).not.toEqual(expect.objectContaining(unexpectedFields));
|
||||
});
|
||||
});
|
||||
|
||||
describe('prepareFieldsForTransformation', () => {
|
||||
test('prepare fields with defaults', () => {
|
||||
const res = prepareFieldsForTransformation({
|
||||
externalCase: fullParams.externalObject,
|
||||
mapping: finalMapping,
|
||||
});
|
||||
expect(res).toEqual([
|
||||
{
|
||||
key: 'short_description',
|
||||
value: 'a title',
|
||||
actionType: 'overwrite',
|
||||
pipes: ['informationCreated'],
|
||||
},
|
||||
{
|
||||
key: 'description',
|
||||
value: 'a description',
|
||||
actionType: 'append',
|
||||
pipes: ['informationCreated', 'append'],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('prepare fields with default pipes', () => {
|
||||
const res = prepareFieldsForTransformation({
|
||||
externalCase: fullParams.externalObject,
|
||||
mapping: finalMapping,
|
||||
defaultPipes: ['myTestPipe'],
|
||||
});
|
||||
expect(res).toEqual([
|
||||
{
|
||||
key: 'short_description',
|
||||
value: 'a title',
|
||||
actionType: 'overwrite',
|
||||
pipes: ['myTestPipe'],
|
||||
},
|
||||
{
|
||||
key: 'description',
|
||||
value: 'a description',
|
||||
actionType: 'append',
|
||||
pipes: ['myTestPipe', 'append'],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformFields', () => {
|
||||
test('transform fields for creation correctly', () => {
|
||||
const fields = prepareFieldsForTransformation({
|
||||
externalCase: fullParams.externalObject,
|
||||
mapping: finalMapping,
|
||||
});
|
||||
|
||||
const res = transformFields<
|
||||
PushToServiceApiParams,
|
||||
{},
|
||||
{ short_description: string; description: string }
|
||||
>({
|
||||
params: fullParams,
|
||||
fields,
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
short_description: 'a title (created at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
description: 'a description (created at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
});
|
||||
});
|
||||
|
||||
test('transform fields for update correctly', () => {
|
||||
const fields = prepareFieldsForTransformation({
|
||||
externalCase: fullParams.externalObject,
|
||||
mapping: finalMapping,
|
||||
defaultPipes: ['informationUpdated'],
|
||||
});
|
||||
|
||||
const res = transformFields<
|
||||
PushToServiceApiParams,
|
||||
{},
|
||||
{ short_description: string; description: string }
|
||||
>({
|
||||
params: {
|
||||
...fullParams,
|
||||
updatedAt: '2020-03-15T08:34:53.450Z',
|
||||
updatedBy: {
|
||||
username: 'anotherUser',
|
||||
fullName: 'Another User',
|
||||
},
|
||||
},
|
||||
fields,
|
||||
currentIncident: {
|
||||
short_description: 'first title (created at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
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:
|
||||
'first description (created at 2020-03-13T08:34:53.450Z by Elastic User) \r\na description (updated at 2020-03-15T08:34:53.450Z by Another User)',
|
||||
});
|
||||
});
|
||||
|
||||
test('add newline character to description', () => {
|
||||
const fields = prepareFieldsForTransformation({
|
||||
externalCase: fullParams.externalObject,
|
||||
mapping: finalMapping,
|
||||
defaultPipes: ['informationUpdated'],
|
||||
});
|
||||
|
||||
const res = transformFields<
|
||||
PushToServiceApiParams,
|
||||
{},
|
||||
{ short_description: string; description: string }
|
||||
>({
|
||||
params: fullParams,
|
||||
fields,
|
||||
currentIncident: {
|
||||
short_description: 'first title',
|
||||
description: 'first description',
|
||||
},
|
||||
});
|
||||
expect(res.description?.includes('\r\n')).toBe(true);
|
||||
});
|
||||
|
||||
test('append username if fullname is undefined when create', () => {
|
||||
const fields = prepareFieldsForTransformation({
|
||||
externalCase: fullParams.externalObject,
|
||||
mapping: finalMapping,
|
||||
});
|
||||
|
||||
const res = transformFields<
|
||||
PushToServiceApiParams,
|
||||
{},
|
||||
{ short_description: string; description: string }
|
||||
>({
|
||||
params: {
|
||||
...fullParams,
|
||||
createdBy: { fullName: '', username: 'elastic' },
|
||||
},
|
||||
fields,
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
short_description: 'a title (created at 2020-03-13T08:34:53.450Z by elastic)',
|
||||
description: 'a description (created at 2020-03-13T08:34:53.450Z by elastic)',
|
||||
});
|
||||
});
|
||||
|
||||
test('append username if fullname is undefined when update', () => {
|
||||
const fields = prepareFieldsForTransformation({
|
||||
externalCase: fullParams.externalObject,
|
||||
mapping: finalMapping,
|
||||
defaultPipes: ['informationUpdated'],
|
||||
});
|
||||
|
||||
const res = transformFields<
|
||||
PushToServiceApiParams,
|
||||
{},
|
||||
{ short_description: string; description: string }
|
||||
>({
|
||||
params: {
|
||||
...fullParams,
|
||||
updatedAt: '2020-03-15T08:34:53.450Z',
|
||||
updatedBy: { username: 'anotherUser', fullName: '' },
|
||||
},
|
||||
fields,
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
short_description: 'a title (updated at 2020-03-15T08:34:53.450Z by anotherUser)',
|
||||
description: 'a description (updated at 2020-03-15T08:34:53.450Z by anotherUser)',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformComments', () => {
|
||||
test('transform creation comments', () => {
|
||||
const comments: Comment[] = [
|
||||
{
|
||||
commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631',
|
||||
comment: 'first comment',
|
||||
createdAt: '2020-03-13T08:34:53.450Z',
|
||||
createdBy: { fullName: 'Elastic User', username: 'elastic' },
|
||||
updatedAt: null,
|
||||
updatedBy: null,
|
||||
},
|
||||
];
|
||||
const res = transformComments(comments, ['informationCreated']);
|
||||
expect(res).toEqual([
|
||||
{
|
||||
commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631',
|
||||
comment: 'first comment (created at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
createdAt: '2020-03-13T08:34:53.450Z',
|
||||
createdBy: { fullName: 'Elastic User', username: 'elastic' },
|
||||
updatedAt: null,
|
||||
updatedBy: null,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('transform update comments', () => {
|
||||
const comments: Comment[] = [
|
||||
{
|
||||
commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631',
|
||||
comment: 'first comment',
|
||||
createdAt: '2020-03-13T08:34:53.450Z',
|
||||
createdBy: { fullName: 'Elastic User', username: 'elastic' },
|
||||
updatedAt: '2020-03-15T08:34:53.450Z',
|
||||
updatedBy: {
|
||||
fullName: 'Another User',
|
||||
username: 'anotherUser',
|
||||
},
|
||||
},
|
||||
];
|
||||
const res = transformComments(comments, ['informationUpdated']);
|
||||
expect(res).toEqual([
|
||||
{
|
||||
commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631',
|
||||
comment: 'first comment (updated at 2020-03-15T08:34:53.450Z by Another User)',
|
||||
createdAt: '2020-03-13T08:34:53.450Z',
|
||||
createdBy: { fullName: 'Elastic User', username: 'elastic' },
|
||||
updatedAt: '2020-03-15T08:34:53.450Z',
|
||||
updatedBy: {
|
||||
fullName: 'Another User',
|
||||
username: 'anotherUser',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('transform added comments', () => {
|
||||
const comments: Comment[] = [
|
||||
{
|
||||
commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631',
|
||||
comment: 'first comment',
|
||||
createdAt: '2020-03-13T08:34:53.450Z',
|
||||
createdBy: { fullName: 'Elastic User', username: 'elastic' },
|
||||
updatedAt: null,
|
||||
updatedBy: null,
|
||||
},
|
||||
];
|
||||
const res = transformComments(comments, ['informationAdded']);
|
||||
expect(res).toEqual([
|
||||
{
|
||||
commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631',
|
||||
comment: 'first comment (added at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
createdAt: '2020-03-13T08:34:53.450Z',
|
||||
createdBy: { fullName: 'Elastic User', username: 'elastic' },
|
||||
updatedAt: null,
|
||||
updatedBy: null,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('transform comments without fullname', () => {
|
||||
const comments: Comment[] = [
|
||||
{
|
||||
commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631',
|
||||
comment: 'first comment',
|
||||
createdAt: '2020-03-13T08:34:53.450Z',
|
||||
createdBy: { fullName: '', username: 'elastic' },
|
||||
updatedAt: null,
|
||||
updatedBy: null,
|
||||
},
|
||||
];
|
||||
const res = transformComments(comments, ['informationAdded']);
|
||||
expect(res).toEqual([
|
||||
{
|
||||
commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631',
|
||||
comment: 'first comment (added at 2020-03-13T08:34:53.450Z by elastic)',
|
||||
createdAt: '2020-03-13T08:34:53.450Z',
|
||||
createdBy: { fullName: '', username: 'elastic' },
|
||||
updatedAt: null,
|
||||
updatedBy: null,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('adds update user correctly', () => {
|
||||
const comments: Comment[] = [
|
||||
{
|
||||
commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631',
|
||||
comment: 'first comment',
|
||||
createdAt: '2020-03-13T08:34:53.450Z',
|
||||
createdBy: { fullName: 'Elastic', username: 'elastic' },
|
||||
updatedAt: '2020-04-13T08:34:53.450Z',
|
||||
updatedBy: { fullName: 'Elastic2', username: 'elastic' },
|
||||
},
|
||||
];
|
||||
const res = transformComments(comments, ['informationAdded']);
|
||||
expect(res).toEqual([
|
||||
{
|
||||
commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631',
|
||||
comment: 'first comment (added at 2020-04-13T08:34:53.450Z by Elastic2)',
|
||||
createdAt: '2020-03-13T08:34:53.450Z',
|
||||
createdBy: { fullName: 'Elastic', username: 'elastic' },
|
||||
updatedAt: '2020-04-13T08:34:53.450Z',
|
||||
updatedBy: { fullName: 'Elastic2', username: 'elastic' },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('adds update user with empty fullname correctly', () => {
|
||||
const comments: Comment[] = [
|
||||
{
|
||||
commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631',
|
||||
comment: 'first comment',
|
||||
createdAt: '2020-03-13T08:34:53.450Z',
|
||||
createdBy: { fullName: 'Elastic', username: 'elastic' },
|
||||
updatedAt: '2020-04-13T08:34:53.450Z',
|
||||
updatedBy: { fullName: '', username: 'elastic2' },
|
||||
},
|
||||
];
|
||||
const res = transformComments(comments, ['informationAdded']);
|
||||
expect(res).toEqual([
|
||||
{
|
||||
commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631',
|
||||
comment: 'first comment (added at 2020-04-13T08:34:53.450Z by elastic2)',
|
||||
createdAt: '2020-03-13T08:34:53.450Z',
|
||||
createdBy: { fullName: 'Elastic', username: 'elastic' },
|
||||
updatedAt: '2020-04-13T08:34:53.450Z',
|
||||
updatedBy: { fullName: '', username: 'elastic2' },
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -1,111 +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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { flow, get } from 'lodash';
|
||||
|
||||
import {
|
||||
MapRecord,
|
||||
TransformFieldsArgs,
|
||||
Comment,
|
||||
EntityInformation,
|
||||
PipedField,
|
||||
AnyParams,
|
||||
PrepareFieldsForTransformArgs,
|
||||
} from './types';
|
||||
|
||||
import { transformers } from './transformers';
|
||||
|
||||
import { SUPPORTED_SOURCE_FIELDS } from './constants';
|
||||
|
||||
export const normalizeMapping = (supportedFields: string[], mapping: MapRecord[]): MapRecord[] => {
|
||||
// Prevent prototype pollution and remove unsupported fields
|
||||
return mapping.filter(
|
||||
(m) =>
|
||||
m.source !== '__proto__' && m.target !== '__proto__' && supportedFields.includes(m.source)
|
||||
);
|
||||
};
|
||||
|
||||
export const buildMap = (mapping: MapRecord[]): Map<string, MapRecord> => {
|
||||
return normalizeMapping(SUPPORTED_SOURCE_FIELDS, mapping).reduce((fieldsMap, field) => {
|
||||
const { source, target, actionType } = field;
|
||||
fieldsMap.set(source, { target, actionType });
|
||||
fieldsMap.set(target, { target: source, actionType });
|
||||
return fieldsMap;
|
||||
}, new Map());
|
||||
};
|
||||
|
||||
export const mapParams = <T extends {}>(params: T, mapping: Map<string, MapRecord>): AnyParams => {
|
||||
return Object.keys(params).reduce((prev: AnyParams, curr: string): AnyParams => {
|
||||
const field = mapping.get(curr);
|
||||
if (field) {
|
||||
prev[field.target] = get(params, curr);
|
||||
}
|
||||
return prev;
|
||||
}, {});
|
||||
};
|
||||
|
||||
export const prepareFieldsForTransformation = ({
|
||||
externalCase,
|
||||
mapping,
|
||||
defaultPipes = ['informationCreated'],
|
||||
}: PrepareFieldsForTransformArgs): PipedField[] => {
|
||||
return Object.keys(externalCase)
|
||||
.filter((p) => mapping.get(p)?.actionType != null && mapping.get(p)?.actionType !== 'nothing')
|
||||
.map((p) => {
|
||||
const actionType = mapping.get(p)?.actionType ?? 'nothing';
|
||||
return {
|
||||
key: p,
|
||||
value: externalCase[p],
|
||||
actionType,
|
||||
pipes: actionType === 'append' ? [...defaultPipes, 'append'] : defaultPipes,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const transformFields = <
|
||||
P extends EntityInformation,
|
||||
S extends Record<string, unknown>,
|
||||
R extends {}
|
||||
>({
|
||||
params,
|
||||
fields,
|
||||
currentIncident,
|
||||
}: TransformFieldsArgs<P, S>): R => {
|
||||
return fields.reduce((prev, cur) => {
|
||||
const transform = flow(...cur.pipes.map((p) => transformers[p]));
|
||||
return {
|
||||
...prev,
|
||||
[cur.key]: transform({
|
||||
value: cur.value,
|
||||
date: params.updatedAt ?? params.createdAt,
|
||||
user: getEntity(params),
|
||||
previousValue: currentIncident ? currentIncident[cur.key] : '',
|
||||
}).value,
|
||||
};
|
||||
}, {} as R);
|
||||
};
|
||||
|
||||
export const transformComments = (comments: Comment[], pipes: string[]): Comment[] => {
|
||||
return comments.map((c) => ({
|
||||
...c,
|
||||
comment: flow(...pipes.map((p) => transformers[p]))({
|
||||
value: c.comment,
|
||||
date: c.updatedAt ?? c.createdAt,
|
||||
user: getEntity(c),
|
||||
}).value,
|
||||
}));
|
||||
};
|
||||
|
||||
export const getEntity = (entity: EntityInformation): string =>
|
||||
(entity.updatedBy != null
|
||||
? entity.updatedBy.fullName
|
||||
? entity.updatedBy.fullName
|
||||
: entity.updatedBy.username
|
||||
: entity.createdBy != null
|
||||
? entity.createdBy.fullName
|
||||
? entity.createdBy.fullName
|
||||
: entity.createdBy.username
|
||||
: '') ?? '';
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { Logger } from '../../../../../../src/core/server';
|
||||
import { externalServiceMock, mapping, apiParams } from './mocks';
|
||||
import { externalServiceMock, apiParams } from './mocks';
|
||||
import { ExternalService } from './types';
|
||||
import { api } from './api';
|
||||
let mockedLogger: jest.Mocked<Logger>;
|
||||
|
@ -22,7 +22,6 @@ describe('api', () => {
|
|||
const params = { ...apiParams, externalId: null };
|
||||
const res = await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
|
@ -49,7 +48,6 @@ describe('api', () => {
|
|||
const params = { ...apiParams, externalId: null, comments: [] };
|
||||
const res = await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
|
@ -63,8 +61,8 @@ describe('api', () => {
|
|||
});
|
||||
|
||||
test('it calls createIncident correctly', async () => {
|
||||
const params = { ...apiParams, externalId: null };
|
||||
await api.pushToService({ externalService, mapping, params, logger: mockedLogger });
|
||||
const params = { ...apiParams, incident: { ...apiParams.incident, externalId: null } };
|
||||
await api.pushToService({ externalService, params, logger: mockedLogger });
|
||||
|
||||
expect(externalService.createIncident).toHaveBeenCalledWith({
|
||||
incident: {
|
||||
|
@ -72,16 +70,16 @@ describe('api', () => {
|
|||
priority: 'High',
|
||||
issueType: '10006',
|
||||
parent: null,
|
||||
description: 'Incident description (created at 2020-04-27T10:59:46.202Z by Elastic User)',
|
||||
summary: 'Incident title (created at 2020-04-27T10:59:46.202Z by Elastic User)',
|
||||
description: 'Incident description',
|
||||
summary: 'Incident title',
|
||||
},
|
||||
});
|
||||
expect(externalService.updateIncident).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('it calls createIncident correctly without mapping', async () => {
|
||||
const params = { ...apiParams, externalId: null };
|
||||
await api.pushToService({ externalService, mapping: null, params, logger: mockedLogger });
|
||||
const params = { ...apiParams, incident: { ...apiParams.incident, externalId: null } };
|
||||
await api.pushToService({ externalService, params, logger: mockedLogger });
|
||||
|
||||
expect(externalService.createIncident).toHaveBeenCalledWith({
|
||||
incident: {
|
||||
|
@ -97,65 +95,35 @@ describe('api', () => {
|
|||
});
|
||||
|
||||
test('it calls createComment correctly', async () => {
|
||||
const params = { ...apiParams, externalId: null };
|
||||
await api.pushToService({ externalService, mapping, params, logger: mockedLogger });
|
||||
expect(externalService.createComment).toHaveBeenCalledTimes(2);
|
||||
expect(externalService.createComment).toHaveBeenNthCalledWith(1, {
|
||||
incidentId: 'incident-1',
|
||||
comment: {
|
||||
commentId: 'case-comment-1',
|
||||
comment: 'A comment (added at 2020-04-27T10:59:46.202Z by Elastic User)',
|
||||
createdAt: '2020-04-27T10:59:46.202Z',
|
||||
createdBy: {
|
||||
fullName: 'Elastic User',
|
||||
username: 'elastic',
|
||||
},
|
||||
updatedAt: '2020-04-27T10:59:46.202Z',
|
||||
updatedBy: {
|
||||
fullName: 'Elastic User',
|
||||
username: 'elastic',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(externalService.createComment).toHaveBeenNthCalledWith(2, {
|
||||
incidentId: 'incident-1',
|
||||
comment: {
|
||||
commentId: 'case-comment-2',
|
||||
comment: 'Another comment (added at 2020-04-27T10:59:46.202Z by Elastic User)',
|
||||
createdAt: '2020-04-27T10:59:46.202Z',
|
||||
createdBy: {
|
||||
fullName: 'Elastic User',
|
||||
username: 'elastic',
|
||||
},
|
||||
updatedAt: '2020-04-27T10:59:46.202Z',
|
||||
updatedBy: {
|
||||
fullName: 'Elastic User',
|
||||
username: 'elastic',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('it calls createComment correctly without mapping', async () => {
|
||||
const params = { ...apiParams, externalId: null };
|
||||
await api.pushToService({ externalService, mapping: null, params, logger: mockedLogger });
|
||||
const params = { ...apiParams, incident: { ...apiParams.incident, externalId: null } };
|
||||
await api.pushToService({ externalService, params, logger: mockedLogger });
|
||||
expect(externalService.createComment).toHaveBeenCalledTimes(2);
|
||||
expect(externalService.createComment).toHaveBeenNthCalledWith(1, {
|
||||
incidentId: 'incident-1',
|
||||
comment: {
|
||||
commentId: 'case-comment-1',
|
||||
comment: 'A comment',
|
||||
},
|
||||
});
|
||||
|
||||
expect(externalService.createComment).toHaveBeenNthCalledWith(2, {
|
||||
incidentId: 'incident-1',
|
||||
comment: {
|
||||
commentId: 'case-comment-2',
|
||||
comment: 'Another comment',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('it calls createComment correctly without mapping', async () => {
|
||||
const params = { ...apiParams, incident: { ...apiParams.incident, externalId: null } };
|
||||
await api.pushToService({ externalService, params, logger: mockedLogger });
|
||||
expect(externalService.createComment).toHaveBeenCalledTimes(2);
|
||||
expect(externalService.createComment).toHaveBeenNthCalledWith(1, {
|
||||
incidentId: 'incident-1',
|
||||
comment: {
|
||||
commentId: 'case-comment-1',
|
||||
comment: 'A comment',
|
||||
createdAt: '2020-04-27T10:59:46.202Z',
|
||||
createdBy: {
|
||||
fullName: 'Elastic User',
|
||||
username: 'elastic',
|
||||
},
|
||||
updatedAt: '2020-04-27T10:59:46.202Z',
|
||||
updatedBy: {
|
||||
fullName: 'Elastic User',
|
||||
username: 'elastic',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -164,16 +132,6 @@ describe('api', () => {
|
|||
comment: {
|
||||
commentId: 'case-comment-2',
|
||||
comment: 'Another comment',
|
||||
createdAt: '2020-04-27T10:59:46.202Z',
|
||||
createdBy: {
|
||||
fullName: 'Elastic User',
|
||||
username: 'elastic',
|
||||
},
|
||||
updatedAt: '2020-04-27T10:59:46.202Z',
|
||||
updatedBy: {
|
||||
fullName: 'Elastic User',
|
||||
username: 'elastic',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -183,7 +141,6 @@ describe('api', () => {
|
|||
test('it updates an incident', async () => {
|
||||
const res = await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
|
@ -210,7 +167,6 @@ describe('api', () => {
|
|||
const params = { ...apiParams, comments: [] };
|
||||
const res = await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
|
@ -225,7 +181,7 @@ describe('api', () => {
|
|||
|
||||
test('it calls updateIncident correctly', async () => {
|
||||
const params = { ...apiParams };
|
||||
await api.pushToService({ externalService, mapping, params, logger: mockedLogger });
|
||||
await api.pushToService({ externalService, params, logger: mockedLogger });
|
||||
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
|
@ -234,8 +190,8 @@ describe('api', () => {
|
|||
priority: 'High',
|
||||
issueType: '10006',
|
||||
parent: null,
|
||||
description: 'Incident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)',
|
||||
summary: 'Incident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)',
|
||||
description: 'Incident description',
|
||||
summary: 'Incident title',
|
||||
},
|
||||
});
|
||||
expect(externalService.createIncident).not.toHaveBeenCalled();
|
||||
|
@ -243,7 +199,7 @@ describe('api', () => {
|
|||
|
||||
test('it calls updateIncident correctly without mapping', async () => {
|
||||
const params = { ...apiParams };
|
||||
await api.pushToService({ externalService, mapping: null, params, logger: mockedLogger });
|
||||
await api.pushToService({ externalService, params, logger: mockedLogger });
|
||||
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
|
@ -261,64 +217,34 @@ describe('api', () => {
|
|||
|
||||
test('it calls createComment correctly', async () => {
|
||||
const params = { ...apiParams };
|
||||
await api.pushToService({ externalService, mapping, params, logger: mockedLogger });
|
||||
expect(externalService.createComment).toHaveBeenCalledTimes(2);
|
||||
expect(externalService.createComment).toHaveBeenNthCalledWith(1, {
|
||||
incidentId: 'incident-1',
|
||||
comment: {
|
||||
commentId: 'case-comment-1',
|
||||
comment: 'A comment (added at 2020-04-27T10:59:46.202Z by Elastic User)',
|
||||
createdAt: '2020-04-27T10:59:46.202Z',
|
||||
createdBy: {
|
||||
fullName: 'Elastic User',
|
||||
username: 'elastic',
|
||||
},
|
||||
updatedAt: '2020-04-27T10:59:46.202Z',
|
||||
updatedBy: {
|
||||
fullName: 'Elastic User',
|
||||
username: 'elastic',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(externalService.createComment).toHaveBeenNthCalledWith(2, {
|
||||
incidentId: 'incident-1',
|
||||
comment: {
|
||||
commentId: 'case-comment-2',
|
||||
comment: 'Another comment (added at 2020-04-27T10:59:46.202Z by Elastic User)',
|
||||
createdAt: '2020-04-27T10:59:46.202Z',
|
||||
createdBy: {
|
||||
fullName: 'Elastic User',
|
||||
username: 'elastic',
|
||||
},
|
||||
updatedAt: '2020-04-27T10:59:46.202Z',
|
||||
updatedBy: {
|
||||
fullName: 'Elastic User',
|
||||
username: 'elastic',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('it calls createComment correctly without mapping', async () => {
|
||||
const params = { ...apiParams };
|
||||
await api.pushToService({ externalService, mapping: null, params, logger: mockedLogger });
|
||||
await api.pushToService({ externalService, params, logger: mockedLogger });
|
||||
expect(externalService.createComment).toHaveBeenCalledTimes(2);
|
||||
expect(externalService.createComment).toHaveBeenNthCalledWith(1, {
|
||||
incidentId: 'incident-1',
|
||||
comment: {
|
||||
commentId: 'case-comment-1',
|
||||
comment: 'A comment',
|
||||
},
|
||||
});
|
||||
|
||||
expect(externalService.createComment).toHaveBeenNthCalledWith(2, {
|
||||
incidentId: 'incident-1',
|
||||
comment: {
|
||||
commentId: 'case-comment-2',
|
||||
comment: 'Another comment',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('it calls createComment correctly without mapping', async () => {
|
||||
const params = { ...apiParams };
|
||||
await api.pushToService({ externalService, params, logger: mockedLogger });
|
||||
expect(externalService.createComment).toHaveBeenCalledTimes(2);
|
||||
expect(externalService.createComment).toHaveBeenNthCalledWith(1, {
|
||||
incidentId: 'incident-1',
|
||||
comment: {
|
||||
commentId: 'case-comment-1',
|
||||
comment: 'A comment',
|
||||
createdAt: '2020-04-27T10:59:46.202Z',
|
||||
createdBy: {
|
||||
fullName: 'Elastic User',
|
||||
username: 'elastic',
|
||||
},
|
||||
updatedAt: '2020-04-27T10:59:46.202Z',
|
||||
updatedBy: {
|
||||
fullName: 'Elastic User',
|
||||
username: 'elastic',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -327,16 +253,6 @@ describe('api', () => {
|
|||
comment: {
|
||||
commentId: 'case-comment-2',
|
||||
comment: 'Another comment',
|
||||
createdAt: '2020-04-27T10:59:46.202Z',
|
||||
createdBy: {
|
||||
fullName: 'Elastic User',
|
||||
username: 'elastic',
|
||||
},
|
||||
updatedAt: '2020-04-27T10:59:46.202Z',
|
||||
updatedBy: {
|
||||
fullName: 'Elastic User',
|
||||
username: 'elastic',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -411,396 +327,4 @@ describe('api', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapping variations', () => {
|
||||
test('overwrite & append', async () => {
|
||||
mapping.set('title', {
|
||||
target: 'summary',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
mapping.set('description', {
|
||||
target: 'description',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('comments', {
|
||||
target: 'comments',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('summary', {
|
||||
target: 'title',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
labels: ['kibana', 'elastic'],
|
||||
priority: 'High',
|
||||
issueType: '10006',
|
||||
parent: null,
|
||||
summary: 'Incident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)',
|
||||
description:
|
||||
'description from jira \r\nIncident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('nothing & append', async () => {
|
||||
mapping.set('title', {
|
||||
target: 'summary',
|
||||
actionType: 'nothing',
|
||||
});
|
||||
|
||||
mapping.set('description', {
|
||||
target: 'description',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('comments', {
|
||||
target: 'comments',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('summary', {
|
||||
target: 'title',
|
||||
actionType: 'nothing',
|
||||
});
|
||||
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
labels: ['kibana', 'elastic'],
|
||||
priority: 'High',
|
||||
issueType: '10006',
|
||||
parent: null,
|
||||
description:
|
||||
'description from jira \r\nIncident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('append & append', async () => {
|
||||
mapping.set('title', {
|
||||
target: 'summary',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('description', {
|
||||
target: 'description',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('comments', {
|
||||
target: 'comments',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('summary', {
|
||||
target: 'title',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
labels: ['kibana', 'elastic'],
|
||||
priority: 'High',
|
||||
issueType: '10006',
|
||||
parent: null,
|
||||
summary:
|
||||
'title from jira \r\nIncident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)',
|
||||
description:
|
||||
'description from jira \r\nIncident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('nothing & nothing', async () => {
|
||||
mapping.set('title', {
|
||||
target: 'summary',
|
||||
actionType: 'nothing',
|
||||
});
|
||||
|
||||
mapping.set('description', {
|
||||
target: 'description',
|
||||
actionType: 'nothing',
|
||||
});
|
||||
|
||||
mapping.set('comments', {
|
||||
target: 'comments',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('summary', {
|
||||
target: 'title',
|
||||
actionType: 'nothing',
|
||||
});
|
||||
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
labels: ['kibana', 'elastic'],
|
||||
priority: 'High',
|
||||
issueType: '10006',
|
||||
parent: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('overwrite & nothing', async () => {
|
||||
mapping.set('title', {
|
||||
target: 'summary',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
mapping.set('description', {
|
||||
target: 'description',
|
||||
actionType: 'nothing',
|
||||
});
|
||||
|
||||
mapping.set('comments', {
|
||||
target: 'comments',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('summary', {
|
||||
target: 'title',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
labels: ['kibana', 'elastic'],
|
||||
priority: 'High',
|
||||
issueType: '10006',
|
||||
parent: null,
|
||||
summary: 'Incident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('overwrite & overwrite', async () => {
|
||||
mapping.set('title', {
|
||||
target: 'summary',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
mapping.set('description', {
|
||||
target: 'description',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
mapping.set('comments', {
|
||||
target: 'comments',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('summary', {
|
||||
target: 'title',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
labels: ['kibana', 'elastic'],
|
||||
priority: 'High',
|
||||
issueType: '10006',
|
||||
parent: null,
|
||||
summary: 'Incident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)',
|
||||
description: 'Incident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('nothing & overwrite', async () => {
|
||||
mapping.set('title', {
|
||||
target: 'summary',
|
||||
actionType: 'nothing',
|
||||
});
|
||||
|
||||
mapping.set('description', {
|
||||
target: 'description',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
mapping.set('comments', {
|
||||
target: 'comments',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('summary', {
|
||||
target: 'title',
|
||||
actionType: 'nothing',
|
||||
});
|
||||
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
labels: ['kibana', 'elastic'],
|
||||
priority: 'High',
|
||||
issueType: '10006',
|
||||
parent: null,
|
||||
description: 'Incident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('append & overwrite', async () => {
|
||||
mapping.set('title', {
|
||||
target: 'summary',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('description', {
|
||||
target: 'description',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
mapping.set('comments', {
|
||||
target: 'comments',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('summary', {
|
||||
target: 'title',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
labels: ['kibana', 'elastic'],
|
||||
priority: 'High',
|
||||
issueType: '10006',
|
||||
parent: null,
|
||||
summary:
|
||||
'title from jira \r\nIncident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)',
|
||||
description: 'Incident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('append & nothing', async () => {
|
||||
mapping.set('title', {
|
||||
target: 'summary',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('description', {
|
||||
target: 'description',
|
||||
actionType: 'nothing',
|
||||
});
|
||||
|
||||
mapping.set('comments', {
|
||||
target: 'comments',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('summary', {
|
||||
target: 'title',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
labels: ['kibana', 'elastic'],
|
||||
priority: 'High',
|
||||
issueType: '10006',
|
||||
parent: null,
|
||||
summary:
|
||||
'title from jira \r\nIncident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('comment nothing', async () => {
|
||||
mapping.set('title', {
|
||||
target: 'summary',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
mapping.set('description', {
|
||||
target: 'description',
|
||||
actionType: 'nothing',
|
||||
});
|
||||
|
||||
mapping.set('comments', {
|
||||
target: 'comments',
|
||||
actionType: 'nothing',
|
||||
});
|
||||
|
||||
mapping.set('summary', {
|
||||
target: 'title',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(externalService.createComment).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
ExternalServiceParams,
|
||||
PushToServiceApiHandlerArgs,
|
||||
HandshakeApiHandlerArgs,
|
||||
GetIncidentApiHandlerArgs,
|
||||
|
@ -14,26 +13,17 @@ import {
|
|||
GetFieldsByIssueTypeHandlerArgs,
|
||||
GetIssueTypesHandlerArgs,
|
||||
GetIssuesHandlerArgs,
|
||||
PushToServiceApiParams,
|
||||
PushToServiceResponse,
|
||||
GetIssueHandlerArgs,
|
||||
GetCommonFieldsHandlerArgs,
|
||||
} from './types';
|
||||
|
||||
// TODO: to remove, need to support Case
|
||||
import { prepareFieldsForTransformation, transformFields, transformComments } from '../case/utils';
|
||||
const handshakeHandler = async ({ externalService, params }: HandshakeApiHandlerArgs) => {};
|
||||
|
||||
const handshakeHandler = async ({
|
||||
externalService,
|
||||
mapping,
|
||||
params,
|
||||
}: HandshakeApiHandlerArgs) => {};
|
||||
|
||||
const getIncidentHandler = async ({
|
||||
externalService,
|
||||
mapping,
|
||||
params,
|
||||
}: GetIncidentApiHandlerArgs) => {};
|
||||
const getIncidentHandler = async ({ externalService, params }: GetIncidentApiHandlerArgs) => {
|
||||
const res = await externalService.getIncident(params.externalId);
|
||||
return res;
|
||||
};
|
||||
|
||||
const getIssueTypesHandler = async ({ externalService }: GetIssueTypesHandlerArgs) => {
|
||||
const res = await externalService.getIssueTypes();
|
||||
|
@ -68,58 +58,12 @@ const getIssueHandler = async ({ externalService, params }: GetIssueHandlerArgs)
|
|||
|
||||
const pushToServiceHandler = async ({
|
||||
externalService,
|
||||
mapping,
|
||||
params,
|
||||
logger,
|
||||
}: PushToServiceApiHandlerArgs): Promise<PushToServiceResponse> => {
|
||||
const { externalId, comments } = params;
|
||||
const updateIncident = externalId ? true : false;
|
||||
const defaultPipes = updateIncident ? ['informationUpdated'] : ['informationCreated'];
|
||||
let currentIncident: ExternalServiceParams | undefined;
|
||||
const { comments } = params;
|
||||
let res: PushToServiceResponse;
|
||||
|
||||
if (externalId) {
|
||||
try {
|
||||
currentIncident = await externalService.getIncident(externalId);
|
||||
} catch (ex) {
|
||||
logger.debug(
|
||||
`Retrieving Incident by id ${externalId} from Jira failed with exception: ${ex}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let incident: Incident;
|
||||
// TODO: should be removed later but currently keep it for the Case implementation support
|
||||
if (mapping) {
|
||||
const fields = prepareFieldsForTransformation({
|
||||
externalCase: params.externalObject,
|
||||
mapping,
|
||||
defaultPipes,
|
||||
});
|
||||
|
||||
const transformedFields = transformFields<
|
||||
PushToServiceApiParams,
|
||||
ExternalServiceParams,
|
||||
Incident
|
||||
>({
|
||||
params,
|
||||
fields,
|
||||
currentIncident,
|
||||
});
|
||||
|
||||
const { priority, labels, issueType, parent } = params;
|
||||
incident = {
|
||||
summary: transformedFields.summary,
|
||||
description: transformedFields.description,
|
||||
priority,
|
||||
labels,
|
||||
issueType,
|
||||
parent,
|
||||
};
|
||||
} else {
|
||||
const { title, description, priority, labels, issueType, parent } = params;
|
||||
incident = { summary: title, description, priority, labels, issueType, parent };
|
||||
}
|
||||
const { externalId, ...rest } = params.incident;
|
||||
const incident: Incident = rest;
|
||||
|
||||
if (externalId != null) {
|
||||
res = await externalService.updateIncident({
|
||||
|
@ -128,23 +72,13 @@ const pushToServiceHandler = async ({
|
|||
});
|
||||
} else {
|
||||
res = await externalService.createIncident({
|
||||
incident: {
|
||||
...incident,
|
||||
},
|
||||
incident,
|
||||
});
|
||||
}
|
||||
|
||||
if (comments && Array.isArray(comments) && comments.length > 0) {
|
||||
if (mapping && mapping.get('comments')?.actionType === 'nothing') {
|
||||
return res;
|
||||
}
|
||||
|
||||
const commentsTransformed = mapping
|
||||
? transformComments(comments, ['informationAdded'])
|
||||
: comments;
|
||||
|
||||
res.comments = [];
|
||||
for (const currentComment of commentsTransformed) {
|
||||
for (const currentComment of comments) {
|
||||
const comment = await externalService.createComment({
|
||||
incidentId: res.id,
|
||||
comment: currentComment,
|
||||
|
|
|
@ -27,13 +27,11 @@ import {
|
|||
ExecutorSubActionGetIssueTypesParams,
|
||||
ExecutorSubActionGetIssuesParams,
|
||||
ExecutorSubActionGetIssueParams,
|
||||
ExecutorSubActionGetIncidentParams,
|
||||
} from './types';
|
||||
import * as i18n from './translations';
|
||||
import { Logger } from '../../../../../../src/core/server';
|
||||
|
||||
// TODO: to remove, need to support Case
|
||||
import { buildMap, mapParams } from '../case/utils';
|
||||
|
||||
interface GetActionTypeParams {
|
||||
logger: Logger;
|
||||
configurationUtilities: ActionsConfigurationUtilities;
|
||||
|
@ -41,6 +39,7 @@ interface GetActionTypeParams {
|
|||
|
||||
const supportedSubActions: string[] = [
|
||||
'getFields',
|
||||
'getIncident',
|
||||
'pushToService',
|
||||
'issueTypes',
|
||||
'fieldsByIssueType',
|
||||
|
@ -109,21 +108,22 @@ async function executor(
|
|||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
if (subAction === 'getIncident') {
|
||||
const getIncidentParams = subActionParams as ExecutorSubActionGetIncidentParams;
|
||||
const res = await api.getIncident({
|
||||
externalService,
|
||||
params: getIncidentParams,
|
||||
});
|
||||
if (res != null) {
|
||||
data = res;
|
||||
}
|
||||
}
|
||||
if (subAction === 'pushToService') {
|
||||
const pushToServiceParams = subActionParams as ExecutorSubActionPushParams;
|
||||
|
||||
const { comments, externalId, ...restParams } = pushToServiceParams;
|
||||
const incidentConfiguration = config.incidentConfiguration;
|
||||
const mapping = incidentConfiguration ? buildMap(incidentConfiguration.mapping) : null;
|
||||
const externalObject =
|
||||
config.incidentConfiguration && mapping
|
||||
? mapParams<ExecutorSubActionPushParams>(restParams as ExecutorSubActionPushParams, mapping)
|
||||
: {};
|
||||
|
||||
data = await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: { ...pushToServiceParams, externalObject },
|
||||
params: pushToServiceParams,
|
||||
logger,
|
||||
});
|
||||
|
||||
|
|
|
@ -6,8 +6,6 @@
|
|||
|
||||
import { ExternalService, PushToServiceApiParams, ExecutorSubActionPushParams } from './types';
|
||||
|
||||
import { MapRecord } from '../case/types';
|
||||
|
||||
const createMock = (): jest.Mocked<ExternalService> => {
|
||||
const service = {
|
||||
getIncident: jest.fn().mockImplementation(() =>
|
||||
|
@ -111,64 +109,31 @@ const createMock = (): jest.Mocked<ExternalService> => {
|
|||
const externalServiceMock = {
|
||||
create: createMock,
|
||||
};
|
||||
const mapping: Map<string, Partial<MapRecord>> = new Map();
|
||||
|
||||
mapping.set('title', {
|
||||
target: 'summary',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
mapping.set('description', {
|
||||
target: 'description',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
mapping.set('comments', {
|
||||
target: 'comments',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('summary', {
|
||||
target: 'title',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
const executorParams: ExecutorSubActionPushParams = {
|
||||
savedObjectId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa',
|
||||
externalId: 'incident-3',
|
||||
createdAt: '2020-04-27T10:59:46.202Z',
|
||||
createdBy: { fullName: 'Elastic User', username: 'elastic' },
|
||||
updatedAt: '2020-04-27T10:59:46.202Z',
|
||||
updatedBy: { fullName: 'Elastic User', username: 'elastic' },
|
||||
title: 'Incident title',
|
||||
description: 'Incident description',
|
||||
labels: ['kibana', 'elastic'],
|
||||
priority: 'High',
|
||||
issueType: '10006',
|
||||
parent: null,
|
||||
incident: {
|
||||
externalId: 'incident-3',
|
||||
summary: 'Incident title',
|
||||
description: 'Incident description',
|
||||
labels: ['kibana', 'elastic'],
|
||||
priority: 'High',
|
||||
issueType: '10006',
|
||||
parent: null,
|
||||
},
|
||||
comments: [
|
||||
{
|
||||
commentId: 'case-comment-1',
|
||||
comment: 'A comment',
|
||||
createdAt: '2020-04-27T10:59:46.202Z',
|
||||
createdBy: { fullName: 'Elastic User', username: 'elastic' },
|
||||
updatedAt: '2020-04-27T10:59:46.202Z',
|
||||
updatedBy: { fullName: 'Elastic User', username: 'elastic' },
|
||||
},
|
||||
{
|
||||
commentId: 'case-comment-2',
|
||||
comment: 'Another comment',
|
||||
createdAt: '2020-04-27T10:59:46.202Z',
|
||||
createdBy: { fullName: 'Elastic User', username: 'elastic' },
|
||||
updatedAt: '2020-04-27T10:59:46.202Z',
|
||||
updatedBy: { fullName: 'Elastic User', username: 'elastic' },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const apiParams: PushToServiceApiParams = {
|
||||
...executorParams,
|
||||
externalObject: { summary: 'Incident title', description: 'Incident description' },
|
||||
};
|
||||
|
||||
export { externalServiceMock, mapping, executorParams, apiParams };
|
||||
export { externalServiceMock, executorParams, apiParams };
|
||||
|
|
|
@ -5,14 +5,10 @@
|
|||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { CommentSchema, EntityInformation, IncidentConfigurationSchema } from '../case/schema';
|
||||
|
||||
export const ExternalIncidentServiceConfiguration = {
|
||||
apiUrl: schema.string(),
|
||||
projectKey: schema.string(),
|
||||
// TODO: to remove - set it optional for the current stage to support Case Jira implementation
|
||||
incidentConfiguration: schema.nullable(IncidentConfigurationSchema),
|
||||
isCaseOwned: schema.nullable(schema.boolean()),
|
||||
};
|
||||
|
||||
export const ExternalIncidentServiceConfigurationSchema = schema.object(
|
||||
|
@ -37,17 +33,23 @@ export const ExecutorSubActionSchema = schema.oneOf([
|
|||
]);
|
||||
|
||||
export const ExecutorSubActionPushParamsSchema = schema.object({
|
||||
savedObjectId: schema.nullable(schema.string()),
|
||||
title: schema.string(),
|
||||
description: schema.nullable(schema.string()),
|
||||
externalId: schema.nullable(schema.string()),
|
||||
issueType: schema.nullable(schema.string()),
|
||||
priority: schema.nullable(schema.string()),
|
||||
labels: schema.nullable(schema.arrayOf(schema.string())),
|
||||
parent: schema.nullable(schema.string()),
|
||||
// TODO: modify later to string[] - need for support Case schema
|
||||
comments: schema.nullable(schema.arrayOf(CommentSchema)),
|
||||
...EntityInformation,
|
||||
incident: schema.object({
|
||||
summary: schema.string(),
|
||||
description: schema.nullable(schema.string()),
|
||||
externalId: schema.nullable(schema.string()),
|
||||
issueType: schema.nullable(schema.string()),
|
||||
priority: schema.nullable(schema.string()),
|
||||
labels: schema.nullable(schema.arrayOf(schema.string())),
|
||||
parent: schema.nullable(schema.string()),
|
||||
}),
|
||||
comments: schema.nullable(
|
||||
schema.arrayOf(
|
||||
schema.object({
|
||||
comment: schema.string(),
|
||||
commentId: schema.string(),
|
||||
})
|
||||
)
|
||||
),
|
||||
});
|
||||
|
||||
export const ExecutorSubActionGetIncidentParamsSchema = schema.object({
|
||||
|
|
|
@ -479,10 +479,6 @@ describe('Jira service', () => {
|
|||
comment: {
|
||||
comment: 'comment',
|
||||
commentId: 'comment-1',
|
||||
createdBy: null,
|
||||
createdAt: null,
|
||||
updatedAt: null,
|
||||
updatedBy: null,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -507,10 +503,6 @@ describe('Jira service', () => {
|
|||
comment: {
|
||||
comment: 'comment',
|
||||
commentId: 'comment-1',
|
||||
createdBy: null,
|
||||
createdAt: null,
|
||||
updatedAt: null,
|
||||
updatedBy: null,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -536,10 +528,6 @@ describe('Jira service', () => {
|
|||
comment: {
|
||||
comment: 'comment',
|
||||
commentId: 'comment-1',
|
||||
createdBy: null,
|
||||
createdAt: null,
|
||||
updatedAt: null,
|
||||
updatedBy: null,
|
||||
},
|
||||
})
|
||||
).rejects.toThrow(
|
||||
|
|
|
@ -133,21 +133,24 @@ export const createExternalService = (
|
|||
[key: string]: {
|
||||
allowedValues?: Array<{}>;
|
||||
defaultValue?: {};
|
||||
name: string;
|
||||
required: boolean;
|
||||
schema: FieldSchema;
|
||||
};
|
||||
}) =>
|
||||
Object.keys(fields ?? {}).reduce((fieldsAcc, fieldKey) => {
|
||||
return {
|
||||
Object.keys(fields ?? {}).reduce(
|
||||
(fieldsAcc, fieldKey) => ({
|
||||
...fieldsAcc,
|
||||
[fieldKey]: {
|
||||
required: fields[fieldKey]?.required,
|
||||
allowedValues: fields[fieldKey]?.allowedValues ?? [],
|
||||
defaultValue: fields[fieldKey]?.defaultValue ?? {},
|
||||
schema: fields[fieldKey]?.schema,
|
||||
name: fields[fieldKey]?.name,
|
||||
},
|
||||
};
|
||||
}, {});
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
const normalizeSearchResults = (
|
||||
issues: Array<{ id: string; key: string; fields: { summary: string } }>
|
||||
|
@ -386,7 +389,6 @@ export const createExternalService = (
|
|||
});
|
||||
|
||||
const fields = res.data.projects[0]?.issuetypes[0]?.fields || {};
|
||||
|
||||
return normalizeFields(fields);
|
||||
} else {
|
||||
const res = await request({
|
||||
|
|
|
@ -17,11 +17,3 @@ export const ALLOWED_HOSTS_ERROR = (message: string) =>
|
|||
message,
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: remove when Case mappings will be removed
|
||||
export const MAPPING_EMPTY = i18n.translate(
|
||||
'xpack.actions.builtin.jira.configuration.emptyMapping',
|
||||
{
|
||||
defaultMessage: '[incidentConfiguration.mapping]: expected non-empty but got empty',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -21,8 +21,6 @@ import {
|
|||
ExecutorSubActionGetIssueParamsSchema,
|
||||
} from './schema';
|
||||
import { ActionsConfigurationUtilities } from '../../actions_config';
|
||||
import { IncidentConfigurationSchema } from '../case/schema';
|
||||
import { Comment } from '../case/types';
|
||||
import { Logger } from '../../../../../../src/core/server';
|
||||
|
||||
export type JiraPublicConfigurationType = TypeOf<typeof ExternalIncidentServiceConfigurationSchema>;
|
||||
|
@ -33,8 +31,6 @@ export type JiraSecretConfigurationType = TypeOf<
|
|||
export type ExecutorParams = TypeOf<typeof ExecutorParamsSchema>;
|
||||
export type ExecutorSubActionPushParams = TypeOf<typeof ExecutorSubActionPushParamsSchema>;
|
||||
|
||||
export type IncidentConfiguration = TypeOf<typeof IncidentConfigurationSchema>;
|
||||
|
||||
export interface ExternalServiceCredentials {
|
||||
config: Record<string, unknown>;
|
||||
secrets: Record<string, unknown>;
|
||||
|
@ -52,18 +48,9 @@ export interface ExternalServiceIncidentResponse {
|
|||
pushedDate: string;
|
||||
}
|
||||
|
||||
export interface ExternalServiceCommentResponse {
|
||||
commentId: string;
|
||||
pushedDate: string;
|
||||
externalCommentId?: string;
|
||||
}
|
||||
|
||||
export type ExternalServiceParams = Record<string, unknown>;
|
||||
|
||||
export type Incident = Pick<
|
||||
ExecutorSubActionPushParams,
|
||||
'description' | 'priority' | 'labels' | 'issueType' | 'parent'
|
||||
> & { summary: string };
|
||||
export type Incident = Omit<ExecutorSubActionPushParams['incident'], 'externalId'>;
|
||||
|
||||
export interface CreateIncidentParams {
|
||||
incident: Incident;
|
||||
|
@ -76,7 +63,7 @@ export interface UpdateIncidentParams {
|
|||
|
||||
export interface CreateCommentParams {
|
||||
incidentId: string;
|
||||
comment: Comment;
|
||||
comment: SimpleComment;
|
||||
}
|
||||
|
||||
export interface FieldsSchema {
|
||||
|
@ -84,18 +71,6 @@ export interface FieldsSchema {
|
|||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface ExternalServiceFields {
|
||||
clauseNames: string[];
|
||||
custom: boolean;
|
||||
id: string;
|
||||
key: string;
|
||||
name: string;
|
||||
navigatable: boolean;
|
||||
orderable: boolean;
|
||||
schema: FieldsSchema;
|
||||
searchable: boolean;
|
||||
}
|
||||
|
||||
export type GetIssueTypesResponse = Array<{ id: string; name: string }>;
|
||||
|
||||
export interface FieldSchema {
|
||||
|
@ -104,7 +79,13 @@ export interface FieldSchema {
|
|||
}
|
||||
export type GetFieldsByIssueTypeResponse = Record<
|
||||
string,
|
||||
{ allowedValues: Array<{}>; defaultValue: {}; required: boolean; schema: FieldSchema }
|
||||
{
|
||||
allowedValues: Array<{}>;
|
||||
defaultValue: {};
|
||||
required: boolean;
|
||||
schema: FieldSchema;
|
||||
name: string;
|
||||
}
|
||||
>;
|
||||
export type GetCommonFieldsResponse = GetFieldsByIssueTypeResponse;
|
||||
|
||||
|
@ -128,9 +109,7 @@ export interface ExternalService {
|
|||
updateIncident: (params: UpdateIncidentParams) => Promise<ExternalServiceIncidentResponse>;
|
||||
}
|
||||
|
||||
export interface PushToServiceApiParams extends ExecutorSubActionPushParams {
|
||||
externalObject: Record<string, any>;
|
||||
}
|
||||
export type PushToServiceApiParams = ExecutorSubActionPushParams;
|
||||
|
||||
export type ExecutorSubActionGetIncidentParams = TypeOf<
|
||||
typeof ExecutorSubActionGetIncidentParamsSchema
|
||||
|
@ -160,7 +139,6 @@ export type ExecutorSubActionGetIssueParams = TypeOf<typeof ExecutorSubActionGet
|
|||
|
||||
export interface ExternalServiceApiHandlerArgs {
|
||||
externalService: ExternalService;
|
||||
mapping: Map<string, any> | null;
|
||||
}
|
||||
|
||||
export interface PushToServiceApiHandlerArgs extends ExternalServiceApiHandlerArgs {
|
||||
|
@ -207,7 +185,7 @@ export interface GetIssueHandlerArgs {
|
|||
|
||||
export interface ExternalServiceApi {
|
||||
getFields: (args: GetCommonFieldsHandlerArgs) => Promise<GetCommonFieldsResponse>;
|
||||
getIncident: (args: GetIncidentApiHandlerArgs) => Promise<void>;
|
||||
getIncident: (args: GetIncidentApiHandlerArgs) => Promise<ExternalServiceParams | undefined>;
|
||||
handshake: (args: HandshakeApiHandlerArgs) => Promise<void>;
|
||||
issueTypes: (args: GetIssueTypesHandlerArgs) => Promise<GetIssueTypesResponse>;
|
||||
pushToService: (args: PushToServiceApiHandlerArgs) => Promise<PushToServiceResponse>;
|
||||
|
@ -223,7 +201,8 @@ export type JiraExecutorResultData =
|
|||
| GetIssueTypesResponse
|
||||
| GetFieldsByIssueTypeResponse
|
||||
| GetIssuesResponse
|
||||
| GetIssueResponse;
|
||||
| GetIssueResponse
|
||||
| ExternalServiceParams;
|
||||
|
||||
export interface Fields {
|
||||
[key: string]: string | string[] | { name: string } | { key: string } | { id: string };
|
||||
|
@ -232,3 +211,12 @@ export interface ResponseError {
|
|||
errorMessages: string[] | null | undefined;
|
||||
errors: { [k: string]: string } | null | undefined;
|
||||
}
|
||||
export interface SimpleComment {
|
||||
comment: string;
|
||||
commentId: string;
|
||||
}
|
||||
export interface ExternalServiceCommentResponse {
|
||||
commentId: string;
|
||||
pushedDate: string;
|
||||
externalCommentId?: string;
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { isEmpty } from 'lodash';
|
||||
import { ActionsConfigurationUtilities } from '../../actions_config';
|
||||
import {
|
||||
JiraPublicConfigurationType,
|
||||
|
@ -18,13 +17,6 @@ export const validateCommonConfig = (
|
|||
configurationUtilities: ActionsConfigurationUtilities,
|
||||
configObject: JiraPublicConfigurationType
|
||||
) => {
|
||||
if (
|
||||
configObject.incidentConfiguration !== null &&
|
||||
isEmpty(configObject.incidentConfiguration.mapping)
|
||||
) {
|
||||
return i18n.MAPPING_EMPTY;
|
||||
}
|
||||
|
||||
try {
|
||||
configurationUtilities.ensureUriAllowed(configObject.apiUrl);
|
||||
} catch (allowedListError) {
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { Logger } from '../../../../../../src/core/server';
|
||||
import { api } from './api';
|
||||
import { externalServiceMock, mapping, apiParams } from './mocks';
|
||||
import { externalServiceMock, apiParams } from './mocks';
|
||||
import { ExternalService } from './types';
|
||||
|
||||
let mockedLogger: jest.Mocked<Logger>;
|
||||
|
@ -28,7 +28,6 @@ describe('api', () => {
|
|||
const params = { ...apiParams, externalId: null };
|
||||
const res = await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
|
@ -55,7 +54,6 @@ describe('api', () => {
|
|||
const params = { ...apiParams, externalId: null, comments: [] };
|
||||
const res = await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
|
@ -69,16 +67,15 @@ describe('api', () => {
|
|||
});
|
||||
|
||||
test('it calls createIncident correctly', async () => {
|
||||
const params = { ...apiParams, externalId: null };
|
||||
await api.pushToService({ externalService, mapping, params, logger: mockedLogger });
|
||||
const params = { ...apiParams, incident: { ...apiParams.incident, externalId: null } };
|
||||
await api.pushToService({ externalService, params, logger: mockedLogger });
|
||||
|
||||
expect(externalService.createIncident).toHaveBeenCalledWith({
|
||||
incident: {
|
||||
incidentTypes: [1001],
|
||||
severityCode: 6,
|
||||
description:
|
||||
'Incident description (created at 2020-06-03T15:09:13.606Z by Elastic User)',
|
||||
name: 'Incident title (created at 2020-06-03T15:09:13.606Z by Elastic User)',
|
||||
description: 'Incident description',
|
||||
name: 'Incident title',
|
||||
},
|
||||
});
|
||||
expect(externalService.updateIncident).not.toHaveBeenCalled();
|
||||
|
@ -86,23 +83,13 @@ describe('api', () => {
|
|||
|
||||
test('it calls createComment correctly', async () => {
|
||||
const params = { ...apiParams, externalId: null };
|
||||
await api.pushToService({ externalService, mapping, params, logger: mockedLogger });
|
||||
await api.pushToService({ externalService, params, logger: mockedLogger });
|
||||
expect(externalService.createComment).toHaveBeenCalledTimes(2);
|
||||
expect(externalService.createComment).toHaveBeenNthCalledWith(1, {
|
||||
incidentId: '1',
|
||||
comment: {
|
||||
commentId: 'case-comment-1',
|
||||
comment: 'A comment (added at 2020-06-03T15:09:13.606Z by Elastic User)',
|
||||
createdAt: '2020-06-03T15:09:13.606Z',
|
||||
createdBy: {
|
||||
fullName: 'Elastic User',
|
||||
username: 'elastic',
|
||||
},
|
||||
updatedAt: '2020-06-03T15:09:13.606Z',
|
||||
updatedBy: {
|
||||
fullName: 'Elastic User',
|
||||
username: 'elastic',
|
||||
},
|
||||
comment: 'A comment',
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -110,17 +97,7 @@ describe('api', () => {
|
|||
incidentId: '1',
|
||||
comment: {
|
||||
commentId: 'case-comment-2',
|
||||
comment: 'Another comment (added at 2020-06-03T15:09:13.606Z by Elastic User)',
|
||||
createdAt: '2020-06-03T15:09:13.606Z',
|
||||
createdBy: {
|
||||
fullName: 'Elastic User',
|
||||
username: 'elastic',
|
||||
},
|
||||
updatedAt: '2020-06-03T15:09:13.606Z',
|
||||
updatedBy: {
|
||||
fullName: 'Elastic User',
|
||||
username: 'elastic',
|
||||
},
|
||||
comment: 'Another comment',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -130,7 +107,6 @@ describe('api', () => {
|
|||
test('it updates an incident', async () => {
|
||||
const res = await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
|
@ -157,7 +133,6 @@ describe('api', () => {
|
|||
const params = { ...apiParams, comments: [] };
|
||||
const res = await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
|
@ -172,16 +147,15 @@ describe('api', () => {
|
|||
|
||||
test('it calls updateIncident correctly', async () => {
|
||||
const params = { ...apiParams };
|
||||
await api.pushToService({ externalService, mapping, params, logger: mockedLogger });
|
||||
await api.pushToService({ externalService, params, logger: mockedLogger });
|
||||
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
incidentTypes: [1001],
|
||||
severityCode: 6,
|
||||
description:
|
||||
'Incident description (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
|
||||
name: 'Incident title (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
|
||||
description: 'Incident description',
|
||||
name: 'Incident title',
|
||||
},
|
||||
});
|
||||
expect(externalService.createIncident).not.toHaveBeenCalled();
|
||||
|
@ -189,23 +163,13 @@ describe('api', () => {
|
|||
|
||||
test('it calls createComment correctly', async () => {
|
||||
const params = { ...apiParams };
|
||||
await api.pushToService({ externalService, mapping, params, logger: mockedLogger });
|
||||
await api.pushToService({ externalService, params, logger: mockedLogger });
|
||||
expect(externalService.createComment).toHaveBeenCalledTimes(2);
|
||||
expect(externalService.createComment).toHaveBeenNthCalledWith(1, {
|
||||
incidentId: '1',
|
||||
comment: {
|
||||
commentId: 'case-comment-1',
|
||||
comment: 'A comment (added at 2020-06-03T15:09:13.606Z by Elastic User)',
|
||||
createdAt: '2020-06-03T15:09:13.606Z',
|
||||
createdBy: {
|
||||
fullName: 'Elastic User',
|
||||
username: 'elastic',
|
||||
},
|
||||
updatedAt: '2020-06-03T15:09:13.606Z',
|
||||
updatedBy: {
|
||||
fullName: 'Elastic User',
|
||||
username: 'elastic',
|
||||
},
|
||||
comment: 'A comment',
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -213,17 +177,7 @@ describe('api', () => {
|
|||
incidentId: '1',
|
||||
comment: {
|
||||
commentId: 'case-comment-2',
|
||||
comment: 'Another comment (added at 2020-06-03T15:09:13.606Z by Elastic User)',
|
||||
createdAt: '2020-06-03T15:09:13.606Z',
|
||||
createdBy: {
|
||||
fullName: 'Elastic User',
|
||||
username: 'elastic',
|
||||
},
|
||||
updatedAt: '2020-06-03T15:09:13.606Z',
|
||||
updatedBy: {
|
||||
fullName: 'Elastic User',
|
||||
username: 'elastic',
|
||||
},
|
||||
comment: 'Another comment',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -236,14 +190,8 @@ describe('api', () => {
|
|||
params: {},
|
||||
});
|
||||
expect(res).toEqual([
|
||||
{
|
||||
id: 17,
|
||||
name: 'Communication error (fax; email)',
|
||||
},
|
||||
{
|
||||
id: 1001,
|
||||
name: 'Custom type',
|
||||
},
|
||||
{ id: 17, name: 'Communication error (fax; email)' },
|
||||
{ id: 1001, name: 'Custom type' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
@ -255,397 +203,11 @@ describe('api', () => {
|
|||
params: { id: '10006' },
|
||||
});
|
||||
expect(res).toEqual([
|
||||
{
|
||||
id: 4,
|
||||
name: 'Low',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Medium',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'High',
|
||||
},
|
||||
{ id: 4, name: 'Low' },
|
||||
{ id: 5, name: 'Medium' },
|
||||
{ id: 6, name: 'High' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapping variations', () => {
|
||||
test('overwrite & append', async () => {
|
||||
mapping.set('title', {
|
||||
target: 'name',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
mapping.set('description', {
|
||||
target: 'description',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('comments', {
|
||||
target: 'comments',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('name', {
|
||||
target: 'title',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
incidentTypes: [1001],
|
||||
severityCode: 6,
|
||||
name: 'Incident title (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
|
||||
description:
|
||||
'description from ibm resilient \r\nIncident description (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('nothing & append', async () => {
|
||||
mapping.set('title', {
|
||||
target: 'name',
|
||||
actionType: 'nothing',
|
||||
});
|
||||
|
||||
mapping.set('description', {
|
||||
target: 'description',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('comments', {
|
||||
target: 'comments',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('name', {
|
||||
target: 'title',
|
||||
actionType: 'nothing',
|
||||
});
|
||||
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
incidentTypes: [1001],
|
||||
severityCode: 6,
|
||||
description:
|
||||
'description from ibm resilient \r\nIncident description (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('append & append', async () => {
|
||||
mapping.set('title', {
|
||||
target: 'name',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('description', {
|
||||
target: 'description',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('comments', {
|
||||
target: 'comments',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('name', {
|
||||
target: 'title',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
incidentTypes: [1001],
|
||||
severityCode: 6,
|
||||
name:
|
||||
'title from ibm resilient \r\nIncident title (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
|
||||
description:
|
||||
'description from ibm resilient \r\nIncident description (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('nothing & nothing', async () => {
|
||||
mapping.set('title', {
|
||||
target: 'name',
|
||||
actionType: 'nothing',
|
||||
});
|
||||
|
||||
mapping.set('description', {
|
||||
target: 'description',
|
||||
actionType: 'nothing',
|
||||
});
|
||||
|
||||
mapping.set('comments', {
|
||||
target: 'comments',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('name', {
|
||||
target: 'title',
|
||||
actionType: 'nothing',
|
||||
});
|
||||
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
incidentTypes: [1001],
|
||||
severityCode: 6,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('overwrite & nothing', async () => {
|
||||
mapping.set('title', {
|
||||
target: 'name',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
mapping.set('description', {
|
||||
target: 'description',
|
||||
actionType: 'nothing',
|
||||
});
|
||||
|
||||
mapping.set('comments', {
|
||||
target: 'comments',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('name', {
|
||||
target: 'title',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
incidentTypes: [1001],
|
||||
severityCode: 6,
|
||||
name: 'Incident title (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('overwrite & overwrite', async () => {
|
||||
mapping.set('title', {
|
||||
target: 'name',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
mapping.set('description', {
|
||||
target: 'description',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
mapping.set('comments', {
|
||||
target: 'comments',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('name', {
|
||||
target: 'title',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
incidentTypes: [1001],
|
||||
severityCode: 6,
|
||||
name: 'Incident title (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
|
||||
description:
|
||||
'Incident description (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('nothing & overwrite', async () => {
|
||||
mapping.set('title', {
|
||||
target: 'name',
|
||||
actionType: 'nothing',
|
||||
});
|
||||
|
||||
mapping.set('description', {
|
||||
target: 'description',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
mapping.set('comments', {
|
||||
target: 'comments',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('name', {
|
||||
target: 'title',
|
||||
actionType: 'nothing',
|
||||
});
|
||||
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
incidentTypes: [1001],
|
||||
severityCode: 6,
|
||||
description:
|
||||
'Incident description (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('append & overwrite', async () => {
|
||||
mapping.set('title', {
|
||||
target: 'name',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('description', {
|
||||
target: 'description',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
mapping.set('comments', {
|
||||
target: 'comments',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('name', {
|
||||
target: 'title',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
incidentTypes: [1001],
|
||||
severityCode: 6,
|
||||
name:
|
||||
'title from ibm resilient \r\nIncident title (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
|
||||
description:
|
||||
'Incident description (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('append & nothing', async () => {
|
||||
mapping.set('title', {
|
||||
target: 'name',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('description', {
|
||||
target: 'description',
|
||||
actionType: 'nothing',
|
||||
});
|
||||
|
||||
mapping.set('comments', {
|
||||
target: 'comments',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('name', {
|
||||
target: 'title',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
incidentTypes: [1001],
|
||||
severityCode: 6,
|
||||
name:
|
||||
'title from ibm resilient \r\nIncident title (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('comment nothing', async () => {
|
||||
mapping.set('title', {
|
||||
target: 'name',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
mapping.set('description', {
|
||||
target: 'description',
|
||||
actionType: 'nothing',
|
||||
});
|
||||
|
||||
mapping.set('comments', {
|
||||
target: 'comments',
|
||||
actionType: 'nothing',
|
||||
});
|
||||
|
||||
mapping.set('name', {
|
||||
target: 'title',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(externalService.createComment).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
ExternalServiceParams,
|
||||
PushToServiceApiHandlerArgs,
|
||||
HandshakeApiHandlerArgs,
|
||||
GetIncidentApiHandlerArgs,
|
||||
|
@ -13,25 +12,13 @@ import {
|
|||
Incident,
|
||||
GetIncidentTypesHandlerArgs,
|
||||
GetSeverityHandlerArgs,
|
||||
PushToServiceApiParams,
|
||||
PushToServiceResponse,
|
||||
GetCommonFieldsHandlerArgs,
|
||||
} from './types';
|
||||
|
||||
// TODO: to remove, need to support Case
|
||||
import { transformFields, prepareFieldsForTransformation, transformComments } from '../case/utils';
|
||||
const handshakeHandler = async ({ externalService, params }: HandshakeApiHandlerArgs) => {};
|
||||
|
||||
const handshakeHandler = async ({
|
||||
externalService,
|
||||
mapping,
|
||||
params,
|
||||
}: HandshakeApiHandlerArgs) => {};
|
||||
|
||||
const getIncidentHandler = async ({
|
||||
externalService,
|
||||
mapping,
|
||||
params,
|
||||
}: GetIncidentApiHandlerArgs) => {};
|
||||
const getIncidentHandler = async ({ externalService, params }: GetIncidentApiHandlerArgs) => {};
|
||||
|
||||
const getFieldsHandler = async ({ externalService }: GetCommonFieldsHandlerArgs) => {
|
||||
const res = await externalService.getFields();
|
||||
|
@ -49,56 +36,12 @@ const getSeverityHandler = async ({ externalService }: GetSeverityHandlerArgs) =
|
|||
|
||||
const pushToServiceHandler = async ({
|
||||
externalService,
|
||||
mapping,
|
||||
params,
|
||||
logger,
|
||||
}: PushToServiceApiHandlerArgs): Promise<PushToServiceResponse> => {
|
||||
const { externalId, comments } = params;
|
||||
const updateIncident = externalId ? true : false;
|
||||
const defaultPipes = updateIncident ? ['informationUpdated'] : ['informationCreated'];
|
||||
let currentIncident: ExternalServiceParams | undefined;
|
||||
const { comments } = params;
|
||||
let res: PushToServiceResponse;
|
||||
|
||||
if (externalId) {
|
||||
try {
|
||||
currentIncident = await externalService.getIncident(externalId);
|
||||
} catch (ex) {
|
||||
logger.debug(
|
||||
`Retrieving Incident by id ${externalId} from IBM Resilient was failed with exception: ${ex}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let incident: Incident;
|
||||
// TODO: should be removed later but currently keep it for the Case implementation support
|
||||
if (mapping) {
|
||||
const fields = prepareFieldsForTransformation({
|
||||
externalCase: params.externalObject,
|
||||
mapping,
|
||||
defaultPipes,
|
||||
});
|
||||
|
||||
const transformedFields = transformFields<
|
||||
PushToServiceApiParams,
|
||||
ExternalServiceParams,
|
||||
Incident
|
||||
>({
|
||||
params,
|
||||
fields,
|
||||
currentIncident,
|
||||
});
|
||||
|
||||
const { incidentTypes, severityCode } = params;
|
||||
incident = {
|
||||
name: transformedFields.name,
|
||||
description: transformedFields.description,
|
||||
incidentTypes,
|
||||
severityCode,
|
||||
};
|
||||
} else {
|
||||
const { title, description, incidentTypes, severityCode } = params;
|
||||
incident = { name: title, description, incidentTypes, severityCode };
|
||||
}
|
||||
const { externalId, ...rest } = params.incident;
|
||||
const incident: Incident = rest;
|
||||
|
||||
if (externalId != null) {
|
||||
res = await externalService.updateIncident({
|
||||
|
@ -107,22 +50,13 @@ const pushToServiceHandler = async ({
|
|||
});
|
||||
} else {
|
||||
res = await externalService.createIncident({
|
||||
incident: {
|
||||
...incident,
|
||||
},
|
||||
incident,
|
||||
});
|
||||
}
|
||||
|
||||
if (comments && Array.isArray(comments) && comments.length > 0) {
|
||||
if (mapping && mapping.get('comments')?.actionType === 'nothing') {
|
||||
return res;
|
||||
}
|
||||
const commentsTransformed = mapping
|
||||
? transformComments(comments, ['informationAdded'])
|
||||
: comments;
|
||||
|
||||
res.comments = [];
|
||||
for (const currentComment of commentsTransformed) {
|
||||
for (const currentComment of comments) {
|
||||
const comment = await externalService.createComment({
|
||||
incidentId: res.id,
|
||||
comment: currentComment,
|
||||
|
|
|
@ -30,9 +30,6 @@ import {
|
|||
import * as i18n from './translations';
|
||||
import { Logger } from '../../../../../../src/core/server';
|
||||
|
||||
// TODO: to remove, need to support Case
|
||||
import { buildMap, mapParams } from '../case/utils';
|
||||
|
||||
interface GetActionTypeParams {
|
||||
logger: Logger;
|
||||
configurationUtilities: ActionsConfigurationUtilities;
|
||||
|
@ -104,19 +101,9 @@ async function executor(
|
|||
if (subAction === 'pushToService') {
|
||||
const pushToServiceParams = subActionParams as ExecutorSubActionPushParams;
|
||||
|
||||
const { comments, externalId, ...restParams } = pushToServiceParams;
|
||||
const mapping = config.incidentConfiguration
|
||||
? buildMap(config.incidentConfiguration.mapping)
|
||||
: null;
|
||||
const externalObject =
|
||||
config.incidentConfiguration && mapping
|
||||
? mapParams<ExecutorSubActionPushParams>(restParams as ExecutorSubActionPushParams, mapping)
|
||||
: {};
|
||||
|
||||
data = await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: { ...pushToServiceParams, externalObject },
|
||||
params: pushToServiceParams,
|
||||
logger,
|
||||
});
|
||||
|
||||
|
|
|
@ -6,8 +6,6 @@
|
|||
|
||||
import { ExternalService, PushToServiceApiParams, ExecutorSubActionPushParams } from './types';
|
||||
|
||||
import { MapRecord } from '../case/types';
|
||||
|
||||
export const resilientFields = [
|
||||
{
|
||||
id: 17,
|
||||
|
@ -348,62 +346,28 @@ const externalServiceMock = {
|
|||
create: createMock,
|
||||
};
|
||||
|
||||
const mapping: Map<string, Partial<MapRecord>> = new Map();
|
||||
|
||||
mapping.set('title', {
|
||||
target: 'name',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
mapping.set('description', {
|
||||
target: 'description',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
mapping.set('comments', {
|
||||
target: 'comments',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('name', {
|
||||
target: 'title',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
const executorParams: ExecutorSubActionPushParams = {
|
||||
savedObjectId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa',
|
||||
externalId: 'incident-3',
|
||||
createdAt: '2020-06-03T15:09:13.606Z',
|
||||
createdBy: { fullName: 'Elastic User', username: 'elastic' },
|
||||
updatedAt: '2020-06-03T15:09:13.606Z',
|
||||
updatedBy: { fullName: 'Elastic User', username: 'elastic' },
|
||||
title: 'Incident title',
|
||||
description: 'Incident description',
|
||||
incidentTypes: [1001],
|
||||
severityCode: 6,
|
||||
incident: {
|
||||
externalId: 'incident-3',
|
||||
name: 'Incident title',
|
||||
description: 'Incident description',
|
||||
incidentTypes: [1001],
|
||||
severityCode: 6,
|
||||
},
|
||||
comments: [
|
||||
{
|
||||
commentId: 'case-comment-1',
|
||||
comment: 'A comment',
|
||||
createdAt: '2020-06-03T15:09:13.606Z',
|
||||
createdBy: { fullName: 'Elastic User', username: 'elastic' },
|
||||
updatedAt: '2020-06-03T15:09:13.606Z',
|
||||
updatedBy: { fullName: 'Elastic User', username: 'elastic' },
|
||||
},
|
||||
{
|
||||
commentId: 'case-comment-2',
|
||||
comment: 'Another comment',
|
||||
createdAt: '2020-06-03T15:09:13.606Z',
|
||||
createdBy: { fullName: 'Elastic User', username: 'elastic' },
|
||||
updatedAt: '2020-06-03T15:09:13.606Z',
|
||||
updatedBy: { fullName: 'Elastic User', username: 'elastic' },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const apiParams: PushToServiceApiParams = {
|
||||
...executorParams,
|
||||
externalObject: { name: 'Incident title', description: 'Incident description' },
|
||||
};
|
||||
|
||||
const incidentTypes = [
|
||||
|
@ -457,4 +421,4 @@ const severity = [
|
|||
},
|
||||
];
|
||||
|
||||
export { externalServiceMock, mapping, executorParams, apiParams, incidentTypes, severity };
|
||||
export { externalServiceMock, executorParams, apiParams, incidentTypes, severity };
|
||||
|
|
|
@ -5,14 +5,10 @@
|
|||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { CommentSchema, EntityInformation, IncidentConfigurationSchema } from '../case/schema';
|
||||
|
||||
export const ExternalIncidentServiceConfiguration = {
|
||||
apiUrl: schema.string(),
|
||||
orgId: schema.string(),
|
||||
// TODO: to remove - set it optional for the current stage to support Case implementation
|
||||
incidentConfiguration: schema.nullable(IncidentConfigurationSchema),
|
||||
isCaseOwned: schema.nullable(schema.boolean()),
|
||||
};
|
||||
|
||||
export const ExternalIncidentServiceConfigurationSchema = schema.object(
|
||||
|
@ -37,15 +33,21 @@ export const ExecutorSubActionSchema = schema.oneOf([
|
|||
]);
|
||||
|
||||
export const ExecutorSubActionPushParamsSchema = schema.object({
|
||||
savedObjectId: schema.nullable(schema.string()),
|
||||
title: schema.string(),
|
||||
description: schema.nullable(schema.string()),
|
||||
externalId: schema.nullable(schema.string()),
|
||||
incidentTypes: schema.nullable(schema.arrayOf(schema.number())),
|
||||
severityCode: schema.nullable(schema.number()),
|
||||
// TODO: remove later - need for support Case push multiple comments
|
||||
comments: schema.nullable(schema.arrayOf(CommentSchema)),
|
||||
...EntityInformation,
|
||||
incident: schema.object({
|
||||
name: schema.string(),
|
||||
description: schema.nullable(schema.string()),
|
||||
externalId: schema.nullable(schema.string()),
|
||||
incidentTypes: schema.nullable(schema.arrayOf(schema.number())),
|
||||
severityCode: schema.nullable(schema.number()),
|
||||
}),
|
||||
comments: schema.nullable(
|
||||
schema.arrayOf(
|
||||
schema.object({
|
||||
comment: schema.string(),
|
||||
commentId: schema.string(),
|
||||
})
|
||||
)
|
||||
),
|
||||
});
|
||||
|
||||
export const ExecutorSubActionGetIncidentParamsSchema = schema.object({
|
||||
|
|
|
@ -450,10 +450,6 @@ describe('IBM Resilient service', () => {
|
|||
comment: {
|
||||
comment: 'comment',
|
||||
commentId: 'comment-1',
|
||||
createdBy: null,
|
||||
createdAt: null,
|
||||
updatedAt: null,
|
||||
updatedBy: null,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -477,10 +473,6 @@ describe('IBM Resilient service', () => {
|
|||
comment: {
|
||||
comment: 'comment',
|
||||
commentId: 'comment-1',
|
||||
createdBy: null,
|
||||
createdAt: null,
|
||||
updatedAt: null,
|
||||
updatedBy: null,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -510,10 +502,6 @@ describe('IBM Resilient service', () => {
|
|||
comment: {
|
||||
comment: 'comment',
|
||||
commentId: 'comment-1',
|
||||
createdBy: null,
|
||||
createdAt: null,
|
||||
updatedAt: null,
|
||||
updatedBy: null,
|
||||
},
|
||||
})
|
||||
).rejects.toThrow(
|
||||
|
|
|
@ -17,11 +17,3 @@ export const ALLOWED_HOSTS_ERROR = (message: string) =>
|
|||
message,
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: remove when Case mappings will be removed
|
||||
export const MAPPING_EMPTY = i18n.translate(
|
||||
'xpack.actions.builtin.servicenow.configuration.emptyMapping',
|
||||
{
|
||||
defaultMessage: '[incidentConfiguration.mapping]: expected non-empty but got empty',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -22,9 +22,6 @@ import {
|
|||
import { ActionsConfigurationUtilities } from '../../actions_config';
|
||||
import { Logger } from '../../../../../../src/core/server';
|
||||
|
||||
import { IncidentConfigurationSchema } from '../case/schema';
|
||||
import { Comment } from '../case/types';
|
||||
|
||||
export type ResilientPublicConfigurationType = TypeOf<
|
||||
typeof ExternalIncidentServiceConfigurationSchema
|
||||
>;
|
||||
|
@ -39,8 +36,6 @@ export type ExecutorSubActionCommonFieldsParams = TypeOf<
|
|||
export type ExecutorParams = TypeOf<typeof ExecutorParamsSchema>;
|
||||
export type ExecutorSubActionPushParams = TypeOf<typeof ExecutorSubActionPushParamsSchema>;
|
||||
|
||||
export type IncidentConfiguration = TypeOf<typeof IncidentConfigurationSchema>;
|
||||
|
||||
export interface ExternalServiceCredentials {
|
||||
config: Record<string, unknown>;
|
||||
secrets: Record<string, unknown>;
|
||||
|
@ -58,28 +53,17 @@ export interface ExternalServiceIncidentResponse {
|
|||
pushedDate: string;
|
||||
}
|
||||
|
||||
export interface ExternalServiceCommentResponse {
|
||||
commentId: string;
|
||||
pushedDate: string;
|
||||
externalCommentId?: string;
|
||||
}
|
||||
|
||||
export type ExternalServiceParams = Record<string, unknown>;
|
||||
export interface ExternalServiceFields {
|
||||
id: string;
|
||||
input_type: string;
|
||||
name: string;
|
||||
read_only: boolean;
|
||||
required?: string;
|
||||
text: string;
|
||||
}
|
||||
export type GetCommonFieldsResponse = ExternalServiceFields[];
|
||||
|
||||
export type Incident = Pick<
|
||||
ExecutorSubActionPushParams,
|
||||
'description' | 'incidentTypes' | 'severityCode'
|
||||
> & {
|
||||
name: string;
|
||||
};
|
||||
export type Incident = Omit<ExecutorSubActionPushParams['incident'], 'externalId'>;
|
||||
|
||||
export interface CreateIncidentParams {
|
||||
incident: Incident;
|
||||
|
@ -92,7 +76,7 @@ export interface UpdateIncidentParams {
|
|||
|
||||
export interface CreateCommentParams {
|
||||
incidentId: string;
|
||||
comment: Comment;
|
||||
comment: SimpleComment;
|
||||
}
|
||||
|
||||
export type GetIncidentTypesResponse = Array<{ id: string; name: string }>;
|
||||
|
@ -108,10 +92,7 @@ export interface ExternalService {
|
|||
updateIncident: (params: UpdateIncidentParams) => Promise<ExternalServiceIncidentResponse>;
|
||||
}
|
||||
|
||||
export interface PushToServiceApiParams extends ExecutorSubActionPushParams {
|
||||
externalObject: Record<string, any>;
|
||||
}
|
||||
|
||||
export type PushToServiceApiParams = ExecutorSubActionPushParams;
|
||||
export type ExecutorSubActionGetIncidentTypesParams = TypeOf<
|
||||
typeof ExecutorSubActionGetIncidentTypesParamsSchema
|
||||
>;
|
||||
|
@ -122,7 +103,6 @@ export type ExecutorSubActionGetSeverityParams = TypeOf<
|
|||
|
||||
export interface ExternalServiceApiHandlerArgs {
|
||||
externalService: ExternalService;
|
||||
mapping: Map<string, any> | null;
|
||||
}
|
||||
|
||||
export type ExecutorSubActionGetIncidentParams = TypeOf<
|
||||
|
@ -222,3 +202,12 @@ export interface CreateIncidentData {
|
|||
incident_type_ids?: Array<{ id: number }>;
|
||||
severity_code?: { id: number };
|
||||
}
|
||||
export interface SimpleComment {
|
||||
comment: string;
|
||||
commentId: string;
|
||||
}
|
||||
export interface ExternalServiceCommentResponse {
|
||||
commentId: string;
|
||||
pushedDate: string;
|
||||
externalCommentId?: string;
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { isEmpty } from 'lodash';
|
||||
import { ActionsConfigurationUtilities } from '../../actions_config';
|
||||
import {
|
||||
ResilientPublicConfigurationType,
|
||||
|
@ -18,13 +17,6 @@ export const validateCommonConfig = (
|
|||
configurationUtilities: ActionsConfigurationUtilities,
|
||||
configObject: ResilientPublicConfigurationType
|
||||
) => {
|
||||
if (
|
||||
configObject.incidentConfiguration !== null &&
|
||||
isEmpty(configObject.incidentConfiguration.mapping)
|
||||
) {
|
||||
return i18n.MAPPING_EMPTY;
|
||||
}
|
||||
|
||||
try {
|
||||
configurationUtilities.ensureUriAllowed(configObject.apiUrl);
|
||||
} catch (allowedListError) {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { Logger } from '../../../../../../src/core/server';
|
||||
import { externalServiceMock, mapping, apiParams, serviceNowCommonFields } from './mocks';
|
||||
import { externalServiceMock, apiParams, serviceNowCommonFields } from './mocks';
|
||||
import { ExternalService } from './types';
|
||||
import { api } from './api';
|
||||
let mockedLogger: jest.Mocked<Logger>;
|
||||
|
@ -19,10 +19,9 @@ describe('api', () => {
|
|||
|
||||
describe('create incident', () => {
|
||||
test('it creates an incident', async () => {
|
||||
const params = { ...apiParams, externalId: null };
|
||||
const params = { ...apiParams, incident: { ...apiParams.incident, externalId: null } };
|
||||
const res = await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params,
|
||||
secrets: {},
|
||||
logger: mockedLogger,
|
||||
|
@ -47,10 +46,13 @@ describe('api', () => {
|
|||
});
|
||||
|
||||
test('it creates an incident without comments', async () => {
|
||||
const params = { ...apiParams, externalId: null, comments: [] };
|
||||
const params = {
|
||||
...apiParams,
|
||||
incident: { ...apiParams.incident, externalId: null },
|
||||
comments: [],
|
||||
};
|
||||
const res = await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params,
|
||||
secrets: {},
|
||||
logger: mockedLogger,
|
||||
|
@ -65,10 +67,12 @@ describe('api', () => {
|
|||
});
|
||||
|
||||
test('it calls createIncident correctly', async () => {
|
||||
const params = { ...apiParams, externalId: null, comments: [] };
|
||||
const params = {
|
||||
incident: { ...apiParams.incident, externalId: null },
|
||||
comments: [],
|
||||
};
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params,
|
||||
secrets: { username: 'elastic', password: 'elastic' },
|
||||
logger: mockedLogger,
|
||||
|
@ -80,18 +84,17 @@ describe('api', () => {
|
|||
urgency: '2',
|
||||
impact: '3',
|
||||
caller_id: 'elastic',
|
||||
description: 'Incident description (created at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
short_description: 'Incident title (created at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
description: 'Incident description',
|
||||
short_description: 'Incident title',
|
||||
},
|
||||
});
|
||||
expect(externalService.updateIncident).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('it calls updateIncident correctly when creating an incident and having comments', async () => {
|
||||
const params = { ...apiParams, externalId: null };
|
||||
const params = { ...apiParams, incident: { ...apiParams.incident, externalId: null } };
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params,
|
||||
secrets: {},
|
||||
logger: mockedLogger,
|
||||
|
@ -102,9 +105,9 @@ describe('api', () => {
|
|||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
comments: 'A comment (added at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
description: 'Incident description (created at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
short_description: 'Incident title (created at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
comments: 'A comment',
|
||||
description: 'Incident description',
|
||||
short_description: 'Incident title',
|
||||
},
|
||||
incidentId: 'incident-1',
|
||||
});
|
||||
|
@ -114,9 +117,9 @@ describe('api', () => {
|
|||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
comments: 'Another comment (added at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
description: 'Incident description (created at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
short_description: 'Incident title (created at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
comments: 'Another comment',
|
||||
description: 'Incident description',
|
||||
short_description: 'Incident title',
|
||||
},
|
||||
incidentId: 'incident-1',
|
||||
});
|
||||
|
@ -127,7 +130,6 @@ describe('api', () => {
|
|||
test('it updates an incident', async () => {
|
||||
const res = await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
secrets: {},
|
||||
logger: mockedLogger,
|
||||
|
@ -155,7 +157,6 @@ describe('api', () => {
|
|||
const params = { ...apiParams, comments: [] };
|
||||
const res = await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params,
|
||||
secrets: {},
|
||||
logger: mockedLogger,
|
||||
|
@ -173,7 +174,6 @@ describe('api', () => {
|
|||
const params = { ...apiParams };
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params,
|
||||
secrets: {},
|
||||
logger: mockedLogger,
|
||||
|
@ -185,8 +185,8 @@ describe('api', () => {
|
|||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
description: 'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
short_description: 'Incident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
description: 'Incident description',
|
||||
short_description: 'Incident title',
|
||||
},
|
||||
});
|
||||
expect(externalService.createIncident).not.toHaveBeenCalled();
|
||||
|
@ -196,7 +196,6 @@ describe('api', () => {
|
|||
const params = { ...apiParams };
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params,
|
||||
secrets: {},
|
||||
logger: mockedLogger,
|
||||
|
@ -207,8 +206,8 @@ describe('api', () => {
|
|||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
description: 'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
short_description: 'Incident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
description: 'Incident description',
|
||||
short_description: 'Incident title',
|
||||
},
|
||||
incidentId: 'incident-3',
|
||||
});
|
||||
|
@ -218,409 +217,15 @@ describe('api', () => {
|
|||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
comments: 'A comment (added at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
description: 'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
short_description: 'Incident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
comments: 'A comment',
|
||||
description: 'Incident description',
|
||||
short_description: 'Incident title',
|
||||
},
|
||||
incidentId: 'incident-2',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapping variations', () => {
|
||||
test('overwrite & append', async () => {
|
||||
mapping.set('title', {
|
||||
target: 'short_description',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
mapping.set('description', {
|
||||
target: 'description',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('comments', {
|
||||
target: 'comments',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('short_description', {
|
||||
target: 'title',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
secrets: {},
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
short_description: 'Incident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
description:
|
||||
'description from servicenow \r\nIncident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('nothing & append', async () => {
|
||||
mapping.set('title', {
|
||||
target: 'short_description',
|
||||
actionType: 'nothing',
|
||||
});
|
||||
|
||||
mapping.set('description', {
|
||||
target: 'description',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('comments', {
|
||||
target: 'comments',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('short_description', {
|
||||
target: 'title',
|
||||
actionType: 'nothing',
|
||||
});
|
||||
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
secrets: {},
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
description:
|
||||
'description from servicenow \r\nIncident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('append & append', async () => {
|
||||
mapping.set('title', {
|
||||
target: 'short_description',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('description', {
|
||||
target: 'description',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('comments', {
|
||||
target: 'comments',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('short_description', {
|
||||
target: 'title',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
secrets: {},
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
short_description:
|
||||
'title from servicenow \r\nIncident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
description:
|
||||
'description from servicenow \r\nIncident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('nothing & nothing', async () => {
|
||||
mapping.set('title', {
|
||||
target: 'short_description',
|
||||
actionType: 'nothing',
|
||||
});
|
||||
|
||||
mapping.set('description', {
|
||||
target: 'description',
|
||||
actionType: 'nothing',
|
||||
});
|
||||
|
||||
mapping.set('comments', {
|
||||
target: 'comments',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('short_description', {
|
||||
target: 'title',
|
||||
actionType: 'nothing',
|
||||
});
|
||||
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
secrets: {},
|
||||
logger: mockedLogger,
|
||||
});
|
||||
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('overwrite & nothing', async () => {
|
||||
mapping.set('title', {
|
||||
target: 'short_description',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
mapping.set('description', {
|
||||
target: 'description',
|
||||
actionType: 'nothing',
|
||||
});
|
||||
|
||||
mapping.set('comments', {
|
||||
target: 'comments',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('short_description', {
|
||||
target: 'title',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
secrets: {},
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
short_description: 'Incident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('overwrite & overwrite', async () => {
|
||||
mapping.set('title', {
|
||||
target: 'short_description',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
mapping.set('description', {
|
||||
target: 'description',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
mapping.set('comments', {
|
||||
target: 'comments',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('short_description', {
|
||||
target: 'title',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
secrets: {},
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
short_description: 'Incident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
description: 'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('nothing & overwrite', async () => {
|
||||
mapping.set('title', {
|
||||
target: 'short_description',
|
||||
actionType: 'nothing',
|
||||
});
|
||||
|
||||
mapping.set('description', {
|
||||
target: 'description',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
mapping.set('comments', {
|
||||
target: 'comments',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('short_description', {
|
||||
target: 'title',
|
||||
actionType: 'nothing',
|
||||
});
|
||||
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
secrets: {},
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
description: 'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('append & overwrite', async () => {
|
||||
mapping.set('title', {
|
||||
target: 'short_description',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('description', {
|
||||
target: 'description',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
mapping.set('comments', {
|
||||
target: 'comments',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('short_description', {
|
||||
target: 'title',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
secrets: {},
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
short_description:
|
||||
'title from servicenow \r\nIncident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
description: 'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('append & nothing', async () => {
|
||||
mapping.set('title', {
|
||||
target: 'short_description',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('description', {
|
||||
target: 'description',
|
||||
actionType: 'nothing',
|
||||
});
|
||||
|
||||
mapping.set('comments', {
|
||||
target: 'comments',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('short_description', {
|
||||
target: 'title',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
secrets: {},
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
short_description:
|
||||
'title from servicenow \r\nIncident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('comment nothing', async () => {
|
||||
mapping.set('title', {
|
||||
target: 'short_description',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
mapping.set('description', {
|
||||
target: 'description',
|
||||
actionType: 'nothing',
|
||||
});
|
||||
|
||||
mapping.set('comments', {
|
||||
target: 'comments',
|
||||
actionType: 'nothing',
|
||||
});
|
||||
|
||||
mapping.set('short_description', {
|
||||
target: 'title',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: apiParams,
|
||||
secrets: {},
|
||||
logger: mockedLogger,
|
||||
});
|
||||
expect(externalService.updateIncident).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFields', () => {
|
||||
test('it returns the fields correctly', async () => {
|
||||
const res = await api.getFields({
|
||||
|
|
|
@ -4,86 +4,30 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import {
|
||||
ExternalServiceParams,
|
||||
PushToServiceApiHandlerArgs,
|
||||
HandshakeApiHandlerArgs,
|
||||
GetIncidentApiHandlerArgs,
|
||||
ExternalServiceApi,
|
||||
PushToServiceApiParams,
|
||||
PushToServiceResponse,
|
||||
Incident,
|
||||
GetCommonFieldsHandlerArgs,
|
||||
GetCommonFieldsResponse,
|
||||
GetIncidentApiHandlerArgs,
|
||||
HandshakeApiHandlerArgs,
|
||||
Incident,
|
||||
PushToServiceApiHandlerArgs,
|
||||
PushToServiceResponse,
|
||||
} from './types';
|
||||
|
||||
// TODO: to remove, need to support Case
|
||||
import { transformFields, transformComments, prepareFieldsForTransformation } from '../case/utils';
|
||||
|
||||
const handshakeHandler = async ({
|
||||
externalService,
|
||||
mapping,
|
||||
params,
|
||||
}: HandshakeApiHandlerArgs) => {};
|
||||
const getIncidentHandler = async ({
|
||||
externalService,
|
||||
mapping,
|
||||
params,
|
||||
}: GetIncidentApiHandlerArgs) => {};
|
||||
const handshakeHandler = async ({ externalService, params }: HandshakeApiHandlerArgs) => {};
|
||||
const getIncidentHandler = async ({ externalService, params }: GetIncidentApiHandlerArgs) => {};
|
||||
|
||||
const pushToServiceHandler = async ({
|
||||
externalService,
|
||||
mapping,
|
||||
params,
|
||||
secrets,
|
||||
logger,
|
||||
}: PushToServiceApiHandlerArgs): Promise<PushToServiceResponse> => {
|
||||
const { externalId, comments } = params;
|
||||
const updateIncident = externalId ? true : false;
|
||||
const defaultPipes = updateIncident ? ['informationUpdated'] : ['informationCreated'];
|
||||
let currentIncident: ExternalServiceParams | undefined;
|
||||
const { comments } = params;
|
||||
let res: PushToServiceResponse;
|
||||
const { externalId, ...rest } = params.incident;
|
||||
const incident: Incident = rest;
|
||||
|
||||
if (externalId) {
|
||||
try {
|
||||
currentIncident = await externalService.getIncident(externalId);
|
||||
} catch (ex) {
|
||||
logger.debug(
|
||||
`Retrieving Incident by id ${externalId} from ServiceNow was failed with exception: ${ex}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let incident = {};
|
||||
// TODO: should be removed later but currently keep it for the Case implementation support
|
||||
if (mapping && Array.isArray(params.comments)) {
|
||||
const fields = prepareFieldsForTransformation({
|
||||
externalCase: params.externalObject,
|
||||
mapping,
|
||||
defaultPipes,
|
||||
});
|
||||
|
||||
const transformedFields = transformFields<
|
||||
PushToServiceApiParams,
|
||||
ExternalServiceParams,
|
||||
Incident
|
||||
>({
|
||||
params,
|
||||
fields,
|
||||
currentIncident,
|
||||
});
|
||||
|
||||
incident = {
|
||||
severity: params.severity,
|
||||
urgency: params.urgency,
|
||||
impact: params.impact,
|
||||
short_description: transformedFields.short_description,
|
||||
description: transformedFields.description,
|
||||
};
|
||||
} else {
|
||||
incident = { ...params, short_description: params.title, comments: params.comment };
|
||||
}
|
||||
|
||||
if (updateIncident) {
|
||||
if (externalId != null) {
|
||||
res = await externalService.updateIncident({
|
||||
incidentId: externalId,
|
||||
incident,
|
||||
|
@ -97,24 +41,15 @@ const pushToServiceHandler = async ({
|
|||
});
|
||||
}
|
||||
|
||||
// TODO: should temporary keep comments for a Case usage
|
||||
if (
|
||||
comments &&
|
||||
Array.isArray(comments) &&
|
||||
comments.length > 0 &&
|
||||
mapping &&
|
||||
mapping.get('comments')?.actionType !== 'nothing'
|
||||
) {
|
||||
if (comments && Array.isArray(comments) && comments.length > 0) {
|
||||
res.comments = [];
|
||||
const commentsTransformed = transformComments(comments, ['informationAdded']);
|
||||
|
||||
const fieldsKey = mapping.get('comments')?.target ?? 'comments';
|
||||
for (const currentComment of commentsTransformed) {
|
||||
for (const currentComment of comments) {
|
||||
await externalService.updateIncident({
|
||||
incidentId: res.id,
|
||||
incident: {
|
||||
...incident,
|
||||
[fieldsKey]: currentComment.comment,
|
||||
comments: currentComment.comment,
|
||||
},
|
||||
});
|
||||
res.comments = [
|
||||
|
|
|
@ -29,9 +29,6 @@ import {
|
|||
ServiceNowExecutorResultData,
|
||||
} from './types';
|
||||
|
||||
// TODO: to remove, need to support Case
|
||||
import { buildMap, mapParams } from '../case/utils';
|
||||
|
||||
interface GetActionTypeParams {
|
||||
logger: Logger;
|
||||
configurationUtilities: ActionsConfigurationUtilities;
|
||||
|
@ -101,17 +98,9 @@ async function executor(
|
|||
|
||||
if (subAction === 'pushToService') {
|
||||
const pushToServiceParams = subActionParams as ExecutorSubActionPushParams;
|
||||
|
||||
const { comments, externalId, ...restParams } = pushToServiceParams;
|
||||
const incidentConfiguration = config.incidentConfiguration;
|
||||
const mapping = incidentConfiguration ? buildMap(incidentConfiguration.mapping) : null;
|
||||
const externalObject =
|
||||
config.incidentConfiguration && mapping ? mapParams(restParams, mapping) : {};
|
||||
|
||||
data = await api.pushToService({
|
||||
externalService,
|
||||
mapping,
|
||||
params: { ...pushToServiceParams, externalObject },
|
||||
params: pushToServiceParams,
|
||||
secrets,
|
||||
logger,
|
||||
});
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
*/
|
||||
|
||||
import { ExternalService, PushToServiceApiParams, ExecutorSubActionPushParams } from './types';
|
||||
import { MapRecord } from '../case/types';
|
||||
|
||||
export const serviceNowCommonFields = [
|
||||
{
|
||||
|
@ -69,64 +68,29 @@ const externalServiceMock = {
|
|||
create: createMock,
|
||||
};
|
||||
|
||||
const mapping: Map<string, Partial<MapRecord>> = new Map();
|
||||
|
||||
mapping.set('title', {
|
||||
target: 'short_description',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
mapping.set('description', {
|
||||
target: 'description',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
mapping.set('comments', {
|
||||
target: 'comments',
|
||||
actionType: 'append',
|
||||
});
|
||||
|
||||
mapping.set('short_description', {
|
||||
target: 'title',
|
||||
actionType: 'overwrite',
|
||||
});
|
||||
|
||||
const executorParams: ExecutorSubActionPushParams = {
|
||||
savedObjectId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa',
|
||||
externalId: 'incident-3',
|
||||
createdAt: '2020-03-13T08:34:53.450Z',
|
||||
createdBy: { fullName: 'Elastic User', username: 'elastic' },
|
||||
updatedAt: '2020-03-13T08:34:53.450Z',
|
||||
updatedBy: { fullName: 'Elastic User', username: 'elastic' },
|
||||
title: 'Incident title',
|
||||
description: 'Incident description',
|
||||
comment: 'test-alert comment',
|
||||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
incident: {
|
||||
externalId: 'incident-3',
|
||||
short_description: 'Incident title',
|
||||
description: 'Incident description',
|
||||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
},
|
||||
comments: [
|
||||
{
|
||||
commentId: 'case-comment-1',
|
||||
comment: 'A comment',
|
||||
createdAt: '2020-03-13T08:34:53.450Z',
|
||||
createdBy: { fullName: 'Elastic User', username: 'elastic' },
|
||||
updatedAt: '2020-03-13T08:34:53.450Z',
|
||||
updatedBy: { fullName: 'Elastic User', username: 'elastic' },
|
||||
},
|
||||
{
|
||||
commentId: 'case-comment-2',
|
||||
comment: 'Another comment',
|
||||
createdAt: '2020-03-13T08:34:53.450Z',
|
||||
createdBy: { fullName: 'Elastic User', username: 'elastic' },
|
||||
updatedAt: '2020-03-13T08:34:53.450Z',
|
||||
updatedBy: { fullName: 'Elastic User', username: 'elastic' },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const apiParams: PushToServiceApiParams = {
|
||||
...executorParams,
|
||||
externalObject: { short_description: 'Incident title', description: 'Incident description' },
|
||||
};
|
||||
|
||||
export { externalServiceMock, mapping, executorParams, apiParams };
|
||||
export { externalServiceMock, executorParams, apiParams };
|
||||
|
|
|
@ -5,13 +5,9 @@
|
|||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { CommentSchema, EntityInformation, IncidentConfigurationSchema } from '../case/schema';
|
||||
|
||||
export const ExternalIncidentServiceConfiguration = {
|
||||
apiUrl: schema.string(),
|
||||
// TODO: to remove - set it optional for the current stage to support Case ServiceNow implementation
|
||||
incidentConfiguration: schema.nullable(IncidentConfigurationSchema),
|
||||
isCaseOwned: schema.maybe(schema.boolean()),
|
||||
};
|
||||
|
||||
export const ExternalIncidentServiceConfigurationSchema = schema.object(
|
||||
|
@ -35,17 +31,22 @@ export const ExecutorSubActionSchema = schema.oneOf([
|
|||
]);
|
||||
|
||||
export const ExecutorSubActionPushParamsSchema = schema.object({
|
||||
savedObjectId: schema.nullable(schema.string()),
|
||||
title: schema.string(),
|
||||
description: schema.nullable(schema.string()),
|
||||
comment: schema.nullable(schema.string()),
|
||||
externalId: schema.nullable(schema.string()),
|
||||
severity: schema.nullable(schema.string()),
|
||||
urgency: schema.nullable(schema.string()),
|
||||
impact: schema.nullable(schema.string()),
|
||||
// TODO: remove later - need for support Case push multiple comments
|
||||
comments: schema.maybe(schema.arrayOf(CommentSchema)),
|
||||
...EntityInformation,
|
||||
incident: schema.object({
|
||||
short_description: schema.string(),
|
||||
description: schema.nullable(schema.string()),
|
||||
externalId: schema.nullable(schema.string()),
|
||||
severity: schema.nullable(schema.string()),
|
||||
urgency: schema.nullable(schema.string()),
|
||||
impact: schema.nullable(schema.string()),
|
||||
}),
|
||||
comments: schema.nullable(
|
||||
schema.arrayOf(
|
||||
schema.object({
|
||||
comment: schema.string(),
|
||||
commentId: schema.string(),
|
||||
})
|
||||
)
|
||||
),
|
||||
});
|
||||
|
||||
export const ExecutorSubActionGetIncidentParamsSchema = schema.object({
|
||||
|
|
|
@ -249,7 +249,7 @@ describe('ServiceNow service', () => {
|
|||
axios,
|
||||
logger,
|
||||
url:
|
||||
'https://dev102283.service-now.com/api/now/v2/table/sys_dictionary?sysparm_query=name=task^internal_type=string&active=true&read_only=false&sysparm_fields=max_length,element,column_label',
|
||||
'https://dev102283.service-now.com/api/now/v2/table/sys_dictionary?sysparm_query=name=task^internal_type=string&active=true&array=false&read_only=false&sysparm_fields=max_length,element,column_label,mandatory',
|
||||
});
|
||||
});
|
||||
test('it returns common fields correctly', async () => {
|
||||
|
@ -265,7 +265,7 @@ describe('ServiceNow service', () => {
|
|||
throw new Error('An error has occurred');
|
||||
});
|
||||
await expect(service.getFields()).rejects.toThrow(
|
||||
'Unable to get common fields. Error: An error has occurred'
|
||||
'[Action][ServiceNow]: Unable to get fields. Error: An error has occurred'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import axios from 'axios';
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
|
||||
import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from './types';
|
||||
|
||||
|
@ -35,7 +35,7 @@ export const createExternalService = (
|
|||
|
||||
const urlWithoutTrailingSlash = url.endsWith('/') ? url.slice(0, -1) : url;
|
||||
const incidentUrl = `${urlWithoutTrailingSlash}/${INCIDENT_URL}`;
|
||||
const fieldsUrl = `${urlWithoutTrailingSlash}/${SYS_DICTIONARY}?sysparm_query=name=task^internal_type=string&active=true&read_only=false&sysparm_fields=max_length,element,column_label`;
|
||||
const fieldsUrl = `${urlWithoutTrailingSlash}/${SYS_DICTIONARY}?sysparm_query=name=task^internal_type=string&active=true&array=false&read_only=false&sysparm_fields=max_length,element,column_label,mandatory`;
|
||||
const axiosInstance = axios.create({
|
||||
auth: { username, password },
|
||||
});
|
||||
|
@ -44,6 +44,14 @@ export const createExternalService = (
|
|||
return `${urlWithoutTrailingSlash}/${VIEW_INCIDENT_URL}${id}`;
|
||||
};
|
||||
|
||||
const checkInstance = (res: AxiosResponse) => {
|
||||
if (res.status === 200 && res.data.result == null) {
|
||||
throw new Error(
|
||||
`There is an issue with your Service Now Instance. Please check ${res.request.connection.servername}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const getIncident = async (id: string) => {
|
||||
try {
|
||||
const res = await request({
|
||||
|
@ -52,7 +60,7 @@ export const createExternalService = (
|
|||
logger,
|
||||
proxySettings,
|
||||
});
|
||||
|
||||
checkInstance(res);
|
||||
return { ...res.data.result };
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
|
@ -70,7 +78,7 @@ export const createExternalService = (
|
|||
proxySettings,
|
||||
params,
|
||||
});
|
||||
|
||||
checkInstance(res);
|
||||
return res.data.result.length > 0 ? { ...res.data.result } : undefined;
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
|
@ -89,7 +97,7 @@ export const createExternalService = (
|
|||
method: 'post',
|
||||
data: { ...(incident as Record<string, unknown>) },
|
||||
});
|
||||
|
||||
checkInstance(res);
|
||||
return {
|
||||
title: res.data.result.number,
|
||||
id: res.data.result.sys_id,
|
||||
|
@ -112,7 +120,7 @@ export const createExternalService = (
|
|||
data: { ...(incident as Record<string, unknown>) },
|
||||
proxySettings,
|
||||
});
|
||||
|
||||
checkInstance(res);
|
||||
return {
|
||||
title: res.data.result.number,
|
||||
id: res.data.result.sys_id,
|
||||
|
@ -137,12 +145,10 @@ export const createExternalService = (
|
|||
logger,
|
||||
proxySettings,
|
||||
});
|
||||
|
||||
checkInstance(res);
|
||||
return res.data.result.length > 0 ? res.data.result : [];
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
getErrorMessage(i18n.NAME, `Unable to get common fields. Error: ${error.message}`)
|
||||
);
|
||||
throw new Error(getErrorMessage(i18n.NAME, `Unable to get fields. Error: ${error.message}`));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -17,11 +17,3 @@ export const ALLOWED_HOSTS_ERROR = (message: string) =>
|
|||
message,
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: remove when Case mappings will be removed
|
||||
export const MAPPING_EMPTY = i18n.translate(
|
||||
'xpack.actions.builtin.servicenow.configuration.emptyMapping',
|
||||
{
|
||||
defaultMessage: '[incidentConfiguration.mapping]: expected non-empty but got empty',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -17,8 +17,6 @@ import {
|
|||
ExternalIncidentServiceSecretConfigurationSchema,
|
||||
} from './schema';
|
||||
import { ActionsConfigurationUtilities } from '../../actions_config';
|
||||
import { ExternalServiceCommentResponse } from '../case/types';
|
||||
import { IncidentConfigurationSchema } from '../case/schema';
|
||||
import { Logger } from '../../../../../../src/core/server';
|
||||
|
||||
export type ServiceNowPublicConfigurationType = TypeOf<
|
||||
|
@ -41,8 +39,6 @@ export interface CreateCommentRequest {
|
|||
export type ExecutorParams = TypeOf<typeof ExecutorParamsSchema>;
|
||||
export type ExecutorSubActionPushParams = TypeOf<typeof ExecutorSubActionPushParamsSchema>;
|
||||
|
||||
export type IncidentConfiguration = TypeOf<typeof IncidentConfigurationSchema>;
|
||||
|
||||
export interface ExternalServiceCredentials {
|
||||
config: Record<string, unknown>;
|
||||
secrets: Record<string, unknown>;
|
||||
|
@ -73,13 +69,10 @@ export interface ExternalService {
|
|||
findIncidents: (params?: Record<string, string>) => Promise<ExternalServiceParams[] | undefined>;
|
||||
}
|
||||
|
||||
export interface PushToServiceApiParams extends ExecutorSubActionPushParams {
|
||||
externalObject: Record<string, any>;
|
||||
}
|
||||
export type PushToServiceApiParams = ExecutorSubActionPushParams;
|
||||
|
||||
export interface ExternalServiceApiHandlerArgs {
|
||||
externalService: ExternalService;
|
||||
mapping: Map<string, any> | null;
|
||||
}
|
||||
|
||||
export type ExecutorSubActionGetIncidentParams = TypeOf<
|
||||
|
@ -90,12 +83,7 @@ export type ExecutorSubActionHandshakeParams = TypeOf<
|
|||
typeof ExecutorSubActionHandshakeParamsSchema
|
||||
>;
|
||||
|
||||
export type Incident = Pick<
|
||||
ExecutorSubActionPushParams,
|
||||
'description' | 'severity' | 'urgency' | 'impact'
|
||||
> & {
|
||||
short_description: string;
|
||||
};
|
||||
export type Incident = Omit<ExecutorSubActionPushParams['incident'], 'externalId'>;
|
||||
|
||||
export interface PushToServiceApiHandlerArgs extends ExternalServiceApiHandlerArgs {
|
||||
params: PushToServiceApiParams;
|
||||
|
@ -112,11 +100,7 @@ export interface HandshakeApiHandlerArgs extends ExternalServiceApiHandlerArgs {
|
|||
}
|
||||
export interface ExternalServiceFields {
|
||||
column_label: string;
|
||||
name: string;
|
||||
internal_type: {
|
||||
link: string;
|
||||
value: string;
|
||||
};
|
||||
mandatory: string;
|
||||
max_length: string;
|
||||
element: string;
|
||||
}
|
||||
|
@ -132,3 +116,9 @@ export interface ExternalServiceApi {
|
|||
pushToService: (args: PushToServiceApiHandlerArgs) => Promise<PushToServiceResponse>;
|
||||
getIncident: (args: GetIncidentApiHandlerArgs) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface ExternalServiceCommentResponse {
|
||||
commentId: string;
|
||||
pushedDate: string;
|
||||
externalCommentId?: string;
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { isEmpty } from 'lodash';
|
||||
import { ActionsConfigurationUtilities } from '../../actions_config';
|
||||
import {
|
||||
ServiceNowPublicConfigurationType,
|
||||
|
@ -18,13 +17,6 @@ export const validateCommonConfig = (
|
|||
configurationUtilities: ActionsConfigurationUtilities,
|
||||
configObject: ServiceNowPublicConfigurationType
|
||||
) => {
|
||||
if (
|
||||
configObject.incidentConfiguration !== null &&
|
||||
isEmpty(configObject.incidentConfiguration.mapping)
|
||||
) {
|
||||
return i18n.MAPPING_EMPTY;
|
||||
}
|
||||
|
||||
try {
|
||||
configurationUtilities.ensureUriAllowed(configObject.apiUrl);
|
||||
} catch (allowedListError) {
|
||||
|
|
|
@ -93,6 +93,21 @@ describe('7.11.0', () => {
|
|||
},
|
||||
});
|
||||
});
|
||||
test('remove cases mapping object', () => {
|
||||
const migration711 = getMigrations(encryptedSavedObjectsSetup)['7.11.0'];
|
||||
const action = getMockData({
|
||||
config: { incidentConfiguration: { mapping: [] }, isCaseOwned: true, another: 'value' },
|
||||
});
|
||||
expect(migration711(action, context)).toEqual({
|
||||
...action,
|
||||
attributes: {
|
||||
...action.attributes,
|
||||
config: {
|
||||
another: 'value',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function getMockDataForWebhook(
|
||||
|
|
|
@ -19,24 +19,23 @@ type ActionMigration = (
|
|||
export function getMigrations(
|
||||
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup
|
||||
): SavedObjectMigrationMap {
|
||||
const migrationActions = encryptedSavedObjects.createMigration<RawAction, RawAction>(
|
||||
const migrationActionsTen = encryptedSavedObjects.createMigration<RawAction, RawAction>(
|
||||
(doc): doc is SavedObjectUnsanitizedDoc<RawAction> =>
|
||||
!!doc.attributes.config?.casesConfiguration || doc.attributes.actionTypeId === '.email',
|
||||
pipeMigrations(renameCasesConfigurationObject, addHasAuthConfigurationObject)
|
||||
);
|
||||
|
||||
const migrationWebhookConnectorHasAuth = encryptedSavedObjects.createMigration<
|
||||
RawAction,
|
||||
RawAction
|
||||
>(
|
||||
const migrationActionsEleven = encryptedSavedObjects.createMigration<RawAction, RawAction>(
|
||||
(doc): doc is SavedObjectUnsanitizedDoc<RawAction> =>
|
||||
!!doc.attributes.config?.isCaseOwned ||
|
||||
!!doc.attributes.config?.incidentConfiguration ||
|
||||
doc.attributes.actionTypeId === '.webhook',
|
||||
pipeMigrations(addHasAuthConfigurationObject)
|
||||
pipeMigrations(removeCasesFieldMappings, addHasAuthConfigurationObject)
|
||||
);
|
||||
|
||||
return {
|
||||
'7.10.0': executeMigrationWithErrorHandling(migrationActions, '7.10.0'),
|
||||
'7.11.0': executeMigrationWithErrorHandling(migrationWebhookConnectorHasAuth, '7.11.0'),
|
||||
'7.10.0': executeMigrationWithErrorHandling(migrationActionsTen, '7.10.0'),
|
||||
'7.11.0': executeMigrationWithErrorHandling(migrationActionsEleven, '7.11.0'),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -77,6 +76,26 @@ function renameCasesConfigurationObject(
|
|||
};
|
||||
}
|
||||
|
||||
function removeCasesFieldMappings(
|
||||
doc: SavedObjectUnsanitizedDoc<RawAction>
|
||||
): SavedObjectUnsanitizedDoc<RawAction> {
|
||||
if (
|
||||
!doc.attributes.config?.hasOwnProperty('isCaseOwned') &&
|
||||
!doc.attributes.config?.hasOwnProperty('incidentConfiguration')
|
||||
) {
|
||||
return doc;
|
||||
}
|
||||
const { incidentConfiguration, isCaseOwned, ...restConfiguration } = doc.attributes.config;
|
||||
|
||||
return {
|
||||
...doc,
|
||||
attributes: {
|
||||
...doc.attributes,
|
||||
config: restConfiguration,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const addHasAuthConfigurationObject = (
|
||||
doc: SavedObjectUnsanitizedDoc<RawAction>
|
||||
): SavedObjectUnsanitizedDoc<RawAction> => {
|
||||
|
|
|
@ -18,6 +18,9 @@ import {
|
|||
} from '../../../../src/core/server';
|
||||
import { ActionTypeExecutorResult } from '../common';
|
||||
export { ActionTypeExecutorResult } from '../common';
|
||||
export { GetFieldsByIssueTypeResponse as JiraGetFieldsResponse } from './builtin_action_types/jira/types';
|
||||
export { GetCommonFieldsResponse as ServiceNowGetFieldsResponse } from './builtin_action_types/servicenow/types';
|
||||
export { GetCommonFieldsResponse as ResilientGetFieldsResponse } from './builtin_action_types/resilient/types';
|
||||
|
||||
export type WithoutQueryAndParams<T> = Pick<T, Exclude<keyof T, 'query' | 'params'>>;
|
||||
export type GetServicesFunction = (request: KibanaRequest) => Services;
|
||||
|
|
|
@ -10,10 +10,7 @@ import { NumberFromString } from '../saved_object';
|
|||
import { UserRT } from '../user';
|
||||
import { CommentResponseRt } from './comment';
|
||||
import { CasesStatusResponseRt } from './status';
|
||||
import { CaseConnectorRt, ESCaseConnector, ConnectorPartialFieldsRt } from '../connectors';
|
||||
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
export { ActionTypeExecutorResult } from '../../../../actions/server/types';
|
||||
import { CaseConnectorRt, ESCaseConnector } from '../connectors';
|
||||
|
||||
export enum CaseStatuses {
|
||||
open = 'open',
|
||||
|
@ -128,66 +125,6 @@ export const CasePatchRequestRt = rt.intersection([
|
|||
export const CasesPatchRequestRt = rt.type({ cases: rt.array(CasePatchRequestRt) });
|
||||
export const CasesResponseRt = rt.array(CaseResponseRt);
|
||||
|
||||
/*
|
||||
* This type are related to this file below
|
||||
* x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts
|
||||
* why because this schema is not share in a common folder
|
||||
* so we redefine then so we can use/validate types
|
||||
*/
|
||||
|
||||
// TODO: Refactor to support multiple connectors with various fields
|
||||
|
||||
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 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 CaseAttributes = rt.TypeOf<typeof CaseAttributesRt>;
|
||||
export type CasePostRequest = rt.TypeOf<typeof CasePostRequestRt>;
|
||||
export type CaseResponse = rt.TypeOf<typeof CaseResponseRt>;
|
||||
|
@ -196,10 +133,7 @@ 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 ServiceConnectorCaseParams = rt.TypeOf<typeof ServiceConnectorCaseParamsRt>;
|
||||
export type ServiceConnectorCaseResponse = rt.TypeOf<typeof ServiceConnectorCaseResponseRt>;
|
||||
export type CaseFullExternalService = rt.TypeOf<typeof CaseFullExternalServiceRt>;
|
||||
export type ServiceConnectorCommentParams = rt.TypeOf<typeof ServiceConnectorCommentParamsRt>;
|
||||
|
||||
export type ESCaseAttributes = Omit<CaseAttributes, 'connector'> & { connector: ESCaseConnector };
|
||||
export type ESCasePatchRequest = Omit<CasePatchRequest, 'connector'> & {
|
||||
|
|
|
@ -8,60 +8,7 @@ import * as rt from 'io-ts';
|
|||
|
||||
import { ActionResult } from '../../../../actions/common';
|
||||
import { UserRT } from '../user';
|
||||
import { JiraCaseFieldsRt } from '../connectors/jira';
|
||||
import { ServiceNowCaseFieldsRT } from '../connectors/servicenow';
|
||||
import { ResilientCaseFieldsRT } from '../connectors/resilient';
|
||||
import { CaseConnectorRt, ESCaseConnector } from '../connectors';
|
||||
|
||||
/*
|
||||
* This types below are related to the service now configuration
|
||||
* mapping between our case and [service-now, jira]
|
||||
*
|
||||
*/
|
||||
|
||||
const ActionTypeRT = rt.union([
|
||||
rt.literal('append'),
|
||||
rt.literal('nothing'),
|
||||
rt.literal('overwrite'),
|
||||
]);
|
||||
|
||||
const CaseFieldRT = rt.union([
|
||||
rt.literal('title'),
|
||||
rt.literal('description'),
|
||||
rt.literal('comments'),
|
||||
]);
|
||||
|
||||
const ThirdPartyFieldRT = rt.union([
|
||||
JiraCaseFieldsRt,
|
||||
ServiceNowCaseFieldsRT,
|
||||
ResilientCaseFieldsRT,
|
||||
rt.literal('not_mapped'),
|
||||
]);
|
||||
|
||||
export const CasesConfigurationMapsRT = rt.type({
|
||||
source: CaseFieldRT,
|
||||
target: ThirdPartyFieldRT,
|
||||
action_type: ActionTypeRT,
|
||||
});
|
||||
|
||||
export const CasesConfigurationRT = rt.type({
|
||||
mapping: rt.array(CasesConfigurationMapsRT),
|
||||
});
|
||||
|
||||
export const CasesConnectorConfigurationRT = rt.type({
|
||||
cases_configuration: CasesConfigurationRT,
|
||||
// version: rt.string,
|
||||
});
|
||||
|
||||
export type ActionType = rt.TypeOf<typeof ActionTypeRT>;
|
||||
export type CaseField = rt.TypeOf<typeof CaseFieldRT>;
|
||||
export type ThirdPartyField = rt.TypeOf<typeof ThirdPartyFieldRT>;
|
||||
|
||||
export type CasesConfigurationMaps = rt.TypeOf<typeof CasesConfigurationMapsRT>;
|
||||
export type CasesConfiguration = rt.TypeOf<typeof CasesConfigurationRT>;
|
||||
export type CasesConnectorConfiguration = rt.TypeOf<typeof CasesConnectorConfigurationRT>;
|
||||
|
||||
/** ********************************************************************** */
|
||||
import { CaseConnectorRt, ConnectorMappingsRt, ESCaseConnector } from '../connectors';
|
||||
|
||||
export type ActionConnector = ActionResult;
|
||||
|
||||
|
@ -91,6 +38,7 @@ export const CaseConfigureAttributesRt = rt.intersection([
|
|||
|
||||
export const CaseConfigureResponseRt = rt.intersection([
|
||||
CaseConfigureAttributesRt,
|
||||
ConnectorMappingsRt,
|
||||
rt.type({
|
||||
version: rt.string,
|
||||
}),
|
||||
|
|
|
@ -12,6 +12,7 @@ import { ServiceNowFieldsRT } from './servicenow';
|
|||
export * from './jira';
|
||||
export * from './servicenow';
|
||||
export * from './resilient';
|
||||
export * from './mappings';
|
||||
|
||||
export const ConnectorFieldsRt = rt.union([
|
||||
JiraFieldsRT,
|
||||
|
@ -19,13 +20,6 @@ export const ConnectorFieldsRt = rt.union([
|
|||
ServiceNowFieldsRT,
|
||||
rt.null,
|
||||
]);
|
||||
|
||||
export const ConnectorPartialFieldsRt = rt.partial({
|
||||
...JiraFieldsRT.props,
|
||||
...ResilientFieldsRT.props,
|
||||
...ServiceNowFieldsRT.props,
|
||||
});
|
||||
|
||||
export enum ConnectorTypes {
|
||||
jira = '.jira',
|
||||
resilient = '.resilient',
|
||||
|
|
|
@ -6,12 +6,6 @@
|
|||
|
||||
import * as rt from 'io-ts';
|
||||
|
||||
export const JiraCaseFieldsRt = rt.union([
|
||||
rt.literal('summary'),
|
||||
rt.literal('description'),
|
||||
rt.literal('comments'),
|
||||
]);
|
||||
|
||||
export const JiraFieldsRT = rt.type({
|
||||
issueType: rt.union([rt.string, rt.null]),
|
||||
priority: rt.union([rt.string, rt.null]),
|
||||
|
|
191
x-pack/plugins/case/common/api/connectors/mappings.ts
Normal file
191
x-pack/plugins/case/common/api/connectors/mappings.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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @kbn/eslint/no-restricted-paths */
|
||||
|
||||
import * as rt from 'io-ts';
|
||||
import { ElasticUser } from '../../../../security_solution/public/cases/containers/types';
|
||||
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 {
|
||||
PushToServiceApiParams as ServiceNowPushToServiceApiParams,
|
||||
Incident as ServiceNowIncident,
|
||||
} from '../../../../actions/server/builtin_action_types/servicenow/types';
|
||||
import { ResilientFieldsRT } from './resilient';
|
||||
import { ServiceNowFieldsRT } from './servicenow';
|
||||
import { JiraFieldsRT } from './jira';
|
||||
|
||||
export {
|
||||
JiraPushToServiceApiParams,
|
||||
ResilientPushToServiceApiParams,
|
||||
ServiceNowPushToServiceApiParams,
|
||||
};
|
||||
export type Incident = JiraIncident | ResilientIncident | ServiceNowIncident;
|
||||
export type PushToServiceApiParams =
|
||||
| JiraPushToServiceApiParams
|
||||
| ResilientPushToServiceApiParams
|
||||
| ServiceNowPushToServiceApiParams;
|
||||
|
||||
const ActionTypeRT = rt.union([
|
||||
rt.literal('append'),
|
||||
rt.literal('nothing'),
|
||||
rt.literal('overwrite'),
|
||||
]);
|
||||
const CaseFieldRT = rt.union([
|
||||
rt.literal('title'),
|
||||
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>;
|
||||
export type ThirdPartyField = rt.TypeOf<typeof ThirdPartyFieldRT>;
|
||||
|
||||
export const ConnectorMappingsAttributesRT = rt.type({
|
||||
action_type: ActionTypeRT,
|
||||
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>;
|
||||
|
||||
const FieldTypeRT = rt.union([rt.literal('text'), rt.literal('textarea')]);
|
||||
|
||||
const ConnectorFieldRt = rt.type({
|
||||
id: rt.string,
|
||||
name: rt.string,
|
||||
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 interface SimpleComment {
|
||||
comment: string;
|
||||
commentId: string;
|
||||
}
|
||||
|
||||
export interface MapIncident {
|
||||
incident: ExternalServiceParams;
|
||||
comments: SimpleComment[];
|
||||
}
|
|
@ -6,12 +6,6 @@
|
|||
|
||||
import * as rt from 'io-ts';
|
||||
|
||||
export const ResilientCaseFieldsRT = rt.union([
|
||||
rt.literal('name'),
|
||||
rt.literal('description'),
|
||||
rt.literal('comments'),
|
||||
]);
|
||||
|
||||
export const ResilientFieldsRT = rt.type({
|
||||
incidentTypes: rt.union([rt.array(rt.string), rt.null]),
|
||||
severityCode: rt.union([rt.string, rt.null]),
|
||||
|
|
|
@ -6,12 +6,6 @@
|
|||
|
||||
import * as rt from 'io-ts';
|
||||
|
||||
export const ServiceNowCaseFieldsRT = rt.union([
|
||||
rt.literal('short_description'),
|
||||
rt.literal('description'),
|
||||
rt.literal('comments'),
|
||||
]);
|
||||
|
||||
export const ServiceNowFieldsRT = rt.type({
|
||||
impact: rt.union([rt.string, rt.null]),
|
||||
severity: rt.union([rt.string, rt.null]),
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
CASE_COMMENTS_URL,
|
||||
CASE_USER_ACTIONS_URL,
|
||||
CASE_COMMENT_DETAILS_URL,
|
||||
CASE_CONFIGURE_PUSH_URL,
|
||||
} from '../constants';
|
||||
|
||||
export const getCaseDetailsUrl = (id: string): string => {
|
||||
|
@ -26,3 +27,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);
|
||||
};
|
||||
|
|
|
@ -21,6 +21,7 @@ export const NumberFromString = new rt.Type<number, string, unknown>(
|
|||
|
||||
export const SavedObjectFindOptionsRt = rt.partial({
|
||||
defaultSearchOperator: rt.union([rt.literal('AND'), rt.literal('OR')]),
|
||||
hasReference: rt.type({ id: rt.string, type: rt.string }),
|
||||
fields: rt.array(rt.string),
|
||||
filter: rt.string,
|
||||
page: NumberFromString,
|
||||
|
|
|
@ -14,6 +14,8 @@ 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_REPORTERS_URL = `${CASES_URL}/reporters`;
|
||||
|
|
31
x-pack/plugins/case/server/client/configure/get_fields.ts
Normal file
31
x-pack/plugins/case/server/client/configure/get_fields.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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import Boom from '@hapi/boom';
|
||||
|
||||
import { GetFieldsResponse } from '../../../common/api';
|
||||
import { ConfigureFields } from '../types';
|
||||
import { createDefaultMapping, formatFields } from './utils';
|
||||
|
||||
export const getFields = () => async ({
|
||||
actionsClient,
|
||||
connectorType,
|
||||
connectorId,
|
||||
}: ConfigureFields): Promise<GetFieldsResponse> => {
|
||||
const results = await actionsClient.execute({
|
||||
actionId: connectorId,
|
||||
params: {
|
||||
subAction: 'getFields',
|
||||
subActionParams: {},
|
||||
},
|
||||
});
|
||||
if (results.status === 'error') {
|
||||
throw Boom.failedDependency(results.serviceMessage);
|
||||
}
|
||||
const fields = formatFields(results.data, connectorType);
|
||||
|
||||
return { fields, defaultMappings: createDefaultMapping(fields, connectorType) };
|
||||
};
|
58
x-pack/plugins/case/server/client/configure/get_mappings.ts
Normal file
58
x-pack/plugins/case/server/client/configure/get_mappings.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { ConnectorMappingsAttributes, ConnectorTypes } from '../../../common/api';
|
||||
import { CaseClientFactoryArguments, MappingsClient } from '../types';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server/saved_objects';
|
||||
|
||||
export const getMappings = ({
|
||||
savedObjectsClient,
|
||||
connectorMappingsService,
|
||||
}: CaseClientFactoryArguments) => async ({
|
||||
actionsClient,
|
||||
caseClient,
|
||||
connectorType,
|
||||
connectorId,
|
||||
}: MappingsClient): Promise<ConnectorMappingsAttributes[]> => {
|
||||
if (connectorType === ConnectorTypes.none) {
|
||||
return [];
|
||||
}
|
||||
const myConnectorMappings = await connectorMappingsService.find({
|
||||
client: savedObjectsClient,
|
||||
options: {
|
||||
hasReference: {
|
||||
type: ACTION_SAVED_OBJECT_TYPE,
|
||||
id: connectorId,
|
||||
},
|
||||
},
|
||||
});
|
||||
let theMapping;
|
||||
// Create connector mappings if there are none
|
||||
if (myConnectorMappings.total === 0) {
|
||||
const res = await caseClient.getFields({
|
||||
actionsClient,
|
||||
connectorId,
|
||||
connectorType,
|
||||
});
|
||||
theMapping = await connectorMappingsService.post({
|
||||
client: savedObjectsClient,
|
||||
attributes: {
|
||||
mappings: res.defaultMappings,
|
||||
},
|
||||
references: [
|
||||
{
|
||||
type: ACTION_SAVED_OBJECT_TYPE,
|
||||
name: `associated-${ACTION_SAVED_OBJECT_TYPE}`,
|
||||
id: connectorId,
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
theMapping = myConnectorMappings.saved_objects[0];
|
||||
}
|
||||
return theMapping ? theMapping.attributes.mappings : [];
|
||||
};
|
545
x-pack/plugins/case/server/client/configure/utils.test.ts
Normal file
545
x-pack/plugins/case/server/client/configure/utils.test.ts
Normal file
|
@ -0,0 +1,545 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
JiraGetFieldsResponse,
|
||||
ResilientGetFieldsResponse,
|
||||
ServiceNowGetFieldsResponse,
|
||||
} from '../../../../actions/server/types';
|
||||
import { formatFields } from './utils';
|
||||
import { ConnectorTypes } from '../../../common/api/connectors';
|
||||
|
||||
const jiraFields: JiraGetFieldsResponse = {
|
||||
summary: {
|
||||
required: true,
|
||||
allowedValues: [],
|
||||
defaultValue: {},
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
name: 'Summary',
|
||||
},
|
||||
issuetype: {
|
||||
required: true,
|
||||
allowedValues: [
|
||||
{
|
||||
self: 'https://siem-kibana.atlassian.net/rest/api/2/issuetype/10023',
|
||||
id: '10023',
|
||||
description: 'A problem or error.',
|
||||
iconUrl:
|
||||
'https://siem-kibana.atlassian.net/secure/viewavatar?size=medium&avatarId=10303&avatarType=issuetype',
|
||||
name: 'Bug',
|
||||
subtask: false,
|
||||
avatarId: 10303,
|
||||
},
|
||||
],
|
||||
defaultValue: {},
|
||||
schema: {
|
||||
type: 'issuetype',
|
||||
},
|
||||
name: 'Issue Type',
|
||||
},
|
||||
attachment: {
|
||||
required: false,
|
||||
allowedValues: [],
|
||||
defaultValue: {},
|
||||
schema: {
|
||||
type: 'array',
|
||||
items: 'attachment',
|
||||
},
|
||||
name: 'Attachment',
|
||||
},
|
||||
duedate: {
|
||||
required: false,
|
||||
allowedValues: [],
|
||||
defaultValue: {},
|
||||
schema: {
|
||||
type: 'date',
|
||||
},
|
||||
name: 'Due date',
|
||||
},
|
||||
description: {
|
||||
required: false,
|
||||
allowedValues: [],
|
||||
defaultValue: {},
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
name: 'Description',
|
||||
},
|
||||
project: {
|
||||
required: true,
|
||||
allowedValues: [
|
||||
{
|
||||
self: 'https://siem-kibana.atlassian.net/rest/api/2/project/10015',
|
||||
id: '10015',
|
||||
key: 'RJ2',
|
||||
name: 'RJ2',
|
||||
projectTypeKey: 'business',
|
||||
simplified: false,
|
||||
avatarUrls: {
|
||||
'48x48':
|
||||
'https://siem-kibana.atlassian.net/secure/projectavatar?pid=10015&avatarId=10412',
|
||||
'24x24':
|
||||
'https://siem-kibana.atlassian.net/secure/projectavatar?size=small&s=small&pid=10015&avatarId=10412',
|
||||
'16x16':
|
||||
'https://siem-kibana.atlassian.net/secure/projectavatar?size=xsmall&s=xsmall&pid=10015&avatarId=10412',
|
||||
'32x32':
|
||||
'https://siem-kibana.atlassian.net/secure/projectavatar?size=medium&s=medium&pid=10015&avatarId=10412',
|
||||
},
|
||||
},
|
||||
],
|
||||
defaultValue: {},
|
||||
schema: {
|
||||
type: 'project',
|
||||
},
|
||||
name: 'Project',
|
||||
},
|
||||
assignee: {
|
||||
required: false,
|
||||
allowedValues: [],
|
||||
defaultValue: {},
|
||||
schema: {
|
||||
type: 'user',
|
||||
},
|
||||
name: 'Assignee',
|
||||
},
|
||||
labels: {
|
||||
required: false,
|
||||
allowedValues: [],
|
||||
defaultValue: {},
|
||||
schema: {
|
||||
type: 'array',
|
||||
items: 'string',
|
||||
},
|
||||
name: 'Labels',
|
||||
},
|
||||
};
|
||||
const resilientFields: ResilientGetFieldsResponse = [
|
||||
{ input_type: 'text', name: 'addr', read_only: false, text: 'Address' },
|
||||
{
|
||||
input_type: 'boolean',
|
||||
name: 'alberta_health_risk_assessment',
|
||||
read_only: false,
|
||||
text: 'Alberta Health Risk Assessment',
|
||||
},
|
||||
{ input_type: 'number', name: 'hard_liability', read_only: true, text: 'Assessed Liability' },
|
||||
{ input_type: 'text', name: 'city', read_only: false, text: 'City' },
|
||||
{ input_type: 'select', name: 'country', read_only: false, text: 'Country/Region' },
|
||||
{ input_type: 'select_owner', name: 'creator_id', read_only: true, text: 'Created By' },
|
||||
{ input_type: 'select', name: 'crimestatus_id', read_only: false, text: 'Criminal Activity' },
|
||||
{ input_type: 'boolean', name: 'data_encrypted', read_only: false, text: 'Data Encrypted' },
|
||||
{ input_type: 'select', name: 'data_format', read_only: false, text: 'Data Format' },
|
||||
{ input_type: 'datetimepicker', name: 'end_date', read_only: true, text: 'Date Closed' },
|
||||
{ input_type: 'datetimepicker', name: 'create_date', read_only: true, text: 'Date Created' },
|
||||
{
|
||||
input_type: 'datetimepicker',
|
||||
name: 'determined_date',
|
||||
read_only: false,
|
||||
text: 'Date Determined',
|
||||
},
|
||||
{
|
||||
input_type: 'datetimepicker',
|
||||
name: 'discovered_date',
|
||||
read_only: false,
|
||||
required: 'always',
|
||||
text: 'Date Discovered',
|
||||
},
|
||||
{ input_type: 'datetimepicker', name: 'start_date', read_only: false, text: 'Date Occurred' },
|
||||
{ input_type: 'select', name: 'exposure_dept_id', read_only: false, text: 'Department' },
|
||||
{ input_type: 'textarea', name: 'description', read_only: false, text: 'Description' },
|
||||
{ input_type: 'boolean', name: 'employee_involved', read_only: false, text: 'Employee Involved' },
|
||||
{ input_type: 'boolean', name: 'data_contained', read_only: false, text: 'Exposure Resolved' },
|
||||
{ input_type: 'select', name: 'exposure_type_id', read_only: false, text: 'Exposure Type' },
|
||||
{
|
||||
input_type: 'multiselect',
|
||||
name: 'gdpr_breach_circumstances',
|
||||
read_only: false,
|
||||
text: 'GDPR Breach Circumstances',
|
||||
},
|
||||
{ input_type: 'select', name: 'gdpr_breach_type', read_only: false, text: 'GDPR Breach Type' },
|
||||
{
|
||||
input_type: 'textarea',
|
||||
name: 'gdpr_breach_type_comment',
|
||||
read_only: false,
|
||||
text: 'GDPR Breach Type Comment',
|
||||
},
|
||||
{ input_type: 'select', name: 'gdpr_consequences', read_only: false, text: 'GDPR Consequences' },
|
||||
{
|
||||
input_type: 'textarea',
|
||||
name: 'gdpr_consequences_comment',
|
||||
read_only: false,
|
||||
text: 'GDPR Consequences Comment',
|
||||
},
|
||||
{
|
||||
input_type: 'select',
|
||||
name: 'gdpr_final_assessment',
|
||||
read_only: false,
|
||||
text: 'GDPR Final Assessment',
|
||||
},
|
||||
{
|
||||
input_type: 'textarea',
|
||||
name: 'gdpr_final_assessment_comment',
|
||||
read_only: false,
|
||||
text: 'GDPR Final Assessment Comment',
|
||||
},
|
||||
{
|
||||
input_type: 'select',
|
||||
name: 'gdpr_identification',
|
||||
read_only: false,
|
||||
text: 'GDPR Identification',
|
||||
},
|
||||
{
|
||||
input_type: 'textarea',
|
||||
name: 'gdpr_identification_comment',
|
||||
read_only: false,
|
||||
text: 'GDPR Identification Comment',
|
||||
},
|
||||
{
|
||||
input_type: 'select',
|
||||
name: 'gdpr_personal_data',
|
||||
read_only: false,
|
||||
text: 'GDPR Personal Data',
|
||||
},
|
||||
{
|
||||
input_type: 'textarea',
|
||||
name: 'gdpr_personal_data_comment',
|
||||
read_only: false,
|
||||
text: 'GDPR Personal Data Comment',
|
||||
},
|
||||
{
|
||||
input_type: 'boolean',
|
||||
name: 'gdpr_subsequent_notification',
|
||||
read_only: false,
|
||||
text: 'GDPR Subsequent Notification',
|
||||
},
|
||||
{ input_type: 'number', name: 'id', read_only: true, text: 'ID' },
|
||||
{ input_type: 'boolean', name: 'impact_likely', read_only: false, text: 'Impact Likely' },
|
||||
{
|
||||
input_type: 'boolean',
|
||||
name: 'ny_impact_likely',
|
||||
read_only: false,
|
||||
text: 'Impact Likely for New York',
|
||||
},
|
||||
{
|
||||
input_type: 'boolean',
|
||||
name: 'or_impact_likely',
|
||||
read_only: false,
|
||||
text: 'Impact Likely for Oregon',
|
||||
},
|
||||
{
|
||||
input_type: 'boolean',
|
||||
name: 'wa_impact_likely',
|
||||
read_only: false,
|
||||
text: 'Impact Likely for Washington',
|
||||
},
|
||||
{ input_type: 'boolean', name: 'confirmed', read_only: false, text: 'Incident Disposition' },
|
||||
{ input_type: 'multiselect', name: 'incident_type_ids', read_only: false, text: 'Incident Type' },
|
||||
{
|
||||
input_type: 'text',
|
||||
name: 'exposure_individual_name',
|
||||
read_only: false,
|
||||
text: 'Individual Name',
|
||||
},
|
||||
{
|
||||
input_type: 'select',
|
||||
name: 'harmstatus_id',
|
||||
read_only: false,
|
||||
text: 'Is harm/risk/misuse foreseeable?',
|
||||
},
|
||||
{ input_type: 'text', name: 'jurisdiction_name', read_only: false, text: 'Jurisdiction' },
|
||||
{
|
||||
input_type: 'datetimepicker',
|
||||
name: 'inc_last_modified_date',
|
||||
read_only: true,
|
||||
text: 'Last Modified',
|
||||
},
|
||||
{
|
||||
input_type: 'multiselect',
|
||||
name: 'gdpr_lawful_data_processing_categories',
|
||||
read_only: false,
|
||||
text: 'Lawful Data Processing Categories',
|
||||
},
|
||||
{ input_type: 'multiselect_members', name: 'members', read_only: false, text: 'Members' },
|
||||
{ input_type: 'text', name: 'name', read_only: false, required: 'always', text: 'Name' },
|
||||
{ input_type: 'boolean', name: 'negative_pr_likely', read_only: false, text: 'Negative PR' },
|
||||
{ input_type: 'datetimepicker', name: 'due_date', read_only: true, text: 'Next Due Date' },
|
||||
{
|
||||
input_type: 'multiselect',
|
||||
name: 'nist_attack_vectors',
|
||||
read_only: false,
|
||||
text: 'NIST Attack Vectors',
|
||||
},
|
||||
{ input_type: 'select', name: 'org_handle', read_only: true, text: 'Organization' },
|
||||
{ input_type: 'select_owner', name: 'owner_id', read_only: false, text: 'Owner' },
|
||||
{ input_type: 'select', name: 'phase_id', read_only: true, text: 'Phase' },
|
||||
{
|
||||
input_type: 'select',
|
||||
name: 'pipeda_other_factors',
|
||||
read_only: false,
|
||||
text: 'PIPEDA Other Factors',
|
||||
},
|
||||
{
|
||||
input_type: 'textarea',
|
||||
name: 'pipeda_other_factors_comment',
|
||||
read_only: false,
|
||||
text: 'PIPEDA Other Factors Comment',
|
||||
},
|
||||
{
|
||||
input_type: 'select',
|
||||
name: 'pipeda_overall_assessment',
|
||||
read_only: false,
|
||||
text: 'PIPEDA Overall Assessment',
|
||||
},
|
||||
{
|
||||
input_type: 'textarea',
|
||||
name: 'pipeda_overall_assessment_comment',
|
||||
read_only: false,
|
||||
text: 'PIPEDA Overall Assessment Comment',
|
||||
},
|
||||
{
|
||||
input_type: 'select',
|
||||
name: 'pipeda_probability_of_misuse',
|
||||
read_only: false,
|
||||
text: 'PIPEDA Probability of Misuse',
|
||||
},
|
||||
{
|
||||
input_type: 'textarea',
|
||||
name: 'pipeda_probability_of_misuse_comment',
|
||||
read_only: false,
|
||||
text: 'PIPEDA Probability of Misuse Comment',
|
||||
},
|
||||
{
|
||||
input_type: 'select',
|
||||
name: 'pipeda_sensitivity_of_pi',
|
||||
read_only: false,
|
||||
text: 'PIPEDA Sensitivity of PI',
|
||||
},
|
||||
{
|
||||
input_type: 'textarea',
|
||||
name: 'pipeda_sensitivity_of_pi_comment',
|
||||
read_only: false,
|
||||
text: 'PIPEDA Sensitivity of PI Comment',
|
||||
},
|
||||
{ input_type: 'text', name: 'reporter', read_only: false, text: 'Reporting Individual' },
|
||||
{
|
||||
input_type: 'select',
|
||||
name: 'resolution_id',
|
||||
read_only: false,
|
||||
required: 'close',
|
||||
text: 'Resolution',
|
||||
},
|
||||
{
|
||||
input_type: 'textarea',
|
||||
name: 'resolution_summary',
|
||||
read_only: false,
|
||||
required: 'close',
|
||||
text: 'Resolution Summary',
|
||||
},
|
||||
{ input_type: 'select', name: 'gdpr_harm_risk', read_only: false, text: 'Risk of Harm' },
|
||||
{ input_type: 'select', name: 'severity_code', read_only: false, text: 'Severity' },
|
||||
{ input_type: 'boolean', name: 'inc_training', read_only: true, text: 'Simulation' },
|
||||
{ input_type: 'multiselect', name: 'data_source_ids', read_only: false, text: 'Source of Data' },
|
||||
{ input_type: 'select', name: 'state', read_only: false, text: 'State' },
|
||||
{ input_type: 'select', name: 'plan_status', read_only: false, text: 'Status' },
|
||||
{ input_type: 'select', name: 'exposure_vendor_id', read_only: false, text: 'Vendor' },
|
||||
{
|
||||
input_type: 'boolean',
|
||||
name: 'data_compromised',
|
||||
read_only: false,
|
||||
text: 'Was personal information or personal data involved?',
|
||||
},
|
||||
{
|
||||
input_type: 'select',
|
||||
name: 'workspace',
|
||||
read_only: false,
|
||||
required: 'always',
|
||||
text: 'Workspace',
|
||||
},
|
||||
{ input_type: 'text', name: 'zip', read_only: false, text: 'Zip' },
|
||||
];
|
||||
const serviceNowFields: ServiceNowGetFieldsResponse = [
|
||||
{
|
||||
column_label: 'Approval',
|
||||
mandatory: 'false',
|
||||
max_length: '40',
|
||||
element: 'approval',
|
||||
},
|
||||
{
|
||||
column_label: 'Close notes',
|
||||
mandatory: 'false',
|
||||
max_length: '4000',
|
||||
element: 'close_notes',
|
||||
},
|
||||
{
|
||||
column_label: 'Contact type',
|
||||
mandatory: 'false',
|
||||
max_length: '40',
|
||||
element: 'contact_type',
|
||||
},
|
||||
{
|
||||
column_label: 'Correlation display',
|
||||
mandatory: 'false',
|
||||
max_length: '100',
|
||||
element: 'correlation_display',
|
||||
},
|
||||
{
|
||||
column_label: 'Correlation ID',
|
||||
mandatory: 'false',
|
||||
max_length: '100',
|
||||
element: 'correlation_id',
|
||||
},
|
||||
{
|
||||
column_label: 'Description',
|
||||
mandatory: 'false',
|
||||
max_length: '4000',
|
||||
element: 'description',
|
||||
},
|
||||
{
|
||||
column_label: 'Number',
|
||||
mandatory: 'false',
|
||||
max_length: '40',
|
||||
element: 'number',
|
||||
},
|
||||
{
|
||||
column_label: 'Short description',
|
||||
mandatory: 'false',
|
||||
max_length: '160',
|
||||
element: 'short_description',
|
||||
},
|
||||
{
|
||||
column_label: 'Created by',
|
||||
mandatory: 'false',
|
||||
max_length: '40',
|
||||
element: 'sys_created_by',
|
||||
},
|
||||
{
|
||||
column_label: 'Updated by',
|
||||
mandatory: 'false',
|
||||
max_length: '40',
|
||||
element: 'sys_updated_by',
|
||||
},
|
||||
{
|
||||
column_label: 'Upon approval',
|
||||
mandatory: 'false',
|
||||
max_length: '40',
|
||||
element: 'upon_approval',
|
||||
},
|
||||
{
|
||||
column_label: 'Upon reject',
|
||||
mandatory: 'false',
|
||||
max_length: '40',
|
||||
element: 'upon_reject',
|
||||
},
|
||||
];
|
||||
|
||||
const formatFieldsTestData = [
|
||||
{
|
||||
expected: [
|
||||
{ id: 'summary', name: 'Summary', required: true, type: 'text' },
|
||||
{ id: 'description', name: 'Description', required: false, type: 'text' },
|
||||
],
|
||||
fields: jiraFields,
|
||||
type: ConnectorTypes.jira,
|
||||
},
|
||||
{
|
||||
expected: [
|
||||
{ id: 'addr', name: 'Address', required: false, type: 'text' },
|
||||
{ id: 'city', name: 'City', required: false, type: 'text' },
|
||||
{ id: 'description', name: 'Description', required: false, type: 'textarea' },
|
||||
{
|
||||
id: 'gdpr_breach_type_comment',
|
||||
name: 'GDPR Breach Type Comment',
|
||||
required: false,
|
||||
type: 'textarea',
|
||||
},
|
||||
{
|
||||
id: 'gdpr_consequences_comment',
|
||||
name: 'GDPR Consequences Comment',
|
||||
required: false,
|
||||
type: 'textarea',
|
||||
},
|
||||
{
|
||||
id: 'gdpr_final_assessment_comment',
|
||||
name: 'GDPR Final Assessment Comment',
|
||||
required: false,
|
||||
type: 'textarea',
|
||||
},
|
||||
{
|
||||
id: 'gdpr_identification_comment',
|
||||
name: 'GDPR Identification Comment',
|
||||
required: false,
|
||||
type: 'textarea',
|
||||
},
|
||||
{
|
||||
id: 'gdpr_personal_data_comment',
|
||||
name: 'GDPR Personal Data Comment',
|
||||
required: false,
|
||||
type: 'textarea',
|
||||
},
|
||||
{ id: 'exposure_individual_name', name: 'Individual Name', required: false, type: 'text' },
|
||||
{ id: 'jurisdiction_name', name: 'Jurisdiction', required: false, type: 'text' },
|
||||
{ id: 'name', name: 'Name', required: true, type: 'text' },
|
||||
{
|
||||
id: 'pipeda_other_factors_comment',
|
||||
name: 'PIPEDA Other Factors Comment',
|
||||
required: false,
|
||||
type: 'textarea',
|
||||
},
|
||||
{
|
||||
id: 'pipeda_overall_assessment_comment',
|
||||
name: 'PIPEDA Overall Assessment Comment',
|
||||
required: false,
|
||||
type: 'textarea',
|
||||
},
|
||||
{
|
||||
id: 'pipeda_probability_of_misuse_comment',
|
||||
name: 'PIPEDA Probability of Misuse Comment',
|
||||
required: false,
|
||||
type: 'textarea',
|
||||
},
|
||||
{
|
||||
id: 'pipeda_sensitivity_of_pi_comment',
|
||||
name: 'PIPEDA Sensitivity of PI Comment',
|
||||
required: false,
|
||||
type: 'textarea',
|
||||
},
|
||||
{ id: 'reporter', name: 'Reporting Individual', required: false, type: 'text' },
|
||||
{ id: 'resolution_summary', name: 'Resolution Summary', required: false, type: 'textarea' },
|
||||
{ id: 'zip', name: 'Zip', required: false, type: 'text' },
|
||||
],
|
||||
fields: resilientFields,
|
||||
type: ConnectorTypes.resilient,
|
||||
},
|
||||
{
|
||||
expected: [
|
||||
{ id: 'approval', name: 'Approval', required: false, type: 'text' },
|
||||
{ id: 'close_notes', name: 'Close notes', required: false, type: 'textarea' },
|
||||
{ id: 'contact_type', name: 'Contact type', required: false, type: 'text' },
|
||||
{ id: 'correlation_display', name: 'Correlation display', required: false, type: 'text' },
|
||||
{ id: 'correlation_id', name: 'Correlation ID', required: false, type: 'text' },
|
||||
{ id: 'description', name: 'Description', required: false, type: 'textarea' },
|
||||
{ id: 'number', name: 'Number', required: false, type: 'text' },
|
||||
{ id: 'short_description', name: 'Short description', required: false, type: 'text' },
|
||||
{ id: 'sys_created_by', name: 'Created by', required: false, type: 'text' },
|
||||
{ id: 'sys_updated_by', name: 'Updated by', required: false, type: 'text' },
|
||||
{ id: 'upon_approval', name: 'Upon approval', required: false, type: 'text' },
|
||||
{ id: 'upon_reject', name: 'Upon reject', required: false, type: 'text' },
|
||||
],
|
||||
fields: serviceNowFields,
|
||||
type: ConnectorTypes.servicenow,
|
||||
},
|
||||
];
|
||||
describe('client/configure/utils', () => {
|
||||
describe('formatFields', () => {
|
||||
formatFieldsTestData.forEach(({ expected, fields, type }) => {
|
||||
it(`normalizes ${type} fields to common type ConnectorField`, () => {
|
||||
const result = formatFields(fields, type);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
169
x-pack/plugins/case/server/client/configure/utils.ts
Normal file
169
x-pack/plugins/case/server/client/configure/utils.ts
Normal file
|
@ -0,0 +1,169 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
ConnectorField,
|
||||
ConnectorMappingsAttributes,
|
||||
ConnectorTypes,
|
||||
} from '../../../common/api/connectors';
|
||||
import {
|
||||
JiraGetFieldsResponse,
|
||||
ResilientGetFieldsResponse,
|
||||
ServiceNowGetFieldsResponse,
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
} from '../../../../actions/server/types';
|
||||
|
||||
const normalizeJiraFields = (jiraFields: JiraGetFieldsResponse): ConnectorField[] =>
|
||||
Object.keys(jiraFields).reduce<ConnectorField[]>(
|
||||
(acc, data) =>
|
||||
jiraFields[data].schema.type === 'string'
|
||||
? [
|
||||
...acc,
|
||||
{
|
||||
id: data,
|
||||
name: jiraFields[data].name,
|
||||
required: jiraFields[data].required,
|
||||
type: 'text',
|
||||
},
|
||||
]
|
||||
: acc,
|
||||
[]
|
||||
);
|
||||
|
||||
const normalizeResilientFields = (resilientFields: ResilientGetFieldsResponse): ConnectorField[] =>
|
||||
resilientFields.reduce<ConnectorField[]>(
|
||||
(acc: ConnectorField[], data) =>
|
||||
(data.input_type === 'textarea' || data.input_type === 'text') && !data.read_only
|
||||
? [
|
||||
...acc,
|
||||
{
|
||||
id: data.name,
|
||||
name: data.text,
|
||||
required: data.required === 'always',
|
||||
type: data.input_type,
|
||||
},
|
||||
]
|
||||
: acc,
|
||||
[]
|
||||
);
|
||||
const normalizeServiceNowFields = (snFields: ServiceNowGetFieldsResponse): ConnectorField[] =>
|
||||
snFields.reduce<ConnectorField[]>(
|
||||
(acc, data) => [
|
||||
...acc,
|
||||
{
|
||||
id: data.element,
|
||||
name: data.column_label,
|
||||
required: data.mandatory === 'true',
|
||||
type: parseFloat(data.max_length) > 160 ? 'textarea' : 'text',
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
export const formatFields = (theData: unknown, theType: string): ConnectorField[] => {
|
||||
switch (theType) {
|
||||
case ConnectorTypes.jira:
|
||||
return normalizeJiraFields(theData as JiraGetFieldsResponse);
|
||||
case ConnectorTypes.resilient:
|
||||
return normalizeResilientFields(theData as ResilientGetFieldsResponse);
|
||||
case ConnectorTypes.servicenow:
|
||||
return normalizeServiceNowFields(theData as ServiceNowGetFieldsResponse);
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
const findTextField = (fields: ConnectorField[]): string =>
|
||||
(
|
||||
fields.find((field: ConnectorField) => field.type === 'text' && field.required) ??
|
||||
fields.find((field: ConnectorField) => field.type === 'text')
|
||||
)?.id ?? '';
|
||||
const findTextAreaField = (fields: ConnectorField[]): string =>
|
||||
(
|
||||
fields.find((field: ConnectorField) => field.type === 'textarea' && field.required) ??
|
||||
fields.find((field: ConnectorField) => field.type === 'textarea') ??
|
||||
fields.find((field: ConnectorField) => field.type === 'text')
|
||||
)?.id ?? '';
|
||||
|
||||
const getPreferredFields = (theType: string) => {
|
||||
let title: string = '';
|
||||
let description: string = '';
|
||||
if (theType === ConnectorTypes.jira) {
|
||||
title = 'summary';
|
||||
description = 'description';
|
||||
} else if (theType === ConnectorTypes.resilient) {
|
||||
title = 'name';
|
||||
description = 'description';
|
||||
} else if (theType === ConnectorTypes.servicenow) {
|
||||
title = 'short_description';
|
||||
description = 'description';
|
||||
}
|
||||
return { title, description };
|
||||
};
|
||||
|
||||
const getRemainingFields = (fields: ConnectorField[], titleTarget: string) =>
|
||||
fields.filter((field: ConnectorField) => field.id !== titleTarget);
|
||||
|
||||
const getDynamicFields = (fields: ConnectorField[], dynamicTitle = findTextField(fields)) => {
|
||||
const remainingFields = getRemainingFields(fields, dynamicTitle);
|
||||
const dynamicDescription = findTextAreaField(remainingFields);
|
||||
return {
|
||||
description: dynamicDescription,
|
||||
title: dynamicTitle,
|
||||
};
|
||||
};
|
||||
|
||||
const getField = (fields: ConnectorField[], fieldId: string) =>
|
||||
fields.find((field: ConnectorField) => field.id === fieldId);
|
||||
|
||||
// if dynamic title is not required and preferred is, true
|
||||
const shouldTargetBePreferred = (
|
||||
fields: ConnectorField[],
|
||||
dynamic: string,
|
||||
preferred: string
|
||||
): boolean => {
|
||||
if (dynamic !== preferred) {
|
||||
const dynamicT = getField(fields, dynamic);
|
||||
const preferredT = getField(fields, preferred);
|
||||
return preferredT != null && !(dynamicT?.required && !preferredT.required);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
export const createDefaultMapping = (
|
||||
fields: ConnectorField[],
|
||||
theType: string
|
||||
): ConnectorMappingsAttributes[] => {
|
||||
const { description: dynamicDescription, title: dynamicTitle } = getDynamicFields(fields);
|
||||
const { description: preferredDescription, title: preferredTitle } = getPreferredFields(theType);
|
||||
let titleTarget = dynamicTitle;
|
||||
let descriptionTarget = dynamicDescription;
|
||||
if (preferredTitle.length > 0 && preferredDescription.length > 0) {
|
||||
if (shouldTargetBePreferred(fields, dynamicTitle, preferredTitle)) {
|
||||
const { description: dynamicDescriptionOverwrite } = getDynamicFields(fields, preferredTitle);
|
||||
titleTarget = preferredTitle;
|
||||
descriptionTarget = dynamicDescriptionOverwrite;
|
||||
}
|
||||
if (shouldTargetBePreferred(fields, descriptionTarget, preferredDescription)) {
|
||||
descriptionTarget = preferredDescription;
|
||||
}
|
||||
}
|
||||
return [
|
||||
{
|
||||
source: 'title',
|
||||
target: titleTarget,
|
||||
action_type: 'overwrite',
|
||||
},
|
||||
{
|
||||
source: 'description',
|
||||
target: descriptionTarget,
|
||||
action_type: 'overwrite',
|
||||
},
|
||||
{
|
||||
source: 'comments',
|
||||
target: 'comments',
|
||||
action_type: 'append',
|
||||
},
|
||||
];
|
||||
};
|
|
@ -8,6 +8,7 @@ import { KibanaRequest, RequestHandlerContext } from 'kibana/server';
|
|||
import { savedObjectsClientMock } from '../../../../../src/core/server/mocks';
|
||||
import { createCaseClient } from '.';
|
||||
import {
|
||||
connectorMappingsServiceMock,
|
||||
createCaseServiceMock,
|
||||
createConfigureServiceMock,
|
||||
createUserActionServiceMock,
|
||||
|
@ -24,12 +25,13 @@ jest.mock('./cases/update');
|
|||
jest.mock('./comments/add');
|
||||
jest.mock('./alerts/update_status');
|
||||
|
||||
const caseService = createCaseServiceMock();
|
||||
const caseConfigureService = createConfigureServiceMock();
|
||||
const userActionService = createUserActionServiceMock();
|
||||
const alertsService = createAlertServiceMock();
|
||||
const savedObjectsClient = savedObjectsClientMock.create();
|
||||
const caseService = createCaseServiceMock();
|
||||
const connectorMappingsService = connectorMappingsServiceMock();
|
||||
const request = {} as KibanaRequest;
|
||||
const savedObjectsClient = savedObjectsClientMock.create();
|
||||
const userActionService = createUserActionServiceMock();
|
||||
const context = {} as RequestHandlerContext;
|
||||
|
||||
const createMock = create as jest.Mock;
|
||||
|
@ -40,53 +42,58 @@ const updateAlertsStatusMock = updateAlertsStatus as jest.Mock;
|
|||
describe('createCaseClient()', () => {
|
||||
test('it creates the client correctly', async () => {
|
||||
createCaseClient({
|
||||
savedObjectsClient,
|
||||
request,
|
||||
alertsService,
|
||||
caseConfigureService,
|
||||
caseService,
|
||||
userActionService,
|
||||
alertsService,
|
||||
connectorMappingsService,
|
||||
context,
|
||||
request,
|
||||
savedObjectsClient,
|
||||
userActionService,
|
||||
});
|
||||
|
||||
expect(createMock).toHaveBeenCalledWith({
|
||||
savedObjectsClient,
|
||||
request,
|
||||
alertsService,
|
||||
caseConfigureService,
|
||||
caseService,
|
||||
userActionService,
|
||||
alertsService,
|
||||
connectorMappingsService,
|
||||
context,
|
||||
request,
|
||||
savedObjectsClient,
|
||||
userActionService,
|
||||
});
|
||||
|
||||
expect(updateMock).toHaveBeenCalledWith({
|
||||
savedObjectsClient,
|
||||
request,
|
||||
alertsService,
|
||||
caseConfigureService,
|
||||
caseService,
|
||||
userActionService,
|
||||
alertsService,
|
||||
connectorMappingsService,
|
||||
context,
|
||||
request,
|
||||
savedObjectsClient,
|
||||
userActionService,
|
||||
});
|
||||
|
||||
expect(addCommentMock).toHaveBeenCalledWith({
|
||||
savedObjectsClient,
|
||||
request,
|
||||
alertsService,
|
||||
caseConfigureService,
|
||||
caseService,
|
||||
userActionService,
|
||||
alertsService,
|
||||
connectorMappingsService,
|
||||
context,
|
||||
request,
|
||||
savedObjectsClient,
|
||||
userActionService,
|
||||
});
|
||||
|
||||
expect(updateAlertsStatusMock).toHaveBeenCalledWith({
|
||||
savedObjectsClient,
|
||||
request,
|
||||
alertsService,
|
||||
caseConfigureService,
|
||||
caseService,
|
||||
userActionService,
|
||||
alertsService,
|
||||
connectorMappingsService,
|
||||
context,
|
||||
request,
|
||||
savedObjectsClient,
|
||||
userActionService,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,55 +8,73 @@ import { CaseClientFactoryArguments, CaseClient } from './types';
|
|||
import { create } from './cases/create';
|
||||
import { update } from './cases/update';
|
||||
import { addComment } from './comments/add';
|
||||
import { getFields } from './configure/get_fields';
|
||||
import { getMappings } from './configure/get_mappings';
|
||||
import { updateAlertsStatus } from './alerts/update_status';
|
||||
|
||||
export { CaseClient } from './types';
|
||||
|
||||
export const createCaseClient = ({
|
||||
savedObjectsClient,
|
||||
request,
|
||||
caseConfigureService,
|
||||
caseService,
|
||||
connectorMappingsService,
|
||||
request,
|
||||
savedObjectsClient,
|
||||
userActionService,
|
||||
alertsService,
|
||||
context,
|
||||
}: CaseClientFactoryArguments): CaseClient => {
|
||||
return {
|
||||
create: create({
|
||||
savedObjectsClient,
|
||||
request,
|
||||
alertsService,
|
||||
caseConfigureService,
|
||||
caseService,
|
||||
userActionService,
|
||||
alertsService,
|
||||
connectorMappingsService,
|
||||
context,
|
||||
request,
|
||||
savedObjectsClient,
|
||||
userActionService,
|
||||
}),
|
||||
update: update({
|
||||
savedObjectsClient,
|
||||
request,
|
||||
alertsService,
|
||||
caseConfigureService,
|
||||
caseService,
|
||||
userActionService,
|
||||
alertsService,
|
||||
connectorMappingsService,
|
||||
context,
|
||||
request,
|
||||
savedObjectsClient,
|
||||
userActionService,
|
||||
}),
|
||||
addComment: addComment({
|
||||
savedObjectsClient,
|
||||
request,
|
||||
alertsService,
|
||||
caseConfigureService,
|
||||
caseService,
|
||||
userActionService,
|
||||
alertsService,
|
||||
connectorMappingsService,
|
||||
context,
|
||||
request,
|
||||
savedObjectsClient,
|
||||
userActionService,
|
||||
}),
|
||||
getFields: getFields(),
|
||||
getMappings: getMappings({
|
||||
alertsService,
|
||||
caseConfigureService,
|
||||
caseService,
|
||||
connectorMappingsService,
|
||||
context,
|
||||
request,
|
||||
savedObjectsClient,
|
||||
userActionService,
|
||||
}),
|
||||
updateAlertsStatus: updateAlertsStatus({
|
||||
savedObjectsClient,
|
||||
request,
|
||||
alertsService,
|
||||
caseConfigureService,
|
||||
caseService,
|
||||
userActionService,
|
||||
alertsService,
|
||||
connectorMappingsService,
|
||||
context,
|
||||
request,
|
||||
savedObjectsClient,
|
||||
userActionService,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -8,10 +8,11 @@ import { KibanaRequest, RequestHandlerContext } from 'kibana/server';
|
|||
import { loggingSystemMock, elasticsearchServiceMock } from '../../../../../src/core/server/mocks';
|
||||
import { actionsClientMock } from '../../../actions/server/mocks';
|
||||
import {
|
||||
CaseService,
|
||||
CaseConfigureService,
|
||||
CaseUserActionServiceSetup,
|
||||
AlertService,
|
||||
CaseConfigureService,
|
||||
CaseService,
|
||||
CaseUserActionServiceSetup,
|
||||
ConnectorMappingsService,
|
||||
} from '../services';
|
||||
import { CaseClient } from './types';
|
||||
import { authenticationMock } from '../routes/api/__fixtures__';
|
||||
|
@ -20,9 +21,11 @@ import { getActions } from '../routes/api/__mocks__/request_responses';
|
|||
|
||||
export type CaseClientMock = jest.Mocked<CaseClient>;
|
||||
export const createCaseClientMock = (): CaseClientMock => ({
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
addComment: jest.fn(),
|
||||
create: jest.fn(),
|
||||
getFields: jest.fn(),
|
||||
getMappings: jest.fn(),
|
||||
update: jest.fn(),
|
||||
updateAlertsStatus: jest.fn(),
|
||||
});
|
||||
|
||||
|
@ -41,11 +44,14 @@ export const createCaseClientWithMockSavedObjectsClient = async (
|
|||
|
||||
const caseServicePlugin = new CaseService(log);
|
||||
const caseConfigureServicePlugin = new CaseConfigureService(log);
|
||||
const connectorMappingsServicePlugin = new ConnectorMappingsService(log);
|
||||
|
||||
const caseService = await caseServicePlugin.setup({
|
||||
authentication: badAuth ? authenticationMock.createInvalid() : authenticationMock.create(),
|
||||
});
|
||||
const caseConfigureService = await caseConfigureServicePlugin.setup();
|
||||
|
||||
const connectorMappingsService = await connectorMappingsServicePlugin.setup();
|
||||
const userActionService = {
|
||||
postUserActions: jest.fn(),
|
||||
getUserActions: jest.fn(),
|
||||
|
@ -75,11 +81,11 @@ export const createCaseClientWithMockSavedObjectsClient = async (
|
|||
request,
|
||||
caseService,
|
||||
caseConfigureService,
|
||||
connectorMappingsService,
|
||||
userActionService,
|
||||
alertsService,
|
||||
context,
|
||||
});
|
||||
|
||||
return {
|
||||
client: caseClient,
|
||||
services: { userActionService },
|
||||
|
|
|
@ -5,13 +5,16 @@
|
|||
*/
|
||||
|
||||
import { KibanaRequest, SavedObjectsClientContract, RequestHandlerContext } from 'kibana/server';
|
||||
import { ActionsClient } from '../../../actions/server';
|
||||
import {
|
||||
CasePostRequest,
|
||||
CasesPatchRequest,
|
||||
CommentRequest,
|
||||
CaseResponse,
|
||||
CasesPatchRequest,
|
||||
CasesResponse,
|
||||
CaseStatuses,
|
||||
CommentRequest,
|
||||
ConnectorMappingsAttributes,
|
||||
GetFieldsResponse,
|
||||
} from '../../common/api';
|
||||
import {
|
||||
CaseConfigureServiceSetup,
|
||||
|
@ -19,7 +22,7 @@ import {
|
|||
CaseUserActionServiceSetup,
|
||||
AlertServiceContract,
|
||||
} from '../services';
|
||||
|
||||
import { ConnectorMappingsServiceSetup } from '../services/connector_mappings';
|
||||
export interface CaseClientCreate {
|
||||
theCase: CasePostRequest;
|
||||
}
|
||||
|
@ -43,18 +46,33 @@ export interface CaseClientUpdateAlertsStatus {
|
|||
type PartialExceptFor<T, K extends keyof T> = Partial<T> & Pick<T, K>;
|
||||
|
||||
export interface CaseClientFactoryArguments {
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
request: KibanaRequest;
|
||||
caseConfigureService: CaseConfigureServiceSetup;
|
||||
caseService: CaseServiceSetup;
|
||||
connectorMappingsService: ConnectorMappingsServiceSetup;
|
||||
request: KibanaRequest;
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
userActionService: CaseUserActionServiceSetup;
|
||||
alertsService: AlertServiceContract;
|
||||
context?: PartialExceptFor<RequestHandlerContext, 'core'>;
|
||||
}
|
||||
|
||||
export interface ConfigureFields {
|
||||
actionsClient: ActionsClient;
|
||||
connectorId: string;
|
||||
connectorType: string;
|
||||
}
|
||||
export interface CaseClient {
|
||||
create: (args: CaseClientCreate) => Promise<CaseResponse>;
|
||||
update: (args: CaseClientUpdate) => Promise<CasesResponse>;
|
||||
addComment: (args: CaseClientAddComment) => Promise<CaseResponse>;
|
||||
create: (args: CaseClientCreate) => Promise<CaseResponse>;
|
||||
getFields: (args: ConfigureFields) => Promise<GetFieldsResponse>;
|
||||
getMappings: (args: MappingsClient) => Promise<ConnectorMappingsAttributes[]>;
|
||||
update: (args: CaseClientUpdate) => Promise<CasesResponse>;
|
||||
updateAlertsStatus: (args: CaseClientUpdateAlertsStatus) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface MappingsClient {
|
||||
actionsClient: ActionsClient;
|
||||
caseClient: CaseClient;
|
||||
connectorId: string;
|
||||
connectorType: string;
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import { actionsMock } from '../../../../actions/server/mocks';
|
|||
import { validateParams } from '../../../../actions/server/lib';
|
||||
import { ConnectorTypes, CommentType, CaseStatuses } from '../../../common/api';
|
||||
import {
|
||||
connectorMappingsServiceMock,
|
||||
createCaseServiceMock,
|
||||
createConfigureServiceMock,
|
||||
createUserActionServiceMock,
|
||||
|
@ -35,12 +36,14 @@ describe('case connector', () => {
|
|||
const logger = loggingSystemMock.create().get() as jest.Mocked<Logger>;
|
||||
const caseService = createCaseServiceMock();
|
||||
const caseConfigureService = createConfigureServiceMock();
|
||||
const connectorMappingsService = connectorMappingsServiceMock();
|
||||
const userActionService = createUserActionServiceMock();
|
||||
const alertsService = createAlertServiceMock();
|
||||
caseActionType = getActionType({
|
||||
logger,
|
||||
caseService,
|
||||
caseConfigureService,
|
||||
connectorMappingsService,
|
||||
userActionService,
|
||||
alertsService,
|
||||
});
|
||||
|
|
|
@ -29,6 +29,7 @@ export function getActionType({
|
|||
logger,
|
||||
caseService,
|
||||
caseConfigureService,
|
||||
connectorMappingsService,
|
||||
userActionService,
|
||||
alertsService,
|
||||
}: GetActionTypeParams): CaseActionType {
|
||||
|
@ -41,11 +42,12 @@ export function getActionType({
|
|||
params: CaseExecutorParamsSchema,
|
||||
},
|
||||
executor: curry(executor)({
|
||||
logger,
|
||||
caseService,
|
||||
caseConfigureService,
|
||||
userActionService,
|
||||
alertsService,
|
||||
caseConfigureService,
|
||||
caseService,
|
||||
connectorMappingsService,
|
||||
logger,
|
||||
userActionService,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
@ -53,11 +55,12 @@ export function getActionType({
|
|||
// action executor
|
||||
async function executor(
|
||||
{
|
||||
logger,
|
||||
caseService,
|
||||
caseConfigureService,
|
||||
userActionService,
|
||||
alertsService,
|
||||
caseConfigureService,
|
||||
caseService,
|
||||
connectorMappingsService,
|
||||
logger,
|
||||
userActionService,
|
||||
}: GetActionTypeParams,
|
||||
execOptions: CaseActionTypeExecutorOptions
|
||||
): Promise<ActionTypeExecutorResult<CaseExecutorResponse | {}>> {
|
||||
|
@ -71,6 +74,7 @@ async function executor(
|
|||
request: {} as KibanaRequest,
|
||||
caseService,
|
||||
caseConfigureService,
|
||||
connectorMappingsService,
|
||||
userActionService,
|
||||
alertsService,
|
||||
// TODO: When case connector is enabled we should figure out how to pass the context.
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
CaseServiceSetup,
|
||||
CaseConfigureServiceSetup,
|
||||
CaseUserActionServiceSetup,
|
||||
ConnectorMappingsServiceSetup,
|
||||
AlertServiceContract,
|
||||
} from '../services';
|
||||
|
||||
|
@ -26,6 +27,7 @@ export interface GetActionTypeParams {
|
|||
logger: Logger;
|
||||
caseService: CaseServiceSetup;
|
||||
caseConfigureService: CaseConfigureServiceSetup;
|
||||
connectorMappingsService: ConnectorMappingsServiceSetup;
|
||||
userActionService: CaseUserActionServiceSetup;
|
||||
alertsService: AlertServiceContract;
|
||||
}
|
||||
|
@ -46,6 +48,7 @@ export const registerConnectors = ({
|
|||
logger,
|
||||
caseService,
|
||||
caseConfigureService,
|
||||
connectorMappingsService,
|
||||
userActionService,
|
||||
alertsService,
|
||||
}: RegisterConnectorsArgs) => {
|
||||
|
@ -54,6 +57,7 @@ export const registerConnectors = ({
|
|||
logger,
|
||||
caseService,
|
||||
caseConfigureService,
|
||||
connectorMappingsService,
|
||||
userActionService,
|
||||
alertsService,
|
||||
})
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { PluginInitializerContext } from '../../../../src/core/server';
|
||||
import { PluginInitializerContext } from 'kibana/server';
|
||||
import { ConfigSchema } from './config';
|
||||
import { CasePlugin } from './plugin';
|
||||
|
||||
|
|
|
@ -22,9 +22,10 @@ import { APP_ID } from '../common/constants';
|
|||
import { ConfigType } from './config';
|
||||
import { initCaseApi } from './routes/api';
|
||||
import {
|
||||
caseSavedObjectType,
|
||||
caseConfigureSavedObjectType,
|
||||
caseCommentSavedObjectType,
|
||||
caseConfigureSavedObjectType,
|
||||
caseConnectorMappingsSavedObjectType,
|
||||
caseSavedObjectType,
|
||||
caseUserActionSavedObjectType,
|
||||
} from './saved_object_types';
|
||||
import {
|
||||
|
@ -34,6 +35,8 @@ import {
|
|||
CaseServiceSetup,
|
||||
CaseUserActionService,
|
||||
CaseUserActionServiceSetup,
|
||||
ConnectorMappingsService,
|
||||
ConnectorMappingsServiceSetup,
|
||||
AlertService,
|
||||
AlertServiceContract,
|
||||
} from './services';
|
||||
|
@ -51,8 +54,9 @@ export interface PluginsSetup {
|
|||
|
||||
export class CasePlugin {
|
||||
private readonly log: Logger;
|
||||
private caseService?: CaseServiceSetup;
|
||||
private caseConfigureService?: CaseConfigureServiceSetup;
|
||||
private caseService?: CaseServiceSetup;
|
||||
private connectorMappingsService?: ConnectorMappingsServiceSetup;
|
||||
private userActionService?: CaseUserActionServiceSetup;
|
||||
private alertsService?: AlertService;
|
||||
|
||||
|
@ -67,9 +71,10 @@ export class CasePlugin {
|
|||
return;
|
||||
}
|
||||
|
||||
core.savedObjects.registerType(caseSavedObjectType);
|
||||
core.savedObjects.registerType(caseCommentSavedObjectType);
|
||||
core.savedObjects.registerType(caseConfigureSavedObjectType);
|
||||
core.savedObjects.registerType(caseConnectorMappingsSavedObjectType);
|
||||
core.savedObjects.registerType(caseSavedObjectType);
|
||||
core.savedObjects.registerType(caseUserActionSavedObjectType);
|
||||
|
||||
this.log.debug(
|
||||
|
@ -82,6 +87,7 @@ export class CasePlugin {
|
|||
authentication: plugins.security != null ? plugins.security.authc : null,
|
||||
});
|
||||
this.caseConfigureService = await new CaseConfigureService(this.log).setup();
|
||||
this.connectorMappingsService = await new ConnectorMappingsService(this.log).setup();
|
||||
this.userActionService = await new CaseUserActionService(this.log).setup();
|
||||
this.alertsService = new AlertService();
|
||||
|
||||
|
@ -91,6 +97,7 @@ export class CasePlugin {
|
|||
core,
|
||||
caseService: this.caseService,
|
||||
caseConfigureService: this.caseConfigureService,
|
||||
connectorMappingsService: this.connectorMappingsService,
|
||||
userActionService: this.userActionService,
|
||||
alertsService: this.alertsService,
|
||||
})
|
||||
|
@ -100,6 +107,7 @@ export class CasePlugin {
|
|||
initCaseApi({
|
||||
caseService: this.caseService,
|
||||
caseConfigureService: this.caseConfigureService,
|
||||
connectorMappingsService: this.connectorMappingsService,
|
||||
userActionService: this.userActionService,
|
||||
router,
|
||||
});
|
||||
|
@ -109,6 +117,7 @@ export class CasePlugin {
|
|||
logger: this.log,
|
||||
caseService: this.caseService,
|
||||
caseConfigureService: this.caseConfigureService,
|
||||
connectorMappingsService: this.connectorMappingsService,
|
||||
userActionService: this.userActionService,
|
||||
alertsService: this.alertsService,
|
||||
});
|
||||
|
@ -127,6 +136,7 @@ export class CasePlugin {
|
|||
request,
|
||||
caseService: this.caseService!,
|
||||
caseConfigureService: this.caseConfigureService!,
|
||||
connectorMappingsService: this.connectorMappingsService!,
|
||||
userActionService: this.userActionService!,
|
||||
alertsService: this.alertsService!,
|
||||
context,
|
||||
|
@ -146,12 +156,14 @@ export class CasePlugin {
|
|||
core,
|
||||
caseService,
|
||||
caseConfigureService,
|
||||
connectorMappingsService,
|
||||
userActionService,
|
||||
alertsService,
|
||||
}: {
|
||||
core: CoreSetup;
|
||||
caseService: CaseServiceSetup;
|
||||
caseConfigureService: CaseConfigureServiceSetup;
|
||||
connectorMappingsService: ConnectorMappingsServiceSetup;
|
||||
userActionService: CaseUserActionServiceSetup;
|
||||
alertsService: AlertServiceContract;
|
||||
}): IContextProvider<RequestHandler<unknown, unknown, unknown>, typeof APP_ID> => {
|
||||
|
@ -163,6 +175,7 @@ export class CasePlugin {
|
|||
savedObjectsClient: savedObjects.getScopedClient(request),
|
||||
caseService,
|
||||
caseConfigureService,
|
||||
connectorMappingsService,
|
||||
userActionService,
|
||||
alertsService,
|
||||
request,
|
||||
|
|
|
@ -15,16 +15,19 @@ import {
|
|||
CASE_COMMENT_SAVED_OBJECT,
|
||||
CASE_SAVED_OBJECT,
|
||||
CASE_CONFIGURE_SAVED_OBJECT,
|
||||
CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT,
|
||||
} from '../../../saved_object_types';
|
||||
|
||||
export const createMockSavedObjectsRepository = ({
|
||||
caseSavedObject = [],
|
||||
caseCommentSavedObject = [],
|
||||
caseConfigureSavedObject = [],
|
||||
caseMappingsSavedObject = [],
|
||||
}: {
|
||||
caseSavedObject?: any[];
|
||||
caseCommentSavedObject?: any[];
|
||||
caseConfigureSavedObject?: any[];
|
||||
caseMappingsSavedObject?: any[];
|
||||
}) => {
|
||||
const mockSavedObjectsClientContract = ({
|
||||
bulkGet: jest.fn((objects: SavedObjectsBulkGetObject[]) => {
|
||||
|
@ -103,6 +106,14 @@ export const createMockSavedObjectsRepository = ({
|
|||
) {
|
||||
throw SavedObjectsErrorHelpers.createGenericNotFoundError('Error thrown for testing');
|
||||
}
|
||||
if (findArgs.type === CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT && caseMappingsSavedObject[0]) {
|
||||
return {
|
||||
page: 1,
|
||||
per_page: 5,
|
||||
total: 1,
|
||||
saved_objects: caseMappingsSavedObject,
|
||||
};
|
||||
}
|
||||
|
||||
if (findArgs.type === CASE_CONFIGURE_SAVED_OBJECT) {
|
||||
return {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { loggingSystemMock, httpServiceMock } from '../../../../../../../src/core/server/mocks';
|
||||
import { CaseService, CaseConfigureService } from '../../../services';
|
||||
import { CaseService, CaseConfigureService, ConnectorMappingsService } from '../../../services';
|
||||
import { authenticationMock } from '../__fixtures__';
|
||||
import { RouteDeps } from '../types';
|
||||
|
||||
|
@ -21,15 +21,18 @@ export const createRoute = async (
|
|||
|
||||
const caseServicePlugin = new CaseService(log);
|
||||
const caseConfigureServicePlugin = new CaseConfigureService(log);
|
||||
const connectorMappingsServicePlugin = new ConnectorMappingsService(log);
|
||||
|
||||
const caseService = await caseServicePlugin.setup({
|
||||
authentication: badAuth ? authenticationMock.createInvalid() : authenticationMock.create(),
|
||||
});
|
||||
const caseConfigureService = await caseConfigureServicePlugin.setup();
|
||||
const connectorMappingsService = await connectorMappingsServicePlugin.setup();
|
||||
|
||||
api({
|
||||
caseConfigureService,
|
||||
caseService,
|
||||
connectorMappingsService,
|
||||
router,
|
||||
userActionService: {
|
||||
postUserActions: jest.fn(),
|
||||
|
|
|
@ -6,13 +6,16 @@
|
|||
|
||||
import { SavedObject, SavedObjectsFindResponse } from 'kibana/server';
|
||||
import {
|
||||
ESCasesConfigureAttributes,
|
||||
CommentAttributes,
|
||||
ESCaseAttributes,
|
||||
ConnectorTypes,
|
||||
CommentType,
|
||||
CaseStatuses,
|
||||
CommentAttributes,
|
||||
CommentType,
|
||||
ConnectorMappings,
|
||||
ConnectorTypes,
|
||||
ESCaseAttributes,
|
||||
ESCasesConfigureAttributes,
|
||||
} from '../../../../common/api';
|
||||
import { mappings } from '../cases/configure/mock';
|
||||
import { CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT } from '../../../saved_object_types';
|
||||
|
||||
export const mockCases: Array<SavedObject<ESCaseAttributes>> = [
|
||||
{
|
||||
|
@ -386,3 +389,23 @@ export const mockCaseConfigureFind: Array<SavedObjectsFindResponse<ESCasesConfig
|
|||
saved_objects: [{ ...mockCaseConfigure[0], score: 0 }],
|
||||
},
|
||||
];
|
||||
|
||||
export const mockCaseMappings: Array<SavedObject<ConnectorMappings>> = [
|
||||
{
|
||||
type: CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT,
|
||||
id: 'mock-mappings-1',
|
||||
attributes: {
|
||||
mappings,
|
||||
},
|
||||
references: [],
|
||||
},
|
||||
];
|
||||
|
||||
export const mockCaseMappingsFind: Array<SavedObjectsFindResponse<ConnectorMappings>> = [
|
||||
{
|
||||
page: 1,
|
||||
per_page: 5,
|
||||
total: mockCaseConfigure.length,
|
||||
saved_objects: [{ ...mockCaseMappings[0], score: 0 }],
|
||||
},
|
||||
];
|
||||
|
|
|
@ -8,7 +8,12 @@ import { RequestHandlerContext, KibanaRequest } from 'src/core/server';
|
|||
import { loggingSystemMock, elasticsearchServiceMock } from 'src/core/server/mocks';
|
||||
import { actionsClientMock } from '../../../../../actions/server/mocks';
|
||||
import { createCaseClient } from '../../../client';
|
||||
import { CaseService, CaseConfigureService, AlertService } from '../../../services';
|
||||
import {
|
||||
AlertService,
|
||||
CaseService,
|
||||
CaseConfigureService,
|
||||
ConnectorMappingsService,
|
||||
} from '../../../services';
|
||||
import { getActions } from '../__mocks__/request_responses';
|
||||
import { authenticationMock } from '../__fixtures__';
|
||||
|
||||
|
@ -20,6 +25,7 @@ export const createRouteContext = async (client: any, badAuth = false) => {
|
|||
|
||||
const caseServicePlugin = new CaseService(log);
|
||||
const caseConfigureServicePlugin = new CaseConfigureService(log);
|
||||
const connectorMappingsServicePlugin = new ConnectorMappingsService(log);
|
||||
|
||||
const caseService = await caseServicePlugin.setup({
|
||||
authentication: badAuth ? authenticationMock.createInvalid() : authenticationMock.create(),
|
||||
|
@ -45,11 +51,13 @@ export const createRouteContext = async (client: any, badAuth = false) => {
|
|||
},
|
||||
} as unknown) as RequestHandlerContext;
|
||||
|
||||
const connectorMappingsService = await connectorMappingsServicePlugin.setup();
|
||||
const caseClient = createCaseClient({
|
||||
savedObjectsClient: client,
|
||||
request: {} as KibanaRequest,
|
||||
caseService,
|
||||
caseConfigureService,
|
||||
connectorMappingsService,
|
||||
userActionService: {
|
||||
postUserActions: jest.fn(),
|
||||
getUserActions: jest.fn(),
|
||||
|
|
|
@ -40,27 +40,7 @@ export const getActions = (): FindActionResult[] => [
|
|||
actionTypeId: '.servicenow',
|
||||
name: 'ServiceNow',
|
||||
config: {
|
||||
incidentConfiguration: {
|
||||
mapping: [
|
||||
{
|
||||
source: 'title',
|
||||
target: 'short_description',
|
||||
actionType: 'overwrite',
|
||||
},
|
||||
{
|
||||
source: 'description',
|
||||
target: 'description',
|
||||
actionType: 'overwrite',
|
||||
},
|
||||
{
|
||||
source: 'comments',
|
||||
target: 'comments',
|
||||
actionType: 'append',
|
||||
},
|
||||
],
|
||||
},
|
||||
apiUrl: 'https://dev102283.service-now.com',
|
||||
isCaseOwned: true,
|
||||
},
|
||||
isPreconfigured: false,
|
||||
referencedByCount: 0,
|
||||
|
@ -70,25 +50,6 @@ export const getActions = (): FindActionResult[] => [
|
|||
actionTypeId: '.jira',
|
||||
name: 'Connector without isCaseOwned',
|
||||
config: {
|
||||
incidentConfiguration: {
|
||||
mapping: [
|
||||
{
|
||||
source: 'title',
|
||||
target: 'short_description',
|
||||
actionType: 'overwrite',
|
||||
},
|
||||
{
|
||||
source: 'description',
|
||||
target: 'description',
|
||||
actionType: 'overwrite',
|
||||
},
|
||||
{
|
||||
source: 'comments',
|
||||
target: 'comments',
|
||||
actionType: 'append',
|
||||
},
|
||||
],
|
||||
},
|
||||
apiUrl: 'https://elastic.jira.com',
|
||||
},
|
||||
isPreconfigured: false,
|
||||
|
|
|
@ -11,11 +11,13 @@ import {
|
|||
createMockSavedObjectsRepository,
|
||||
createRoute,
|
||||
createRouteContext,
|
||||
mockCaseConfigure,
|
||||
mockCaseMappings,
|
||||
} from '../../__fixtures__';
|
||||
|
||||
import { mockCaseConfigure } from '../../__fixtures__/mock_saved_objects';
|
||||
import { initGetCaseConfigure } from './get_configure';
|
||||
import { CASE_CONFIGURE_URL } from '../../../../../common/constants';
|
||||
import { mappings } from './mock';
|
||||
|
||||
describe('GET configuration', () => {
|
||||
let routeHandler: RequestHandler<any, any, any>;
|
||||
|
@ -32,6 +34,7 @@ describe('GET configuration', () => {
|
|||
const context = await createRouteContext(
|
||||
createMockSavedObjectsRepository({
|
||||
caseConfigureSavedObject: mockCaseConfigure,
|
||||
caseMappingsSavedObject: mockCaseMappings,
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -39,6 +42,7 @@ describe('GET configuration', () => {
|
|||
expect(res.status).toEqual(200);
|
||||
expect(res.payload).toEqual({
|
||||
...mockCaseConfigure[0].attributes,
|
||||
mappings,
|
||||
version: mockCaseConfigure[0].version,
|
||||
});
|
||||
});
|
||||
|
@ -52,6 +56,7 @@ describe('GET configuration', () => {
|
|||
const context = await createRouteContext(
|
||||
createMockSavedObjectsRepository({
|
||||
caseConfigureSavedObject: [{ ...mockCaseConfigure[0], version: undefined }],
|
||||
caseMappingsSavedObject: mockCaseMappings,
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -71,6 +76,7 @@ describe('GET configuration', () => {
|
|||
email: 'testemail@elastic.co',
|
||||
username: 'elastic',
|
||||
},
|
||||
mappings,
|
||||
updated_at: '2020-04-09T09:43:51.778Z',
|
||||
updated_by: {
|
||||
full_name: 'elastic',
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { CaseConfigureResponseRt } from '../../../../../common/api';
|
||||
import Boom from '@hapi/boom';
|
||||
import { CaseConfigureResponseRt, ConnectorMappingsAttributes } from '../../../../../common/api';
|
||||
import { RouteDeps } from '../../types';
|
||||
import { wrapError } from '../../utils';
|
||||
import { CASE_CONFIGURE_URL } from '../../../../../common/constants';
|
||||
|
@ -24,6 +25,23 @@ export function initGetCaseConfigure({ caseConfigureService, router }: RouteDeps
|
|||
|
||||
const { connector, ...caseConfigureWithoutConnector } = myCaseConfigure.saved_objects[0]
|
||||
?.attributes ?? { connector: null };
|
||||
let mappings: ConnectorMappingsAttributes[] = [];
|
||||
if (connector != null) {
|
||||
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');
|
||||
}
|
||||
mappings = await caseClient.getMappings({
|
||||
actionsClient,
|
||||
caseClient,
|
||||
connectorId: connector.id,
|
||||
connectorType: connector.type,
|
||||
});
|
||||
}
|
||||
|
||||
return response.ok({
|
||||
body:
|
||||
|
@ -31,6 +49,7 @@ export function initGetCaseConfigure({ caseConfigureService, router }: RouteDeps
|
|||
? CaseConfigureResponseRt.encode({
|
||||
...caseConfigureWithoutConnector,
|
||||
connector: transformESConnectorToCaseConnector(connector),
|
||||
mappings,
|
||||
version: myCaseConfigure.saved_objects[0].version ?? '',
|
||||
})
|
||||
: {},
|
||||
|
|
|
@ -11,11 +11,13 @@ import {
|
|||
createMockSavedObjectsRepository,
|
||||
createRoute,
|
||||
createRouteContext,
|
||||
mockCaseConfigure,
|
||||
mockCaseMappings,
|
||||
} from '../../__fixtures__';
|
||||
|
||||
import { mockCaseConfigure } from '../../__fixtures__/mock_saved_objects';
|
||||
import { initCaseConfigureGetActionConnector } from './get_connectors';
|
||||
import { CASE_CONFIGURE_CONNECTORS_URL } from '../../../../../common/constants';
|
||||
import { getActions } from '../../__mocks__/request_responses';
|
||||
|
||||
describe('GET connectors', () => {
|
||||
let routeHandler: RequestHandler<any, any, any>;
|
||||
|
@ -32,72 +34,16 @@ describe('GET connectors', () => {
|
|||
const context = await createRouteContext(
|
||||
createMockSavedObjectsRepository({
|
||||
caseConfigureSavedObject: mockCaseConfigure,
|
||||
caseMappingsSavedObject: mockCaseMappings,
|
||||
})
|
||||
);
|
||||
|
||||
const res = await routeHandler(context, req, kibanaResponseFactory);
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.payload).toEqual([
|
||||
{
|
||||
id: '123',
|
||||
actionTypeId: '.servicenow',
|
||||
name: 'ServiceNow',
|
||||
config: {
|
||||
incidentConfiguration: {
|
||||
mapping: [
|
||||
{
|
||||
source: 'title',
|
||||
target: 'short_description',
|
||||
actionType: 'overwrite',
|
||||
},
|
||||
{
|
||||
source: 'description',
|
||||
target: 'description',
|
||||
actionType: 'overwrite',
|
||||
},
|
||||
{
|
||||
source: 'comments',
|
||||
target: 'comments',
|
||||
actionType: 'append',
|
||||
},
|
||||
],
|
||||
},
|
||||
apiUrl: 'https://dev102283.service-now.com',
|
||||
isCaseOwned: true,
|
||||
},
|
||||
isPreconfigured: false,
|
||||
referencedByCount: 0,
|
||||
},
|
||||
{
|
||||
id: '456',
|
||||
actionTypeId: '.jira',
|
||||
name: 'Connector without isCaseOwned',
|
||||
config: {
|
||||
incidentConfiguration: {
|
||||
mapping: [
|
||||
{
|
||||
source: 'title',
|
||||
target: 'short_description',
|
||||
actionType: 'overwrite',
|
||||
},
|
||||
{
|
||||
source: 'description',
|
||||
target: 'description',
|
||||
actionType: 'overwrite',
|
||||
},
|
||||
{
|
||||
source: 'comments',
|
||||
target: 'comments',
|
||||
actionType: 'append',
|
||||
},
|
||||
],
|
||||
},
|
||||
apiUrl: 'https://elastic.jira.com',
|
||||
},
|
||||
isPreconfigured: false,
|
||||
referencedByCount: 0,
|
||||
},
|
||||
]);
|
||||
|
||||
const expected = getActions();
|
||||
expected.shift();
|
||||
expect(res.payload).toEqual(expected);
|
||||
});
|
||||
|
||||
it('it throws an error when actions client is null', async () => {
|
||||
|
@ -109,6 +55,7 @@ describe('GET connectors', () => {
|
|||
const context = await createRouteContext(
|
||||
createMockSavedObjectsRepository({
|
||||
caseConfigureSavedObject: mockCaseConfigure,
|
||||
caseMappingsSavedObject: mockCaseMappings,
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
@ -17,39 +17,16 @@ import {
|
|||
RESILIENT_ACTION_TYPE_ID,
|
||||
} from '../../../../../common/constants';
|
||||
|
||||
/**
|
||||
* We need to take into account connectors that have been created within cases and
|
||||
* they do not have the isCaseOwned field. Checking for the existence of
|
||||
* the mapping attribute ensures that the connector is indeed a case connector.
|
||||
* Cases connector should always have a mapping.
|
||||
*/
|
||||
|
||||
interface CaseAction extends FindActionResult {
|
||||
config?: {
|
||||
isCaseOwned?: boolean;
|
||||
incidentConfiguration?: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
const isCaseOwned = (action: CaseAction): boolean => {
|
||||
if (
|
||||
[SERVICENOW_ACTION_TYPE_ID, JIRA_ACTION_TYPE_ID, RESILIENT_ACTION_TYPE_ID].includes(
|
||||
action.actionTypeId
|
||||
)
|
||||
) {
|
||||
if (action.config?.isCaseOwned === true || action.config?.incidentConfiguration?.mapping) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
const isConnectorSupported = (action: FindActionResult): boolean =>
|
||||
[SERVICENOW_ACTION_TYPE_ID, JIRA_ACTION_TYPE_ID, RESILIENT_ACTION_TYPE_ID].includes(
|
||||
action.actionTypeId
|
||||
);
|
||||
|
||||
/*
|
||||
* Be aware that this api will only return 20 connectors
|
||||
*/
|
||||
|
||||
export function initCaseConfigureGetActionConnector({ caseService, router }: RouteDeps) {
|
||||
export function initCaseConfigureGetActionConnector({ router }: RouteDeps) {
|
||||
router.get(
|
||||
{
|
||||
path: `${CASE_CONFIGURE_CONNECTORS_URL}/_find`,
|
||||
|
@ -63,7 +40,7 @@ export function initCaseConfigureGetActionConnector({ caseService, router }: Rou
|
|||
throw Boom.notFound('Action client have not been found');
|
||||
}
|
||||
|
||||
const results = (await actionsClient.getAll()).filter(isCaseOwned);
|
||||
const results = (await actionsClient.getAll()).filter(isConnectorSupported);
|
||||
return response.ok({ body: results });
|
||||
} catch (error) {
|
||||
return response.customError(wrapError(error));
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import Boom from '@hapi/boom';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { fold } from 'fp-ts/lib/Either';
|
||||
import { identity } from 'fp-ts/lib/function';
|
||||
import { RouteDeps } from '../../types';
|
||||
import { escapeHatch, wrapError } from '../../utils';
|
||||
|
||||
import { CASE_CONFIGURE_CONNECTOR_DETAILS_URL } from '../../../../../common/constants';
|
||||
import {
|
||||
ConnectorRequestParamsRt,
|
||||
GetFieldsRequestQueryRt,
|
||||
throwErrors,
|
||||
} from '../../../../../common/api';
|
||||
|
||||
export function initCaseConfigureGetFields({ router }: RouteDeps) {
|
||||
router.get(
|
||||
{
|
||||
path: CASE_CONFIGURE_CONNECTOR_DETAILS_URL,
|
||||
validate: {
|
||||
params: escapeHatch,
|
||||
query: escapeHatch,
|
||||
},
|
||||
},
|
||||
async (context, request, response) => {
|
||||
try {
|
||||
if (!context.case) {
|
||||
throw Boom.badRequest('RouteHandlerContext is not registered for cases');
|
||||
}
|
||||
const query = pipe(
|
||||
GetFieldsRequestQueryRt.decode(request.query),
|
||||
fold(throwErrors(Boom.badRequest), identity)
|
||||
);
|
||||
const params = pipe(
|
||||
ConnectorRequestParamsRt.decode(request.params),
|
||||
fold(throwErrors(Boom.badRequest), identity)
|
||||
);
|
||||
|
||||
const caseClient = context.case.getCaseClient();
|
||||
|
||||
const connectorType = query.connector_type;
|
||||
if (connectorType == null) {
|
||||
throw Boom.illegal('no connectorType value provided');
|
||||
}
|
||||
|
||||
const actionsClient = await context.actions?.getActionsClient();
|
||||
if (actionsClient == null) {
|
||||
throw Boom.notFound('Action client have not been found');
|
||||
}
|
||||
|
||||
const res = await caseClient.getFields({
|
||||
actionsClient,
|
||||
connectorId: params.connector_id,
|
||||
connectorType,
|
||||
});
|
||||
|
||||
return response.ok({
|
||||
body: res.fields,
|
||||
});
|
||||
} catch (error) {
|
||||
return response.customError(wrapError(error));
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import {
|
||||
ServiceConnectorCaseParams,
|
||||
ServiceConnectorCommentParams,
|
||||
ConnectorMappingsAttributes,
|
||||
} 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'];
|
||||
export const params = {
|
||||
comments: [comment],
|
||||
description: 'a description',
|
||||
impact: '3',
|
||||
savedObjectId: '1231231231232',
|
||||
severity: '1',
|
||||
title: 'a title',
|
||||
urgency: '2',
|
||||
...entity,
|
||||
} as ServiceConnectorCaseParams;
|
||||
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',
|
||||
},
|
||||
];
|
|
@ -11,6 +11,7 @@ import {
|
|||
createMockSavedObjectsRepository,
|
||||
createRoute,
|
||||
createRouteContext,
|
||||
mockCaseMappings,
|
||||
} from '../../__fixtures__';
|
||||
|
||||
import { mockCaseConfigure } from '../../__fixtures__/mock_saved_objects';
|
||||
|
@ -42,6 +43,7 @@ describe('PATCH configuration', () => {
|
|||
const context = await createRouteContext(
|
||||
createMockSavedObjectsRepository({
|
||||
caseConfigureSavedObject: mockCaseConfigure,
|
||||
caseMappingsSavedObject: mockCaseMappings,
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -75,6 +77,7 @@ describe('PATCH configuration', () => {
|
|||
const context = await createRouteContext(
|
||||
createMockSavedObjectsRepository({
|
||||
caseConfigureSavedObject: mockCaseConfigure,
|
||||
caseMappingsSavedObject: mockCaseMappings,
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -113,6 +116,7 @@ describe('PATCH configuration', () => {
|
|||
const context = await createRouteContext(
|
||||
createMockSavedObjectsRepository({
|
||||
caseConfigureSavedObject: mockCaseConfigure,
|
||||
caseMappingsSavedObject: mockCaseMappings,
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -166,6 +170,7 @@ describe('PATCH configuration', () => {
|
|||
const context = await createRouteContext(
|
||||
createMockSavedObjectsRepository({
|
||||
caseConfigureSavedObject: mockCaseConfigure,
|
||||
caseMappingsSavedObject: mockCaseMappings,
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -193,6 +198,7 @@ describe('PATCH configuration', () => {
|
|||
const context = await createRouteContext(
|
||||
createMockSavedObjectsRepository({
|
||||
caseConfigureSavedObject: mockCaseConfigure,
|
||||
caseMappingsSavedObject: mockCaseMappings,
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
CasesConfigurePatchRt,
|
||||
CaseConfigureResponseRt,
|
||||
throwErrors,
|
||||
ConnectorMappingsAttributes,
|
||||
} from '../../../../../common/api';
|
||||
import { RouteDeps } from '../../types';
|
||||
import { wrapError, escapeHatch } from '../../utils';
|
||||
|
@ -56,6 +57,24 @@ export function initPatchCaseConfigure({ caseConfigureService, caseService, rout
|
|||
const { username, full_name, email } = await caseService.getUser({ request, response });
|
||||
|
||||
const updateDate = new Date().toISOString();
|
||||
|
||||
let mappings: ConnectorMappingsAttributes[] = [];
|
||||
if (connector != null) {
|
||||
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');
|
||||
}
|
||||
mappings = await caseClient.getMappings({
|
||||
actionsClient,
|
||||
caseClient,
|
||||
connectorId: connector.id,
|
||||
connectorType: connector.type,
|
||||
});
|
||||
}
|
||||
const patch = await caseConfigureService.patch({
|
||||
client,
|
||||
caseConfigureId: myCaseConfigure.saved_objects[0].id,
|
||||
|
@ -68,7 +87,6 @@ export function initPatchCaseConfigure({ caseConfigureService, caseService, rout
|
|||
updated_by: { email, full_name, username },
|
||||
},
|
||||
});
|
||||
|
||||
return response.ok({
|
||||
body: CaseConfigureResponseRt.encode({
|
||||
...myCaseConfigure.saved_objects[0].attributes,
|
||||
|
@ -76,6 +94,7 @@ export function initPatchCaseConfigure({ caseConfigureService, caseService, rout
|
|||
connector: transformESConnectorToCaseConnector(
|
||||
patch.attributes.connector ?? myCaseConfigure.saved_objects[0].attributes.connector
|
||||
),
|
||||
mappings,
|
||||
version: patch.version ?? '',
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -11,9 +11,10 @@ import {
|
|||
createMockSavedObjectsRepository,
|
||||
createRoute,
|
||||
createRouteContext,
|
||||
mockCaseConfigure,
|
||||
mockCaseMappings,
|
||||
} from '../../__fixtures__';
|
||||
|
||||
import { mockCaseConfigure } from '../../__fixtures__/mock_saved_objects';
|
||||
import { initPostCaseConfigure } from './post_configure';
|
||||
import { newConfiguration } from '../../__mocks__/request_responses';
|
||||
import { CASE_CONFIGURE_URL } from '../../../../../common/constants';
|
||||
|
@ -40,6 +41,7 @@ describe('POST configuration', () => {
|
|||
const context = await createRouteContext(
|
||||
createMockSavedObjectsRepository({
|
||||
caseConfigureSavedObject: mockCaseConfigure,
|
||||
caseMappingsSavedObject: mockCaseMappings,
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -75,6 +77,7 @@ describe('POST configuration', () => {
|
|||
const context = await createRouteContext(
|
||||
createMockSavedObjectsRepository({
|
||||
caseConfigureSavedObject: mockCaseConfigure,
|
||||
caseMappingsSavedObject: mockCaseMappings,
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -115,6 +118,7 @@ describe('POST configuration', () => {
|
|||
const context = await createRouteContext(
|
||||
createMockSavedObjectsRepository({
|
||||
caseConfigureSavedObject: mockCaseConfigure,
|
||||
caseMappingsSavedObject: mockCaseMappings,
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -140,6 +144,7 @@ describe('POST configuration', () => {
|
|||
const context = await createRouteContext(
|
||||
createMockSavedObjectsRepository({
|
||||
caseConfigureSavedObject: mockCaseConfigure,
|
||||
caseMappingsSavedObject: mockCaseMappings,
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -165,6 +170,7 @@ describe('POST configuration', () => {
|
|||
const context = await createRouteContext(
|
||||
createMockSavedObjectsRepository({
|
||||
caseConfigureSavedObject: mockCaseConfigure,
|
||||
caseMappingsSavedObject: mockCaseMappings,
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -190,6 +196,7 @@ describe('POST configuration', () => {
|
|||
const context = await createRouteContext(
|
||||
createMockSavedObjectsRepository({
|
||||
caseConfigureSavedObject: mockCaseConfigure,
|
||||
caseMappingsSavedObject: mockCaseMappings,
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -215,6 +222,7 @@ describe('POST configuration', () => {
|
|||
const context = await createRouteContext(
|
||||
createMockSavedObjectsRepository({
|
||||
caseConfigureSavedObject: mockCaseConfigure,
|
||||
caseMappingsSavedObject: mockCaseMappings,
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -232,6 +240,7 @@ describe('POST configuration', () => {
|
|||
|
||||
const savedObjectRepository = createMockSavedObjectsRepository({
|
||||
caseConfigureSavedObject: mockCaseConfigure,
|
||||
caseMappingsSavedObject: mockCaseMappings,
|
||||
});
|
||||
|
||||
const context = await createRouteContext(savedObjectRepository);
|
||||
|
@ -251,6 +260,7 @@ describe('POST configuration', () => {
|
|||
|
||||
const savedObjectRepository = createMockSavedObjectsRepository({
|
||||
caseConfigureSavedObject: [],
|
||||
caseMappingsSavedObject: mockCaseMappings,
|
||||
});
|
||||
|
||||
const context = await createRouteContext(savedObjectRepository);
|
||||
|
@ -273,6 +283,7 @@ describe('POST configuration', () => {
|
|||
mockCaseConfigure[0],
|
||||
{ ...mockCaseConfigure[0], id: 'mock-configuration-2' },
|
||||
],
|
||||
caseMappingsSavedObject: mockCaseMappings,
|
||||
});
|
||||
|
||||
const context = await createRouteContext(savedObjectRepository);
|
||||
|
@ -337,6 +348,7 @@ describe('POST configuration', () => {
|
|||
const context = await createRouteContext(
|
||||
createMockSavedObjectsRepository({
|
||||
caseConfigureSavedObject: mockCaseConfigure,
|
||||
caseMappingsSavedObject: mockCaseMappings,
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -363,6 +375,7 @@ describe('POST configuration', () => {
|
|||
const context = await createRouteContext(
|
||||
createMockSavedObjectsRepository({
|
||||
caseConfigureSavedObject: mockCaseConfigure,
|
||||
caseMappingsSavedObject: mockCaseMappings,
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -388,6 +401,7 @@ describe('POST configuration', () => {
|
|||
const context = await createRouteContext(
|
||||
createMockSavedObjectsRepository({
|
||||
caseConfigureSavedObject: mockCaseConfigure,
|
||||
caseMappingsSavedObject: mockCaseMappings,
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -409,6 +423,7 @@ describe('POST configuration', () => {
|
|||
const context = await createRouteContext(
|
||||
createMockSavedObjectsRepository({
|
||||
caseConfigureSavedObject: mockCaseConfigure,
|
||||
caseMappingsSavedObject: mockCaseMappings,
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
@ -32,6 +32,14 @@ export function initPostCaseConfigure({ caseConfigureService, caseService, route
|
|||
},
|
||||
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 client = context.core.savedObjects.client;
|
||||
const query = pipe(
|
||||
CasesConfigureRequestRt.decode(request.body),
|
||||
|
@ -39,7 +47,6 @@ export function initPostCaseConfigure({ caseConfigureService, caseService, route
|
|||
);
|
||||
|
||||
const myCaseConfigure = await caseConfigureService.find({ client });
|
||||
|
||||
if (myCaseConfigure.saved_objects.length > 0) {
|
||||
await Promise.all(
|
||||
myCaseConfigure.saved_objects.map((cc) =>
|
||||
|
@ -51,6 +58,12 @@ export function initPostCaseConfigure({ caseConfigureService, caseService, route
|
|||
const { email, full_name, username } = await caseService.getUser({ request, response });
|
||||
|
||||
const creationDate = new Date().toISOString();
|
||||
const mappings = await caseClient.getMappings({
|
||||
actionsClient,
|
||||
caseClient,
|
||||
connectorId: query.connector.id,
|
||||
connectorType: query.connector.type,
|
||||
});
|
||||
const post = await caseConfigureService.post({
|
||||
client,
|
||||
attributes: {
|
||||
|
@ -68,6 +81,7 @@ export function initPostCaseConfigure({ caseConfigureService, caseService, route
|
|||
...post.attributes,
|
||||
// Reserve for future implementations
|
||||
connector: transformESConnectorToCaseConnector(post.attributes.connector),
|
||||
mappings,
|
||||
version: post.version ?? '',
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
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, connectorMappingsService }: 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));
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
|
@ -0,0 +1,385 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
prepareFieldsForTransformation,
|
||||
transformFields,
|
||||
transformComments,
|
||||
transformers,
|
||||
} from './utils';
|
||||
|
||||
import { comment as commentObj, defaultPipes, mappings, params, updateUser } from './mock';
|
||||
import {
|
||||
ServiceConnectorCaseParams,
|
||||
ExternalServiceParams,
|
||||
Incident,
|
||||
} from '../../../../../common/api/connectors';
|
||||
const formatComment = { commentId: commentObj.commentId, comment: commentObj.comment };
|
||||
describe('api/cases/configure/utils', () => {
|
||||
describe('prepareFieldsForTransformation', () => {
|
||||
test('prepare fields with defaults', () => {
|
||||
const res = prepareFieldsForTransformation({
|
||||
defaultPipes,
|
||||
params,
|
||||
mappings,
|
||||
});
|
||||
expect(res).toEqual([
|
||||
{
|
||||
actionType: 'overwrite',
|
||||
key: 'short_description',
|
||||
pipes: ['informationCreated'],
|
||||
value: 'a title',
|
||||
},
|
||||
{
|
||||
actionType: 'append',
|
||||
key: 'description',
|
||||
pipes: ['informationCreated', 'append'],
|
||||
value: 'a description',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('prepare fields with default pipes', () => {
|
||||
const res = prepareFieldsForTransformation({
|
||||
defaultPipes: ['myTestPipe'],
|
||||
mappings,
|
||||
params,
|
||||
});
|
||||
expect(res).toEqual([
|
||||
{
|
||||
actionType: 'overwrite',
|
||||
key: 'short_description',
|
||||
pipes: ['myTestPipe'],
|
||||
value: 'a title',
|
||||
},
|
||||
{
|
||||
actionType: 'append',
|
||||
key: 'description',
|
||||
pipes: ['myTestPipe', 'append'],
|
||||
value: 'a description',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('transformFields', () => {
|
||||
test('transform fields for creation correctly', () => {
|
||||
const fields = prepareFieldsForTransformation({
|
||||
defaultPipes,
|
||||
mappings,
|
||||
params,
|
||||
});
|
||||
|
||||
const res = transformFields<ServiceConnectorCaseParams, ExternalServiceParams, Incident>({
|
||||
params,
|
||||
fields,
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
short_description: 'a title (created at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
description: 'a description (created at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
});
|
||||
});
|
||||
|
||||
test('transform fields for update correctly', () => {
|
||||
const fields = prepareFieldsForTransformation({
|
||||
params,
|
||||
mappings,
|
||||
defaultPipes: ['informationUpdated'],
|
||||
});
|
||||
|
||||
const res = transformFields<ServiceConnectorCaseParams, ExternalServiceParams, Incident>({
|
||||
params: {
|
||||
...params,
|
||||
updatedAt: '2020-03-15T08:34:53.450Z',
|
||||
updatedBy: {
|
||||
username: 'anotherUser',
|
||||
fullName: 'Another User',
|
||||
},
|
||||
},
|
||||
fields,
|
||||
currentIncident: {
|
||||
short_description: 'first title (created at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
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:
|
||||
'first description (created at 2020-03-13T08:34:53.450Z by Elastic User) \r\na description (updated at 2020-03-15T08:34:53.450Z by Another User)',
|
||||
});
|
||||
});
|
||||
|
||||
test('add newline character to description', () => {
|
||||
const fields = prepareFieldsForTransformation({
|
||||
params,
|
||||
mappings,
|
||||
defaultPipes: ['informationUpdated'],
|
||||
});
|
||||
|
||||
const res = transformFields<ServiceConnectorCaseParams, ExternalServiceParams, Incident>({
|
||||
params,
|
||||
fields,
|
||||
currentIncident: {
|
||||
short_description: 'first title',
|
||||
description: 'first description',
|
||||
},
|
||||
});
|
||||
expect(res.description?.includes('\r\n')).toBe(true);
|
||||
});
|
||||
|
||||
test('append username if fullname is undefined when create', () => {
|
||||
const fields = prepareFieldsForTransformation({
|
||||
defaultPipes,
|
||||
mappings,
|
||||
params,
|
||||
});
|
||||
|
||||
const res = transformFields<ServiceConnectorCaseParams, ExternalServiceParams, Incident>({
|
||||
params: {
|
||||
...params,
|
||||
createdBy: { fullName: '', username: 'elastic' },
|
||||
},
|
||||
fields,
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
short_description: 'a title (created at 2020-03-13T08:34:53.450Z by elastic)',
|
||||
description: 'a description (created at 2020-03-13T08:34:53.450Z by elastic)',
|
||||
});
|
||||
});
|
||||
|
||||
test('append username if fullname is undefined when update', () => {
|
||||
const fields = prepareFieldsForTransformation({
|
||||
defaultPipes: ['informationUpdated'],
|
||||
mappings,
|
||||
params,
|
||||
});
|
||||
|
||||
const res = transformFields<ServiceConnectorCaseParams, ExternalServiceParams, Incident>({
|
||||
params: {
|
||||
...params,
|
||||
updatedAt: '2020-03-15T08:34:53.450Z',
|
||||
updatedBy: { username: 'anotherUser', fullName: '' },
|
||||
},
|
||||
fields,
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
short_description: 'a title (updated at 2020-03-15T08:34:53.450Z by anotherUser)',
|
||||
description: 'a description (updated at 2020-03-15T08:34:53.450Z by anotherUser)',
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('transformComments', () => {
|
||||
test('transform creation comments', () => {
|
||||
const comments = [commentObj];
|
||||
const res = transformComments(comments, ['informationCreated']);
|
||||
expect(res).toEqual([
|
||||
{
|
||||
...formatComment,
|
||||
comment: `${formatComment.comment} (created at ${comments[0].createdAt} by ${comments[0].createdBy.fullName})`,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('transform update comments', () => {
|
||||
const comments = [
|
||||
{
|
||||
...commentObj,
|
||||
...updateUser,
|
||||
},
|
||||
];
|
||||
const res = transformComments(comments, ['informationUpdated']);
|
||||
expect(res).toEqual([
|
||||
{
|
||||
...formatComment,
|
||||
comment: `${formatComment.comment} (updated at ${updateUser.updatedAt} by ${updateUser.updatedBy.fullName})`,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('transform added comments', () => {
|
||||
const comments = [commentObj];
|
||||
const res = transformComments(comments, ['informationAdded']);
|
||||
expect(res).toEqual([
|
||||
{
|
||||
...formatComment,
|
||||
comment: `${formatComment.comment} (added at ${comments[0].createdAt} by ${comments[0].createdBy.fullName})`,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('transform comments without fullname', () => {
|
||||
const comments = [{ ...commentObj, createdBy: { username: commentObj.createdBy.username } }];
|
||||
// @ts-ignore testing no fullName
|
||||
const res = transformComments(comments, ['informationAdded']);
|
||||
expect(res).toEqual([
|
||||
{
|
||||
...formatComment,
|
||||
comment: `${formatComment.comment} (added at ${comments[0].createdAt} by ${comments[0].createdBy.username})`,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('adds update user correctly', () => {
|
||||
const comments = [
|
||||
{
|
||||
...commentObj,
|
||||
updatedAt: '2020-04-13T08:34:53.450Z',
|
||||
updatedBy: { fullName: 'Elastic2', username: 'elastic' },
|
||||
},
|
||||
];
|
||||
const res = transformComments(comments, ['informationAdded']);
|
||||
expect(res).toEqual([
|
||||
{
|
||||
...formatComment,
|
||||
comment: `${formatComment.comment} (added at ${comments[0].updatedAt} by ${comments[0].updatedBy.fullName})`,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('adds update user with empty fullname correctly', () => {
|
||||
const comments = [
|
||||
{
|
||||
...commentObj,
|
||||
updatedAt: '2020-04-13T08:34:53.450Z',
|
||||
updatedBy: { fullName: '', username: 'elastic2' },
|
||||
},
|
||||
];
|
||||
const res = transformComments(comments, ['informationAdded']);
|
||||
expect(res).toEqual([
|
||||
{
|
||||
...formatComment,
|
||||
comment: `${formatComment.comment} (added at ${comments[0].updatedAt} by ${comments[0].updatedBy.username})`,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('transformers', () => {
|
||||
const { informationCreated, informationUpdated, informationAdded, append } = transformers;
|
||||
describe('informationCreated', () => {
|
||||
test('transforms correctly', () => {
|
||||
const res = informationCreated({
|
||||
value: 'a value',
|
||||
date: '2020-04-15T08:19:27.400Z',
|
||||
user: 'elastic',
|
||||
});
|
||||
expect(res).toEqual({ value: 'a value (created at 2020-04-15T08:19:27.400Z by elastic)' });
|
||||
});
|
||||
|
||||
test('transforms correctly without optional fields', () => {
|
||||
const res = informationCreated({
|
||||
value: 'a value',
|
||||
});
|
||||
expect(res).toEqual({ value: 'a value (created at by )' });
|
||||
});
|
||||
|
||||
test('returns correctly rest fields', () => {
|
||||
const res = informationCreated({
|
||||
value: 'a value',
|
||||
date: '2020-04-15T08:19:27.400Z',
|
||||
user: 'elastic',
|
||||
previousValue: 'previous value',
|
||||
});
|
||||
expect(res).toEqual({
|
||||
value: 'a value (created at 2020-04-15T08:19:27.400Z by elastic)',
|
||||
previousValue: 'previous value',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('informationUpdated', () => {
|
||||
test('transforms correctly', () => {
|
||||
const res = informationUpdated({
|
||||
value: 'a value',
|
||||
date: '2020-04-15T08:19:27.400Z',
|
||||
user: 'elastic',
|
||||
});
|
||||
expect(res).toEqual({ value: 'a value (updated at 2020-04-15T08:19:27.400Z by elastic)' });
|
||||
});
|
||||
|
||||
test('transforms correctly without optional fields', () => {
|
||||
const res = informationUpdated({
|
||||
value: 'a value',
|
||||
});
|
||||
expect(res).toEqual({ value: 'a value (updated at by )' });
|
||||
});
|
||||
|
||||
test('returns correctly rest fields', () => {
|
||||
const res = informationUpdated({
|
||||
value: 'a value',
|
||||
date: '2020-04-15T08:19:27.400Z',
|
||||
user: 'elastic',
|
||||
previousValue: 'previous value',
|
||||
});
|
||||
expect(res).toEqual({
|
||||
value: 'a value (updated at 2020-04-15T08:19:27.400Z by elastic)',
|
||||
previousValue: 'previous value',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('informationAdded', () => {
|
||||
test('transforms correctly', () => {
|
||||
const res = informationAdded({
|
||||
value: 'a value',
|
||||
date: '2020-04-15T08:19:27.400Z',
|
||||
user: 'elastic',
|
||||
});
|
||||
expect(res).toEqual({ value: 'a value (added at 2020-04-15T08:19:27.400Z by elastic)' });
|
||||
});
|
||||
|
||||
test('transforms correctly without optional fields', () => {
|
||||
const res = informationAdded({
|
||||
value: 'a value',
|
||||
});
|
||||
expect(res).toEqual({ value: 'a value (added at by )' });
|
||||
});
|
||||
|
||||
test('returns correctly rest fields', () => {
|
||||
const res = informationAdded({
|
||||
value: 'a value',
|
||||
date: '2020-04-15T08:19:27.400Z',
|
||||
user: 'elastic',
|
||||
previousValue: 'previous value',
|
||||
});
|
||||
expect(res).toEqual({
|
||||
value: 'a value (added at 2020-04-15T08:19:27.400Z by elastic)',
|
||||
previousValue: 'previous value',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('append', () => {
|
||||
test('transforms correctly', () => {
|
||||
const res = append({
|
||||
value: 'a value',
|
||||
previousValue: 'previous value',
|
||||
});
|
||||
expect(res).toEqual({ value: 'previous value \r\na value' });
|
||||
});
|
||||
|
||||
test('transforms correctly without optional fields', () => {
|
||||
const res = append({
|
||||
value: 'a value',
|
||||
});
|
||||
expect(res).toEqual({ value: 'a value' });
|
||||
});
|
||||
|
||||
test('returns correctly rest fields', () => {
|
||||
const res = append({
|
||||
value: 'a value',
|
||||
user: 'elastic',
|
||||
previousValue: 'previous value',
|
||||
});
|
||||
expect(res).toEqual({
|
||||
value: 'previous value \r\na value',
|
||||
user: 'elastic',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
237
x-pack/plugins/case/server/routes/api/cases/configure/utils.ts
Normal file
237
x-pack/plugins/case/server/routes/api/cases/configure/utils.ts
Normal file
|
@ -0,0 +1,237 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { flow } from 'lodash';
|
||||
import {
|
||||
ServiceConnectorCaseParams,
|
||||
ServiceConnectorCommentParams,
|
||||
ConnectorMappingsAttributes,
|
||||
ConnectorTypes,
|
||||
EntityInformation,
|
||||
ExternalServiceParams,
|
||||
Incident,
|
||||
JiraPushToServiceApiParams,
|
||||
MapIncident,
|
||||
PipedField,
|
||||
PrepareFieldsForTransformArgs,
|
||||
PushToServiceApiParams,
|
||||
ResilientPushToServiceApiParams,
|
||||
ServiceNowPushToServiceApiParams,
|
||||
SimpleComment,
|
||||
Transformer,
|
||||
TransformerArgs,
|
||||
TransformFieldsArgs,
|
||||
} from '../../../../../common/api';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { ActionsClient } from '../../../../../../actions/server/actions_client';
|
||||
|
||||
export const mapIncident = async (
|
||||
actionsClient: ActionsClient,
|
||||
connectorId: string,
|
||||
connectorType: string,
|
||||
mappings: ConnectorMappingsAttributes[],
|
||||
params: ServiceConnectorCaseParams
|
||||
): Promise<MapIncident> => {
|
||||
const { comments: caseComments, externalId } = params;
|
||||
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;
|
||||
if (externalId) {
|
||||
try {
|
||||
currentIncident = ((await actionsClient.execute({
|
||||
actionId: connectorId,
|
||||
params: {
|
||||
subAction: 'getIncident',
|
||||
subActionParams: { externalId },
|
||||
},
|
||||
})) as unknown) as ExternalServiceParams | undefined;
|
||||
} catch (ex) {
|
||||
throw new Error(
|
||||
`Retrieving Incident by id ${externalId} from ${thirdPartyName} failed with exception: ${ex}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const fields = prepareFieldsForTransformation({
|
||||
defaultPipes,
|
||||
mappings,
|
||||
params,
|
||||
});
|
||||
|
||||
const transformedFields = transformFields<
|
||||
ServiceConnectorCaseParams,
|
||||
ExternalServiceParams,
|
||||
Incident
|
||||
>({
|
||||
params,
|
||||
fields,
|
||||
currentIncident,
|
||||
});
|
||||
incident = { ...incident, ...transformedFields, externalId };
|
||||
let comments: SimpleComment[] = [];
|
||||
if (caseComments && Array.isArray(caseComments) && caseComments.length > 0) {
|
||||
const commentsMapping = mappings.find((m) => m.source === 'comments');
|
||||
if (commentsMapping?.action_type !== 'nothing') {
|
||||
comments = transformComments(caseComments, ['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 ServiceNowPushToServiceApiParams['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.username
|
||||
: entity.createdBy != null
|
||||
? entity.createdBy.fullName
|
||||
? entity.createdBy.fullName
|
||||
: entity.createdBy.username
|
||||
: '') ?? '';
|
||||
|
||||
export const FIELD_INFORMATION = (
|
||||
mode: string,
|
||||
date: string | undefined,
|
||||
user: string | undefined
|
||||
) => {
|
||||
switch (mode) {
|
||||
case 'create':
|
||||
return i18n.translate('xpack.case.connectors.case.externalIncidentCreated', {
|
||||
values: { date, user },
|
||||
defaultMessage: '(created at {date} by {user})',
|
||||
});
|
||||
case 'update':
|
||||
return i18n.translate('xpack.case.connectors.case.externalIncidentUpdated', {
|
||||
values: { date, user },
|
||||
defaultMessage: '(updated at {date} by {user})',
|
||||
});
|
||||
case 'add':
|
||||
return i18n.translate('xpack.case.connectors.case.externalIncidentAdded', {
|
||||
values: { date, user },
|
||||
defaultMessage: '(added at {date} by {user})',
|
||||
});
|
||||
default:
|
||||
return i18n.translate('xpack.case.connectors.case.externalIncidentDefault', {
|
||||
values: { date, user },
|
||||
defaultMessage: '(created at {date} by {user})',
|
||||
});
|
||||
}
|
||||
};
|
||||
export const transformers: Record<string, Transformer> = {
|
||||
informationCreated: ({ value, date, user, ...rest }: TransformerArgs): TransformerArgs => ({
|
||||
value: `${value} ${FIELD_INFORMATION('create', date, user)}`,
|
||||
...rest,
|
||||
}),
|
||||
informationUpdated: ({ value, date, user, ...rest }: TransformerArgs): TransformerArgs => ({
|
||||
value: `${value} ${FIELD_INFORMATION('update', date, user)}`,
|
||||
...rest,
|
||||
}),
|
||||
informationAdded: ({ value, date, user, ...rest }: TransformerArgs): TransformerArgs => ({
|
||||
value: `${value} ${FIELD_INFORMATION('add', date, user)}`,
|
||||
...rest,
|
||||
}),
|
||||
append: ({ value, previousValue, ...rest }: TransformerArgs): TransformerArgs => ({
|
||||
value: previousValue ? `${previousValue} \r\n${value}` : `${value}`,
|
||||
...rest,
|
||||
}),
|
||||
};
|
||||
export const prepareFieldsForTransformation = ({
|
||||
defaultPipes,
|
||||
mappings,
|
||||
params,
|
||||
}: PrepareFieldsForTransformArgs): PipedField[] =>
|
||||
mappings.reduce(
|
||||
(acc: PipedField[], mapping) =>
|
||||
mapping != null &&
|
||||
mapping.target !== 'not_mapped' &&
|
||||
mapping.action_type !== 'nothing' &&
|
||||
mapping.source !== 'comments'
|
||||
? [
|
||||
...acc,
|
||||
{
|
||||
key: mapping.target,
|
||||
value: params[mapping.source] ?? '',
|
||||
actionType: mapping.action_type,
|
||||
pipes: mapping.action_type === 'append' ? [...defaultPipes, 'append'] : defaultPipes,
|
||||
},
|
||||
]
|
||||
: acc,
|
||||
[]
|
||||
);
|
||||
|
||||
export const transformFields = <
|
||||
P extends EntityInformation,
|
||||
S extends Record<string, unknown>,
|
||||
R extends {}
|
||||
>({
|
||||
params,
|
||||
fields,
|
||||
currentIncident,
|
||||
}: TransformFieldsArgs<P, S>): R => {
|
||||
return fields.reduce((prev, cur) => {
|
||||
const transform = flow(...cur.pipes.map((p) => transformers[p]));
|
||||
return {
|
||||
...prev,
|
||||
[cur.key]: transform({
|
||||
value: cur.value,
|
||||
date: params.updatedAt ?? params.createdAt,
|
||||
user: getEntity(params),
|
||||
previousValue: currentIncident ? currentIncident[cur.key] : '',
|
||||
}).value,
|
||||
};
|
||||
}, {} as R);
|
||||
};
|
||||
|
||||
export const transformComments = (
|
||||
comments: ServiceConnectorCommentParams[],
|
||||
pipes: string[]
|
||||
): SimpleComment[] =>
|
||||
comments.map((c) => ({
|
||||
comment: flow(...pipes.map((p) => transformers[p]))({
|
||||
value: c.comment,
|
||||
date: c.updatedAt ?? c.createdAt,
|
||||
user: getEntity(c),
|
||||
}).value,
|
||||
commentId: c.commentId,
|
||||
}));
|
|
@ -25,8 +25,10 @@ import { initPostCommentApi } from './cases/comments/post_comment';
|
|||
|
||||
import { initCaseConfigureGetActionConnector } from './cases/configure/get_connectors';
|
||||
import { initGetCaseConfigure } from './cases/configure/get_configure';
|
||||
import { initCaseConfigureGetFields } from './cases/configure/get_fields';
|
||||
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';
|
||||
|
||||
|
@ -52,6 +54,8 @@ export function initCaseApi(deps: RouteDeps) {
|
|||
initGetCaseConfigure(deps);
|
||||
initPatchCaseConfigure(deps);
|
||||
initPostCaseConfigure(deps);
|
||||
initCaseConfigureGetFields(deps);
|
||||
initPostPushToService(deps);
|
||||
// Reporters
|
||||
initGetReportersApi(deps);
|
||||
// Status
|
||||
|
|
|
@ -9,13 +9,15 @@ import {
|
|||
CaseConfigureServiceSetup,
|
||||
CaseServiceSetup,
|
||||
CaseUserActionServiceSetup,
|
||||
ConnectorMappingsServiceSetup,
|
||||
} from '../../services';
|
||||
|
||||
export interface RouteDeps {
|
||||
caseConfigureService: CaseConfigureServiceSetup;
|
||||
caseService: CaseServiceSetup;
|
||||
userActionService: CaseUserActionServiceSetup;
|
||||
connectorMappingsService: ConnectorMappingsServiceSetup;
|
||||
router: IRouter;
|
||||
userActionService: CaseUserActionServiceSetup;
|
||||
}
|
||||
|
||||
export enum SortFieldCase {
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { SavedObjectsType } from 'src/core/server';
|
||||
|
||||
export const CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT = 'cases-connector-mappings';
|
||||
|
||||
export const caseConnectorMappingsSavedObjectType: SavedObjectsType = {
|
||||
name: CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT,
|
||||
hidden: false,
|
||||
namespaceType: 'single',
|
||||
mappings: {
|
||||
properties: {
|
||||
mappings: {
|
||||
properties: {
|
||||
source: {
|
||||
type: 'keyword',
|
||||
},
|
||||
target: {
|
||||
type: 'keyword',
|
||||
},
|
||||
action_type: {
|
||||
type: 'keyword',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
|
@ -8,3 +8,7 @@ export { caseSavedObjectType, CASE_SAVED_OBJECT } from './cases';
|
|||
export { caseConfigureSavedObjectType, CASE_CONFIGURE_SAVED_OBJECT } from './configure';
|
||||
export { caseCommentSavedObjectType, CASE_COMMENT_SAVED_OBJECT } from './comments';
|
||||
export { caseUserActionSavedObjectType, CASE_USER_ACTION_SAVED_OBJECT } from './user_actions';
|
||||
export {
|
||||
caseConnectorMappingsSavedObjectType,
|
||||
CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT,
|
||||
} from './connector_mappings';
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
Logger,
|
||||
SavedObject,
|
||||
SavedObjectReference,
|
||||
SavedObjectsClientContract,
|
||||
SavedObjectsFindResponse,
|
||||
} from 'kibana/server';
|
||||
|
||||
import { ConnectorMappings, SavedObjectFindOptions } from '../../../common/api';
|
||||
import { CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT } from '../../saved_object_types';
|
||||
|
||||
interface ClientArgs {
|
||||
client: SavedObjectsClientContract;
|
||||
}
|
||||
interface FindConnectorMappingsArgs extends ClientArgs {
|
||||
options?: SavedObjectFindOptions;
|
||||
}
|
||||
|
||||
interface PostConnectorMappingsArgs extends ClientArgs {
|
||||
attributes: ConnectorMappings;
|
||||
references: SavedObjectReference[];
|
||||
}
|
||||
|
||||
export interface ConnectorMappingsServiceSetup {
|
||||
find(args: FindConnectorMappingsArgs): Promise<SavedObjectsFindResponse<ConnectorMappings>>;
|
||||
post(args: PostConnectorMappingsArgs): Promise<SavedObject<ConnectorMappings>>;
|
||||
}
|
||||
|
||||
export class ConnectorMappingsService {
|
||||
constructor(private readonly log: Logger) {}
|
||||
public setup = async (): Promise<ConnectorMappingsServiceSetup> => ({
|
||||
find: async ({ client, options }: FindConnectorMappingsArgs) => {
|
||||
try {
|
||||
this.log.debug(`Attempting to find all connector mappings`);
|
||||
return await client.find({ ...options, type: CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT });
|
||||
} catch (error) {
|
||||
this.log.debug(`Attempting to find all connector mappings`);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
post: async ({ client, attributes, references }: PostConnectorMappingsArgs) => {
|
||||
try {
|
||||
this.log.debug(`Attempting to POST a new connector mappings`);
|
||||
return await client.create(CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT, attributes, {
|
||||
references,
|
||||
});
|
||||
} catch (error) {
|
||||
this.log.debug(`Error on POST a new connector mappings: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
|
@ -31,6 +31,7 @@ import { readTags } from './tags/read_tags';
|
|||
|
||||
export { CaseConfigureService, CaseConfigureServiceSetup } from './configure';
|
||||
export { CaseUserActionService, CaseUserActionServiceSetup } from './user_actions';
|
||||
export { ConnectorMappingsService, ConnectorMappingsServiceSetup } from './connector_mappings';
|
||||
export { AlertService, AlertServiceContract } from './alerts';
|
||||
|
||||
export interface ClientArgs {
|
||||
|
|
|
@ -5,14 +5,16 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
AlertServiceContract,
|
||||
CaseConfigureServiceSetup,
|
||||
CaseServiceSetup,
|
||||
CaseUserActionServiceSetup,
|
||||
AlertServiceContract,
|
||||
ConnectorMappingsServiceSetup,
|
||||
} from '.';
|
||||
|
||||
export type CaseServiceMock = jest.Mocked<CaseServiceSetup>;
|
||||
export type CaseConfigureServiceMock = jest.Mocked<CaseConfigureServiceSetup>;
|
||||
export type ConnectorMappingsServiceMock = jest.Mocked<ConnectorMappingsServiceSetup>;
|
||||
export type CaseUserActionServiceMock = jest.Mocked<CaseUserActionServiceSetup>;
|
||||
export type AlertServiceMock = jest.Mocked<AlertServiceContract>;
|
||||
|
||||
|
@ -43,6 +45,11 @@ export const createConfigureServiceMock = (): CaseConfigureServiceMock => ({
|
|||
post: jest.fn(),
|
||||
});
|
||||
|
||||
export const connectorMappingsServiceMock = (): ConnectorMappingsServiceMock => ({
|
||||
find: jest.fn(),
|
||||
post: jest.fn(),
|
||||
});
|
||||
|
||||
export const createUserActionServiceMock = (): CaseUserActionServiceMock => ({
|
||||
getUserActions: jest.fn(),
|
||||
postUserActions: jest.fn(),
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
import { serviceNowConnector } from '../objects/case';
|
||||
|
||||
import { TOASTER } from '../screens/configure_cases';
|
||||
import { SERVICE_NOW_MAPPING, TOASTER } from '../screens/configure_cases';
|
||||
|
||||
import { goToEditExternalConnection } from '../tasks/all_cases';
|
||||
import {
|
||||
|
@ -18,9 +18,33 @@ import { loginAndWaitForPageWithoutDateRange } from '../tasks/login';
|
|||
import { CASES_URL } from '../urls/navigation';
|
||||
|
||||
describe('Cases connectors', () => {
|
||||
const configureResult = {
|
||||
connector: {
|
||||
id: 'e271c3b8-f702-4fbc-98e0-db942b573bbd',
|
||||
name: 'SN',
|
||||
type: '.servicenow',
|
||||
fields: null,
|
||||
},
|
||||
closure_type: 'close-by-user',
|
||||
created_at: '2020-12-01T16:28:09.219Z',
|
||||
created_by: { email: null, full_name: null, username: 'elastic' },
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
mappings: [
|
||||
{ source: 'title', target: 'short_description', action_type: 'overwrite' },
|
||||
{ source: 'description', target: 'description', action_type: 'overwrite' },
|
||||
{ source: 'comments', target: 'comments', action_type: 'append' },
|
||||
],
|
||||
version: 'WzEwNCwxXQ==',
|
||||
};
|
||||
before(() => {
|
||||
cy.intercept('POST', '/api/actions/action').as('createConnector');
|
||||
cy.intercept('POST', '/api/cases/configure').as('saveConnector');
|
||||
cy.intercept('POST', '/api/cases/configure', (req) => {
|
||||
const connector = req.body.connector;
|
||||
req.reply((res) => {
|
||||
res.send(200, { ...configureResult, connector });
|
||||
});
|
||||
}).as('saveConnector');
|
||||
});
|
||||
|
||||
it('Configures a new connector', () => {
|
||||
|
@ -37,6 +61,7 @@ describe('Cases connectors', () => {
|
|||
selectLastConnectorCreated(response!.body.id);
|
||||
|
||||
cy.wait('@saveConnector', { timeout: 10000 }).its('response.statusCode').should('eql', 200);
|
||||
cy.get(SERVICE_NOW_MAPPING).first().should('have.text', 'short_description');
|
||||
cy.get(TOASTER).should('have.text', 'Saved external connection settings');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -83,14 +83,6 @@ export const mockConnectorsResponse = [
|
|||
actionTypeId: '.jira',
|
||||
name: 'Jira',
|
||||
config: {
|
||||
incidentConfiguration: {
|
||||
mapping: [
|
||||
{ source: 'title', target: 'summary', actionType: 'overwrite' },
|
||||
{ source: 'description', target: 'description', actionType: 'overwrite' },
|
||||
{ source: 'comments', target: 'comments', actionType: 'append' },
|
||||
],
|
||||
},
|
||||
isCaseOwned: true,
|
||||
apiUrl: 'https://siem-kibana.atlassian.net',
|
||||
projectKey: 'RJ',
|
||||
},
|
||||
|
@ -102,14 +94,6 @@ export const mockConnectorsResponse = [
|
|||
actionTypeId: '.resilient',
|
||||
name: 'Resilient',
|
||||
config: {
|
||||
incidentConfiguration: {
|
||||
mapping: [
|
||||
{ source: 'title', target: 'name', actionType: 'overwrite' },
|
||||
{ source: 'description', target: 'description', actionType: 'overwrite' },
|
||||
{ source: 'comments', target: 'comments', actionType: 'append' },
|
||||
],
|
||||
},
|
||||
isCaseOwned: true,
|
||||
apiUrl: 'https://ibm-resilient.siem.estc.dev',
|
||||
orgId: '201',
|
||||
},
|
||||
|
@ -121,14 +105,6 @@ export const mockConnectorsResponse = [
|
|||
actionTypeId: '.servicenow',
|
||||
name: 'ServiceNow',
|
||||
config: {
|
||||
incidentConfiguration: {
|
||||
mapping: [
|
||||
{ source: 'title', target: 'short_description', actionType: 'overwrite' },
|
||||
{ source: 'description', target: 'description', actionType: 'overwrite' },
|
||||
{ source: 'comments', target: 'comments', actionType: 'append' },
|
||||
],
|
||||
},
|
||||
isCaseOwned: true,
|
||||
apiUrl: 'https://dev65287.service-now.com',
|
||||
},
|
||||
isPreconfigured: false,
|
||||
|
|
|
@ -28,3 +28,5 @@ export const TOASTER = '[data-test-subj="euiToastHeader"]';
|
|||
export const URL = '[data-test-subj="apiUrlFromInput"]';
|
||||
|
||||
export const USERNAME = '[data-test-subj="connector-servicenow-username-form-input"]';
|
||||
|
||||
export const SERVICE_NOW_MAPPING = 'code[data-test-subj="field-mapping-target"]';
|
||||
|
|
|
@ -8,9 +8,8 @@ import { ActionConnector } from '../../../containers/configure/types';
|
|||
import { UseConnectorsResponse } from '../../../containers/configure/use_connectors';
|
||||
import { connectorsMock } from '../../../containers/configure/mock';
|
||||
import { ReturnUseCaseConfigure } from '../../../containers/configure/use_configure';
|
||||
export { mapping } from '../../../containers/configure/mock';
|
||||
import { ConnectorTypes } from '../../../../../../case/common/api';
|
||||
|
||||
export { mappings } from '../../../containers/configure/mock';
|
||||
export const connectors: ActionConnector[] = connectorsMock;
|
||||
|
||||
// x - pack / plugins / triggers_actions_ui;
|
||||
|
@ -36,14 +35,14 @@ export const useCaseConfigureResponse: ReturnUseCaseConfigure = {
|
|||
},
|
||||
firstLoad: false,
|
||||
loading: false,
|
||||
mapping: null,
|
||||
mappings: [],
|
||||
persistCaseConfigure: jest.fn(),
|
||||
persistLoading: false,
|
||||
refetchCaseConfigure: jest.fn(),
|
||||
setClosureType: jest.fn(),
|
||||
setConnector: jest.fn(),
|
||||
setCurrentConfiguration: jest.fn(),
|
||||
setMapping: jest.fn(),
|
||||
setMappings: jest.fn(),
|
||||
version: '',
|
||||
};
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import { Connectors, Props } from './connectors';
|
|||
import { TestProviders } from '../../../common/mock';
|
||||
import { ConnectorsDropdown } from './connectors_dropdown';
|
||||
import { connectors } from './__mock__';
|
||||
import { ConnectorTypes } from '../../../../../case/common/api/connectors';
|
||||
|
||||
describe('Connectors', () => {
|
||||
let wrapper: ReactWrapper;
|
||||
|
@ -18,13 +19,14 @@ describe('Connectors', () => {
|
|||
const handleShowEditFlyout = jest.fn();
|
||||
|
||||
const props: Props = {
|
||||
disabled: false,
|
||||
updateConnectorDisabled: false,
|
||||
connectors,
|
||||
selectedConnector: 'none',
|
||||
isLoading: false,
|
||||
onChangeConnector,
|
||||
disabled: false,
|
||||
handleShowEditFlyout,
|
||||
isLoading: false,
|
||||
mappings: [],
|
||||
onChangeConnector,
|
||||
selectedConnector: { id: 'none', type: ConnectorTypes.none },
|
||||
updateConnectorDisabled: false,
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
|
@ -66,9 +68,15 @@ describe('Connectors', () => {
|
|||
|
||||
test('the connector is changed successfully to none', () => {
|
||||
onChangeConnector.mockClear();
|
||||
const newWrapper = mount(<Connectors {...props} selectedConnector={'servicenow-1'} />, {
|
||||
wrappingComponent: TestProviders,
|
||||
});
|
||||
const newWrapper = mount(
|
||||
<Connectors
|
||||
{...props}
|
||||
selectedConnector={{ id: 'servicenow-1', type: ConnectorTypes.servicenow }}
|
||||
/>,
|
||||
{
|
||||
wrappingComponent: TestProviders,
|
||||
}
|
||||
);
|
||||
|
||||
newWrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
|
||||
newWrapper.find('button[data-test-subj="dropdown-connector-no-connector"]').simulate('click');
|
||||
|
@ -87,9 +95,15 @@ describe('Connectors', () => {
|
|||
});
|
||||
|
||||
test('the text of the update button is shown correctly', () => {
|
||||
const newWrapper = mount(<Connectors {...props} selectedConnector={'servicenow-1'} />, {
|
||||
wrappingComponent: TestProviders,
|
||||
});
|
||||
const newWrapper = mount(
|
||||
<Connectors
|
||||
{...props}
|
||||
selectedConnector={{ id: 'servicenow-1', type: ConnectorTypes.servicenow }}
|
||||
/>,
|
||||
{
|
||||
wrappingComponent: TestProviders,
|
||||
}
|
||||
);
|
||||
|
||||
expect(
|
||||
newWrapper
|
||||
|
|
|
@ -18,7 +18,9 @@ import styled from 'styled-components';
|
|||
import { ConnectorsDropdown } from './connectors_dropdown';
|
||||
import * as i18n from './translations';
|
||||
|
||||
import { ActionConnector } from '../../containers/configure/types';
|
||||
import { ActionConnector, CaseConnectorMapping } from '../../containers/configure/types';
|
||||
import { Mapping } from './mapping';
|
||||
import { ConnectorTypes } from '../../../../../case/common/api/connectors';
|
||||
|
||||
const EuiFormRowExtended = styled(EuiFormRow)`
|
||||
.euiFormRow__labelWrapper {
|
||||
|
@ -31,24 +33,26 @@ const EuiFormRowExtended = styled(EuiFormRow)`
|
|||
export interface Props {
|
||||
connectors: ActionConnector[];
|
||||
disabled: boolean;
|
||||
isLoading: boolean;
|
||||
updateConnectorDisabled: boolean;
|
||||
onChangeConnector: (id: string) => void;
|
||||
selectedConnector: string;
|
||||
handleShowEditFlyout: () => void;
|
||||
isLoading: boolean;
|
||||
mappings: CaseConnectorMapping[];
|
||||
onChangeConnector: (id: string) => void;
|
||||
selectedConnector: { id: string; type: string };
|
||||
updateConnectorDisabled: boolean;
|
||||
}
|
||||
const ConnectorsComponent: React.FC<Props> = ({
|
||||
connectors,
|
||||
isLoading,
|
||||
disabled,
|
||||
updateConnectorDisabled,
|
||||
handleShowEditFlyout,
|
||||
isLoading,
|
||||
mappings,
|
||||
onChangeConnector,
|
||||
selectedConnector,
|
||||
handleShowEditFlyout,
|
||||
updateConnectorDisabled,
|
||||
}) => {
|
||||
const connectorsName = useMemo(
|
||||
() => connectors.find((c) => c.id === selectedConnector)?.name ?? 'none',
|
||||
[connectors, selectedConnector]
|
||||
() => connectors.find((c) => c.id === selectedConnector.id)?.name ?? 'none',
|
||||
[connectors, selectedConnector.id]
|
||||
);
|
||||
|
||||
const dropDownLabel = useMemo(
|
||||
|
@ -68,10 +72,8 @@ const ConnectorsComponent: React.FC<Props> = ({
|
|||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[connectorsName, updateConnectorDisabled]
|
||||
[connectorsName, handleShowEditFlyout, updateConnectorDisabled]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiDescribedFormGroup
|
||||
|
@ -85,15 +87,28 @@ const ConnectorsComponent: React.FC<Props> = ({
|
|||
label={dropDownLabel}
|
||||
data-test-subj="case-connectors-form-row"
|
||||
>
|
||||
<ConnectorsDropdown
|
||||
connectors={connectors}
|
||||
disabled={disabled}
|
||||
selectedConnector={selectedConnector}
|
||||
isLoading={isLoading}
|
||||
onChange={onChangeConnector}
|
||||
data-test-subj="case-connectors-dropdown"
|
||||
appendAddConnectorButton={true}
|
||||
/>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem grow={false}>
|
||||
<ConnectorsDropdown
|
||||
connectors={connectors}
|
||||
disabled={disabled}
|
||||
selectedConnector={selectedConnector.id}
|
||||
isLoading={isLoading}
|
||||
onChange={onChangeConnector}
|
||||
data-test-subj="case-connectors-dropdown"
|
||||
appendAddConnectorButton={true}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{selectedConnector.type !== ConnectorTypes.none ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<Mapping
|
||||
connectorActionTypeId={selectedConnector.type}
|
||||
isLoading={isLoading}
|
||||
mappings={mappings}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
</EuiFormRowExtended>
|
||||
</EuiDescribedFormGroup>
|
||||
</>
|
||||
|
|
|
@ -7,77 +7,48 @@
|
|||
import React from 'react';
|
||||
import { mount, ReactWrapper } from 'enzyme';
|
||||
|
||||
import { connectorsConfiguration, createDefaultMapping } from '../connectors';
|
||||
|
||||
import { FieldMapping, FieldMappingProps } from './field_mapping';
|
||||
import { mapping } from './__mock__';
|
||||
import { FieldMappingRow } from './field_mapping_row';
|
||||
import { mappings } from './__mock__';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
import { FieldMappingRowStatic } from './field_mapping_row_static';
|
||||
|
||||
describe('FieldMappingRow', () => {
|
||||
let wrapper: ReactWrapper;
|
||||
const onChangeMapping = jest.fn();
|
||||
const props: FieldMappingProps = {
|
||||
disabled: false,
|
||||
mapping,
|
||||
onChangeMapping,
|
||||
isLoading: false,
|
||||
mappings,
|
||||
connectorActionTypeId: '.servicenow',
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
wrapper = mount(<FieldMapping {...props} />, { wrappingComponent: TestProviders });
|
||||
});
|
||||
|
||||
test('it renders', () => {
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="case-configure-field-mapping-cols"]').first().exists()
|
||||
wrapper.find('[data-test-subj="case-configure-field-mappings-row-wrapper"]').first().exists()
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="case-configure-field-mapping-row-wrapper"]').first().exists()
|
||||
).toBe(true);
|
||||
|
||||
expect(wrapper.find(FieldMappingRow).length).toEqual(3);
|
||||
expect(wrapper.find(FieldMappingRowStatic).length).toEqual(3);
|
||||
});
|
||||
|
||||
test('it shows the correct number of FieldMappingRow with default mapping', () => {
|
||||
const newWrapper = mount(<FieldMapping {...props} mapping={null} />, {
|
||||
test('it does not render without mappings', () => {
|
||||
const newWrapper = mount(<FieldMapping {...props} mappings={[]} />, {
|
||||
wrappingComponent: TestProviders,
|
||||
});
|
||||
|
||||
expect(newWrapper.find(FieldMappingRow).length).toEqual(3);
|
||||
expect(
|
||||
newWrapper
|
||||
.find('[data-test-subj="case-configure-field-mappings-row-wrapper"]')
|
||||
.first()
|
||||
.exists()
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('it pass the corrects props to mapping row', () => {
|
||||
const rows = wrapper.find(FieldMappingRow);
|
||||
const rows = wrapper.find(FieldMappingRowStatic);
|
||||
rows.forEach((row, index) => {
|
||||
expect(row.prop('securitySolutionField')).toEqual(mapping[index].source);
|
||||
expect(row.prop('selectedActionType')).toEqual(mapping[index].actionType);
|
||||
expect(row.prop('selectedThirdParty')).toEqual(mapping[index].target);
|
||||
expect(row.prop('securitySolutionField')).toEqual(mappings[index].source);
|
||||
expect(row.prop('selectedActionType')).toEqual(mappings[index].actionType);
|
||||
expect(row.prop('selectedThirdParty')).toEqual(mappings[index].target);
|
||||
});
|
||||
});
|
||||
|
||||
test('it pass the default mapping when mapping is null', () => {
|
||||
const newWrapper = mount(<FieldMapping {...props} mapping={null} />, {
|
||||
wrappingComponent: TestProviders,
|
||||
});
|
||||
|
||||
const selectedConnector = connectorsConfiguration['.servicenow'];
|
||||
const defaultMapping = createDefaultMapping(selectedConnector.fields);
|
||||
|
||||
const rows = newWrapper.find(FieldMappingRow);
|
||||
rows.forEach((row, index) => {
|
||||
expect(row.prop('securitySolutionField')).toEqual(defaultMapping[index].source);
|
||||
expect(row.prop('selectedActionType')).toEqual(defaultMapping[index].actionType);
|
||||
expect(row.prop('selectedThirdParty')).toEqual(defaultMapping[index].target);
|
||||
});
|
||||
});
|
||||
|
||||
test('it should show zero rows on empty array', () => {
|
||||
const newWrapper = mount(<FieldMapping {...props} mapping={[]} />, {
|
||||
wrappingComponent: TestProviders,
|
||||
});
|
||||
|
||||
expect(newWrapper.find(FieldMappingRow).length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,148 +4,69 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { EuiFormRow, EuiFlexItem, EuiFlexGroup, EuiSuperSelectOption } from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import {
|
||||
CasesConfigurationMapping,
|
||||
CaseField,
|
||||
ActionType,
|
||||
ThirdPartyField,
|
||||
} from '../../containers/configure/types';
|
||||
import {
|
||||
ThirdPartyField as ConnectorConfigurationThirdPartyField,
|
||||
AllThirdPartyFields,
|
||||
createDefaultMapping,
|
||||
connectorsConfiguration,
|
||||
} from '../connectors';
|
||||
|
||||
import { FieldMappingRow } from './field_mapping_row';
|
||||
import { FieldMappingRowStatic } from './field_mapping_row_static';
|
||||
import * as i18n from './translations';
|
||||
import { setActionTypeToMapping, setThirdPartyToMapping } from './utils';
|
||||
|
||||
import { CaseConnectorMapping } from '../../containers/configure/types';
|
||||
import { connectorsConfiguration } from '../connectors';
|
||||
|
||||
const FieldRowWrapper = styled.div`
|
||||
margin-top: 8px;
|
||||
margin: 10px 0;
|
||||
font-size: 14px;
|
||||
`;
|
||||
|
||||
const actionTypeOptions: Array<EuiSuperSelectOption<ActionType>> = [
|
||||
{
|
||||
value: 'nothing',
|
||||
inputDisplay: <>{i18n.FIELD_MAPPING_EDIT_NOTHING}</>,
|
||||
'data-test-subj': 'edit-update-option-nothing',
|
||||
},
|
||||
{
|
||||
value: 'overwrite',
|
||||
inputDisplay: <>{i18n.FIELD_MAPPING_EDIT_OVERWRITE}</>,
|
||||
'data-test-subj': 'edit-update-option-overwrite',
|
||||
},
|
||||
{
|
||||
value: 'append',
|
||||
inputDisplay: <>{i18n.FIELD_MAPPING_EDIT_APPEND}</>,
|
||||
'data-test-subj': 'edit-update-option-append',
|
||||
},
|
||||
];
|
||||
|
||||
const getThirdPartyOptions = (
|
||||
caseField: CaseField,
|
||||
thirdPartyFields: Record<string, ConnectorConfigurationThirdPartyField>
|
||||
): Array<EuiSuperSelectOption<AllThirdPartyFields>> =>
|
||||
(Object.keys(thirdPartyFields) as AllThirdPartyFields[]).reduce<
|
||||
Array<EuiSuperSelectOption<AllThirdPartyFields>>
|
||||
>(
|
||||
(acc, key) => {
|
||||
if (thirdPartyFields[key].validSourceFields.includes(caseField)) {
|
||||
return [
|
||||
...acc,
|
||||
{
|
||||
value: key,
|
||||
inputDisplay: <span>{thirdPartyFields[key].label}</span>,
|
||||
'data-test-subj': `dropdown-mapping-${key}`,
|
||||
},
|
||||
];
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[
|
||||
{
|
||||
value: 'not_mapped',
|
||||
inputDisplay: i18n.MAPPING_FIELD_NOT_MAPPED,
|
||||
'data-test-subj': 'dropdown-mapping-not_mapped',
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
export interface FieldMappingProps {
|
||||
disabled: boolean;
|
||||
mapping: CasesConfigurationMapping[] | null;
|
||||
connectorActionTypeId: string;
|
||||
onChangeMapping: (newMapping: CasesConfigurationMapping[]) => void;
|
||||
isLoading: boolean;
|
||||
mappings: CaseConnectorMapping[];
|
||||
}
|
||||
|
||||
const FieldMappingComponent: React.FC<FieldMappingProps> = ({
|
||||
disabled,
|
||||
mapping,
|
||||
onChangeMapping,
|
||||
connectorActionTypeId,
|
||||
isLoading,
|
||||
mappings,
|
||||
}) => {
|
||||
const onChangeActionType = useCallback(
|
||||
(caseField: CaseField, newActionType: ActionType) => {
|
||||
const myMapping = mapping ?? defaultMapping;
|
||||
onChangeMapping(setActionTypeToMapping(caseField, newActionType, myMapping));
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[mapping]
|
||||
const selectedConnector = useMemo(
|
||||
() => connectorsConfiguration[connectorActionTypeId] ?? { fields: {} },
|
||||
[connectorActionTypeId]
|
||||
);
|
||||
|
||||
const onChangeThirdParty = useCallback(
|
||||
(caseField: CaseField, newThirdPartyField: ThirdPartyField) => {
|
||||
const myMapping = mapping ?? defaultMapping;
|
||||
onChangeMapping(setThirdPartyToMapping(caseField, newThirdPartyField, myMapping));
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[mapping]
|
||||
);
|
||||
|
||||
const selectedConnector = connectorsConfiguration[connectorActionTypeId] ?? { fields: {} };
|
||||
const defaultMapping = useMemo(() => createDefaultMapping(selectedConnector.fields), [
|
||||
selectedConnector.fields,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFormRow fullWidth data-test-subj="case-configure-field-mapping-cols">
|
||||
return mappings.length ? (
|
||||
<EuiFlexGroup direction="column" gutterSize="none">
|
||||
<EuiFlexItem>
|
||||
{' '}
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<span className="euiFormLabel">{i18n.FIELD_MAPPING_FIRST_COL}</span>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<span className="euiFormLabel">{i18n.FIELD_MAPPING_SECOND_COL}</span>
|
||||
<span className="euiFormLabel">
|
||||
{i18n.FIELD_MAPPING_SECOND_COL(selectedConnector.name)}
|
||||
</span>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<span className="euiFormLabel">{i18n.FIELD_MAPPING_THIRD_COL}</span>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFormRow>
|
||||
<FieldRowWrapper data-test-subj="case-configure-field-mapping-row-wrapper">
|
||||
{(mapping ?? defaultMapping).map((item) => (
|
||||
<FieldMappingRow
|
||||
key={`${item.source}`}
|
||||
id={`${item.source}`}
|
||||
disabled={disabled}
|
||||
securitySolutionField={item.source}
|
||||
thirdPartyOptions={getThirdPartyOptions(item.source, selectedConnector.fields)}
|
||||
actionTypeOptions={actionTypeOptions}
|
||||
onChangeActionType={onChangeActionType}
|
||||
onChangeThirdParty={onChangeThirdParty}
|
||||
selectedActionType={item.actionType}
|
||||
selectedThirdParty={item.target ?? 'not_mapped'}
|
||||
/>
|
||||
))}
|
||||
</FieldRowWrapper>
|
||||
</>
|
||||
);
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<FieldRowWrapper data-test-subj="case-configure-field-mappings-row-wrapper">
|
||||
{mappings.map((item) => (
|
||||
<FieldMappingRowStatic
|
||||
key={`${item.source}`}
|
||||
securitySolutionField={item.source}
|
||||
isLoading={isLoading}
|
||||
selectedActionType={item.actionType}
|
||||
selectedThirdParty={item.target ?? 'not_mapped'}
|
||||
/>
|
||||
))}
|
||||
</FieldRowWrapper>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export const FieldMapping = React.memo(FieldMappingComponent);
|
||||
|
|
|
@ -1,114 +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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { mount, ReactWrapper } from 'enzyme';
|
||||
import { EuiSuperSelectOption, EuiSuperSelect } from '@elastic/eui';
|
||||
|
||||
import { FieldMappingRow, RowProps } from './field_mapping_row';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
import { ThirdPartyField, ActionType } from '../../containers/configure/types';
|
||||
|
||||
const thirdPartyOptions: Array<EuiSuperSelectOption<ThirdPartyField>> = [
|
||||
{
|
||||
value: 'short_description',
|
||||
inputDisplay: <span>{'Short Description'}</span>,
|
||||
'data-test-subj': 'third-party-short-desc',
|
||||
},
|
||||
{
|
||||
value: 'description',
|
||||
inputDisplay: <span>{'Description'}</span>,
|
||||
'data-test-subj': 'third-party-desc',
|
||||
},
|
||||
];
|
||||
|
||||
const actionTypeOptions: Array<EuiSuperSelectOption<ActionType>> = [
|
||||
{
|
||||
value: 'nothing',
|
||||
inputDisplay: <>{'Nothing'}</>,
|
||||
'data-test-subj': 'edit-update-option-nothing',
|
||||
},
|
||||
{
|
||||
value: 'overwrite',
|
||||
inputDisplay: <>{'Overwrite'}</>,
|
||||
'data-test-subj': 'edit-update-option-overwrite',
|
||||
},
|
||||
{
|
||||
value: 'append',
|
||||
inputDisplay: <>{'Append'}</>,
|
||||
'data-test-subj': 'edit-update-option-append',
|
||||
},
|
||||
];
|
||||
|
||||
describe('FieldMappingRow', () => {
|
||||
let wrapper: ReactWrapper;
|
||||
const onChangeActionType = jest.fn();
|
||||
const onChangeThirdParty = jest.fn();
|
||||
|
||||
const props: RowProps = {
|
||||
id: 'title',
|
||||
disabled: false,
|
||||
securitySolutionField: 'title',
|
||||
thirdPartyOptions,
|
||||
actionTypeOptions,
|
||||
onChangeActionType,
|
||||
onChangeThirdParty,
|
||||
selectedActionType: 'nothing',
|
||||
selectedThirdParty: 'short_description',
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
wrapper = mount(<FieldMappingRow {...props} />, { wrappingComponent: TestProviders });
|
||||
});
|
||||
|
||||
test('it renders', () => {
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="case-configure-third-party-select-title"]').first().exists()
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="case-configure-action-type-select-title"]').first().exists()
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('it passes thirdPartyOptions correctly', () => {
|
||||
const selectProps = wrapper.find(EuiSuperSelect).first().props();
|
||||
|
||||
expect(selectProps.options).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
value: 'short_description',
|
||||
'data-test-subj': 'third-party-short-desc',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
value: 'description',
|
||||
'data-test-subj': 'third-party-desc',
|
||||
}),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
test('it passes the correct actionTypeOptions', () => {
|
||||
const selectProps = wrapper.find(EuiSuperSelect).at(1).props();
|
||||
|
||||
expect(selectProps.options).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
value: 'nothing',
|
||||
'data-test-subj': 'edit-update-option-nothing',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
value: 'overwrite',
|
||||
'data-test-subj': 'edit-update-option-overwrite',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
value: 'append',
|
||||
'data-test-subj': 'edit-update-option-append',
|
||||
}),
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
|
@ -1,80 +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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import {
|
||||
EuiFlexItem,
|
||||
EuiFlexGroup,
|
||||
EuiSuperSelect,
|
||||
EuiIcon,
|
||||
EuiSuperSelectOption,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { capitalize } from 'lodash/fp';
|
||||
import { CaseField, ActionType, ThirdPartyField } from '../../containers/configure/types';
|
||||
import { AllThirdPartyFields } from '../connectors';
|
||||
|
||||
export interface RowProps {
|
||||
id: string;
|
||||
disabled: boolean;
|
||||
securitySolutionField: CaseField;
|
||||
thirdPartyOptions: Array<EuiSuperSelectOption<AllThirdPartyFields>>;
|
||||
actionTypeOptions: Array<EuiSuperSelectOption<ActionType>>;
|
||||
onChangeActionType: (caseField: CaseField, newActionType: ActionType) => void;
|
||||
onChangeThirdParty: (caseField: CaseField, newThirdPartyField: ThirdPartyField) => void;
|
||||
selectedActionType: ActionType;
|
||||
selectedThirdParty: ThirdPartyField;
|
||||
}
|
||||
|
||||
const FieldMappingRowComponent: React.FC<RowProps> = ({
|
||||
id,
|
||||
disabled,
|
||||
securitySolutionField,
|
||||
thirdPartyOptions,
|
||||
actionTypeOptions,
|
||||
onChangeActionType,
|
||||
onChangeThirdParty,
|
||||
selectedActionType,
|
||||
selectedThirdParty,
|
||||
}) => {
|
||||
const securitySolutionFieldCapitalized = useMemo(() => capitalize(securitySolutionField), [
|
||||
securitySolutionField,
|
||||
]);
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup component="span" justifyContent="spaceBetween">
|
||||
<EuiFlexItem component="span" grow={false}>
|
||||
{securitySolutionFieldCapitalized}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem component="span" grow={false}>
|
||||
<EuiIcon type="sortRight" />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiSuperSelect
|
||||
disabled={disabled}
|
||||
options={thirdPartyOptions}
|
||||
valueOfSelected={selectedThirdParty}
|
||||
onChange={onChangeThirdParty.bind(null, securitySolutionField)}
|
||||
data-test-subj={`case-configure-third-party-select-${id}`}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiSuperSelect
|
||||
disabled={disabled}
|
||||
options={actionTypeOptions}
|
||||
valueOfSelected={selectedActionType}
|
||||
onChange={onChangeActionType.bind(null, securitySolutionField)}
|
||||
data-test-subj={`case-configure-action-type-select-${id}`}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export const FieldMappingRow = React.memo(FieldMappingRowComponent);
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue