mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Cases] Improve connectors mapping (#101145)
This commit is contained in:
parent
0f5620e5c4
commit
144e014dbf
39 changed files with 516 additions and 1121 deletions
|
@ -38,6 +38,8 @@ export enum ConnectorTypes {
|
|||
none = '.none',
|
||||
}
|
||||
|
||||
export const connectorTypes = Object.values(ConnectorTypes);
|
||||
|
||||
const ConnectorJiraTypeFieldsRt = rt.type({
|
||||
type: rt.literal(ConnectorTypes.jira),
|
||||
fields: rt.union([JiraFieldsRT, rt.null]),
|
||||
|
|
|
@ -50,7 +50,7 @@ describe('Mapping', () => {
|
|||
wrappingComponent: TestProviders,
|
||||
});
|
||||
expect(wrapper.find('[data-test-subj="field-mapping-desc"]').first().text()).toBe(
|
||||
'Field mappings require an established connection to ServiceNow ITSM. Please check your connection credentials.'
|
||||
'Failed to retrieve mappings for ServiceNow ITSM.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -102,8 +102,7 @@ export const FIELD_MAPPING_DESC = (thirdPartyName: string): string => {
|
|||
export const FIELD_MAPPING_DESC_ERR = (thirdPartyName: string): string => {
|
||||
return i18n.translate('xpack.cases.configureCases.fieldMappingDescErr', {
|
||||
values: { thirdPartyName },
|
||||
defaultMessage:
|
||||
'Field mappings require an established connection to { thirdPartyName }. Please check your connection credentials.',
|
||||
defaultMessage: 'Failed to retrieve mappings for { thirdPartyName }.',
|
||||
});
|
||||
};
|
||||
export const EDIT_FIELD_MAPPING_TITLE = (thirdPartyName: string): string => {
|
||||
|
|
|
@ -25,6 +25,7 @@ import { createCaseError, flattenCaseSavedObject, getAlertInfoFromComments } fro
|
|||
import { ENABLE_CASE_CONNECTOR } from '../../../common/constants';
|
||||
import { CasesClient, CasesClientArgs, CasesClientInternal } from '..';
|
||||
import { Operations } from '../../authorization';
|
||||
import { casesConnectors } from '../../connectors';
|
||||
|
||||
/**
|
||||
* Returns true if the case should be closed based on the configuration settings and whether the case
|
||||
|
@ -110,8 +111,7 @@ export const push = async (
|
|||
});
|
||||
|
||||
const connectorMappings = await casesClientInternal.configuration.getMappings({
|
||||
connectorId: connector.id,
|
||||
connectorType: connector.actionTypeId,
|
||||
connector: theCase.connector,
|
||||
});
|
||||
|
||||
if (connectorMappings.length === 0) {
|
||||
|
@ -125,6 +125,7 @@ export const push = async (
|
|||
connector: connector as ActionConnector,
|
||||
mappings: connectorMappings[0].attributes.mappings,
|
||||
alerts,
|
||||
casesConnectors,
|
||||
});
|
||||
|
||||
const pushRes = await actionsClient.execute({
|
||||
|
|
|
@ -30,6 +30,7 @@ import {
|
|||
} from './utils';
|
||||
import { flattenCaseSavedObject } from '../../common';
|
||||
import { SECURITY_SOLUTION_OWNER } from '../../../common';
|
||||
import { casesConnectors } from '../../connectors';
|
||||
|
||||
const formatComment = {
|
||||
commentId: commentObj.id,
|
||||
|
@ -443,6 +444,7 @@ describe('utils', () => {
|
|||
connector,
|
||||
mappings,
|
||||
alerts: [],
|
||||
casesConnectors,
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
|
@ -471,6 +473,7 @@ describe('utils', () => {
|
|||
connector,
|
||||
mappings,
|
||||
alerts: [],
|
||||
casesConnectors,
|
||||
});
|
||||
|
||||
expect(res.comments).toEqual([
|
||||
|
@ -501,6 +504,7 @@ describe('utils', () => {
|
|||
},
|
||||
],
|
||||
alerts: [],
|
||||
casesConnectors,
|
||||
});
|
||||
|
||||
expect(res.comments).toEqual([]);
|
||||
|
@ -531,6 +535,7 @@ describe('utils', () => {
|
|||
},
|
||||
],
|
||||
alerts: [],
|
||||
casesConnectors,
|
||||
});
|
||||
|
||||
expect(res.comments).toEqual([
|
||||
|
@ -561,6 +566,7 @@ describe('utils', () => {
|
|||
connector,
|
||||
mappings,
|
||||
alerts: [],
|
||||
casesConnectors,
|
||||
});
|
||||
|
||||
expect(res.comments).toEqual([
|
||||
|
@ -595,6 +601,7 @@ describe('utils', () => {
|
|||
connector,
|
||||
mappings,
|
||||
alerts: [],
|
||||
casesConnectors,
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
|
@ -626,6 +633,7 @@ describe('utils', () => {
|
|||
connector,
|
||||
mappings,
|
||||
alerts: [],
|
||||
casesConnectors,
|
||||
}).catch((e) => {
|
||||
expect(e).not.toBeNull();
|
||||
expect(e).toEqual(
|
||||
|
@ -645,6 +653,7 @@ describe('utils', () => {
|
|||
connector: { ...connector, actionTypeId: 'not-supported' },
|
||||
mappings,
|
||||
alerts: [],
|
||||
casesConnectors,
|
||||
}).catch((e) => {
|
||||
expect(e).not.toBeNull();
|
||||
expect(e).toEqual(new Error('Invalid external service'));
|
||||
|
|
|
@ -12,17 +12,15 @@ import {
|
|||
CaseFullExternalService,
|
||||
CaseResponse,
|
||||
CaseUserActionsResponse,
|
||||
CommentAttributes,
|
||||
CommentRequestAlertType,
|
||||
CommentRequestUserType,
|
||||
CommentResponse,
|
||||
CommentResponseAlertsType,
|
||||
CommentType,
|
||||
ConnectorMappingsAttributes,
|
||||
ConnectorTypes,
|
||||
CommentAttributes,
|
||||
CommentRequestUserType,
|
||||
CommentRequestAlertType,
|
||||
} from '../../../common';
|
||||
import { ActionsClient } from '../../../../actions/server';
|
||||
import { externalServiceFormatters, FormatterConnectorTypes } from '../../connectors';
|
||||
import { CasesClientGetAlertsResponse } from '../../client/alerts/types';
|
||||
import {
|
||||
BasicParams,
|
||||
|
@ -39,6 +37,7 @@ import {
|
|||
TransformFieldsArgs,
|
||||
} from './types';
|
||||
import { getAlertIds } from '../utils';
|
||||
import { CasesConnectorsMap } from '../../connectors';
|
||||
|
||||
interface CreateIncidentArgs {
|
||||
actionsClient: ActionsClient;
|
||||
|
@ -47,6 +46,7 @@ interface CreateIncidentArgs {
|
|||
connector: ActionConnector;
|
||||
mappings: ConnectorMappingsAttributes[];
|
||||
alerts: CasesClientGetAlertsResponse;
|
||||
casesConnectors: CasesConnectorsMap;
|
||||
}
|
||||
|
||||
export const getLatestPushInfo = (
|
||||
|
@ -70,9 +70,6 @@ export const getLatestPushInfo = (
|
|||
return null;
|
||||
};
|
||||
|
||||
const isConnectorSupported = (connectorId: string): connectorId is FormatterConnectorTypes =>
|
||||
Object.values(ConnectorTypes).includes(connectorId as ConnectorTypes);
|
||||
|
||||
const getCommentContent = (comment: CommentResponse): string => {
|
||||
if (comment.type === CommentType.user) {
|
||||
return comment.comment;
|
||||
|
@ -99,6 +96,7 @@ export const createIncident = async ({
|
|||
connector,
|
||||
mappings,
|
||||
alerts,
|
||||
casesConnectors,
|
||||
}: CreateIncidentArgs): Promise<MapIncident> => {
|
||||
const {
|
||||
comments: caseComments,
|
||||
|
@ -110,20 +108,15 @@ export const createIncident = async ({
|
|||
updated_by: updatedBy,
|
||||
} = theCase;
|
||||
|
||||
if (!isConnectorSupported(connector.actionTypeId)) {
|
||||
throw new Error('Invalid external service');
|
||||
}
|
||||
|
||||
const params = { title, description, createdAt, createdBy, updatedAt, updatedBy };
|
||||
const latestPushInfo = getLatestPushInfo(connector.id, userActions);
|
||||
const externalId = latestPushInfo?.pushedInfo?.external_id ?? null;
|
||||
const defaultPipes = externalId ? ['informationUpdated'] : ['informationCreated'];
|
||||
let currentIncident: ExternalServiceParams | undefined;
|
||||
|
||||
const externalServiceFields = externalServiceFormatters[connector.actionTypeId].format(
|
||||
theCase,
|
||||
alerts
|
||||
);
|
||||
const externalServiceFields =
|
||||
casesConnectors.get(connector.actionTypeId)?.format(theCase, alerts) ?? {};
|
||||
|
||||
let incident: Partial<PushToServiceApiParams['incident']> = { ...externalServiceFields };
|
||||
|
||||
if (externalId) {
|
||||
|
|
|
@ -26,7 +26,6 @@ import {
|
|||
excess,
|
||||
GetConfigureFindRequest,
|
||||
GetConfigureFindRequestRt,
|
||||
GetFieldsResponse,
|
||||
throwErrors,
|
||||
CasesConfigurationsResponse,
|
||||
CaseConfigurationsResponseRt,
|
||||
|
@ -41,7 +40,6 @@ import {
|
|||
} from '../../common';
|
||||
import { CasesClientInternal } from '../client_internal';
|
||||
import { CasesClientArgs } from '../types';
|
||||
import { getFields } from './get_fields';
|
||||
import { getMappings } from './get_mappings';
|
||||
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
|
@ -49,12 +47,7 @@ import { FindActionResult } from '../../../../actions/server/types';
|
|||
import { ActionType } from '../../../../actions/common';
|
||||
import { Operations } from '../../authorization';
|
||||
import { combineAuthorizedAndOwnerFilter } from '../utils';
|
||||
import {
|
||||
ConfigurationGetFields,
|
||||
MappingsArgs,
|
||||
CreateMappingsArgs,
|
||||
UpdateMappingsArgs,
|
||||
} from './types';
|
||||
import { MappingsArgs, CreateMappingsArgs, UpdateMappingsArgs } from './types';
|
||||
import { createMappings } from './create_mappings';
|
||||
import { updateMappings } from './update_mappings';
|
||||
import {
|
||||
|
@ -69,7 +62,6 @@ import {
|
|||
* @ignore
|
||||
*/
|
||||
export interface InternalConfigureSubClient {
|
||||
getFields(params: ConfigurationGetFields): Promise<GetFieldsResponse>;
|
||||
getMappings(
|
||||
params: MappingsArgs
|
||||
): Promise<SavedObjectsFindResponse<ConnectorMappings>['saved_objects']>;
|
||||
|
@ -116,12 +108,9 @@ export const createInternalConfigurationSubClient = (
|
|||
casesClientInternal: CasesClientInternal
|
||||
): InternalConfigureSubClient => {
|
||||
const configureSubClient: InternalConfigureSubClient = {
|
||||
getFields: (params: ConfigurationGetFields) => getFields(params, clientArgs),
|
||||
getMappings: (params: MappingsArgs) => getMappings(params, clientArgs),
|
||||
createMappings: (params: CreateMappingsArgs) =>
|
||||
createMappings(params, clientArgs, casesClientInternal),
|
||||
updateMappings: (params: UpdateMappingsArgs) =>
|
||||
updateMappings(params, clientArgs, casesClientInternal),
|
||||
createMappings: (params: CreateMappingsArgs) => createMappings(params, clientArgs),
|
||||
updateMappings: (params: UpdateMappingsArgs) => updateMappings(params, clientArgs),
|
||||
};
|
||||
|
||||
return Object.freeze(configureSubClient);
|
||||
|
@ -194,8 +183,7 @@ async function get(
|
|||
if (connector != null) {
|
||||
try {
|
||||
mappings = await casesClientInternal.configuration.getMappings({
|
||||
connectorId: connector.id,
|
||||
connectorType: connector.type,
|
||||
connector: transformESConnectorToCaseConnector(connector),
|
||||
});
|
||||
} catch (e) {
|
||||
error = e.isBoom
|
||||
|
@ -303,22 +291,22 @@ async function update(
|
|||
|
||||
try {
|
||||
const resMappings = await casesClientInternal.configuration.getMappings({
|
||||
connectorId: connector != null ? connector.id : configuration.attributes.connector.id,
|
||||
connectorType: connector != null ? connector.type : configuration.attributes.connector.type,
|
||||
connector:
|
||||
connector != null
|
||||
? connector
|
||||
: transformESConnectorToCaseConnector(configuration.attributes.connector),
|
||||
});
|
||||
mappings = resMappings.length > 0 ? resMappings[0].attributes.mappings : [];
|
||||
|
||||
if (connector != null) {
|
||||
if (resMappings.length !== 0) {
|
||||
mappings = await casesClientInternal.configuration.updateMappings({
|
||||
connectorId: connector.id,
|
||||
connectorType: connector.type,
|
||||
connector,
|
||||
mappingId: resMappings[0].id,
|
||||
});
|
||||
} else {
|
||||
mappings = await casesClientInternal.configuration.createMappings({
|
||||
connectorId: connector.id,
|
||||
connectorType: connector.type,
|
||||
connector,
|
||||
owner: configuration.attributes.owner,
|
||||
});
|
||||
}
|
||||
|
@ -326,9 +314,9 @@ async function update(
|
|||
} catch (e) {
|
||||
error = e.isBoom
|
||||
? e.output.payload.message
|
||||
: `Error connecting to ${
|
||||
: `Error creating mapping for ${
|
||||
connector != null ? connector.name : configuration.attributes.connector.name
|
||||
} instance`;
|
||||
}`;
|
||||
}
|
||||
|
||||
const patch = await caseConfigureService.patch({
|
||||
|
@ -429,14 +417,13 @@ async function create(
|
|||
|
||||
try {
|
||||
mappings = await casesClientInternal.configuration.createMappings({
|
||||
connectorId: configuration.connector.id,
|
||||
connectorType: configuration.connector.type,
|
||||
connector: configuration.connector,
|
||||
owner: configuration.owner,
|
||||
});
|
||||
} catch (e) {
|
||||
error = e.isBoom
|
||||
? e.output.payload.message
|
||||
: `Error connecting to ${configuration.connector.name} instance`;
|
||||
: `Error creating mapping for ${configuration.connector.name}`;
|
||||
}
|
||||
|
||||
const post = await caseConfigureService.post({
|
||||
|
|
|
@ -5,40 +5,33 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ConnectorMappingsAttributes, ConnectorTypes } from '../../../common/api';
|
||||
import { ConnectorMappingsAttributes } from '../../../common/api';
|
||||
import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server';
|
||||
import { createCaseError } from '../../common/error';
|
||||
import { CasesClientArgs, CasesClientInternal } from '..';
|
||||
import { CasesClientArgs } from '..';
|
||||
import { CreateMappingsArgs } from './types';
|
||||
import { casesConnectors } from '../../connectors';
|
||||
|
||||
export const createMappings = async (
|
||||
{ connectorType, connectorId, owner }: CreateMappingsArgs,
|
||||
clientArgs: CasesClientArgs,
|
||||
casesClientInternal: CasesClientInternal
|
||||
{ connector, owner }: CreateMappingsArgs,
|
||||
clientArgs: CasesClientArgs
|
||||
): Promise<ConnectorMappingsAttributes[]> => {
|
||||
const { unsecuredSavedObjectsClient, connectorMappingsService, logger } = clientArgs;
|
||||
|
||||
try {
|
||||
if (connectorType === ConnectorTypes.none) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const res = await casesClientInternal.configuration.getFields({
|
||||
connectorId,
|
||||
connectorType,
|
||||
});
|
||||
const mappings = casesConnectors.get(connector.type)?.getMapping() ?? [];
|
||||
|
||||
const theMapping = await connectorMappingsService.post({
|
||||
unsecuredSavedObjectsClient,
|
||||
attributes: {
|
||||
mappings: res.defaultMappings,
|
||||
mappings,
|
||||
owner,
|
||||
},
|
||||
references: [
|
||||
{
|
||||
type: ACTION_SAVED_OBJECT_TYPE,
|
||||
name: `associated-${ACTION_SAVED_OBJECT_TYPE}`,
|
||||
id: connectorId,
|
||||
id: connector.id,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -46,7 +39,7 @@ export const createMappings = async (
|
|||
return theMapping.attributes.mappings;
|
||||
} catch (error) {
|
||||
throw createCaseError({
|
||||
message: `Failed to create mapping connector id: ${connectorId} type: ${connectorType}: ${error}`,
|
||||
message: `Failed to create mapping connector id: ${connector.id} type: ${connector.type}: ${error}`,
|
||||
error,
|
||||
logger,
|
||||
});
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import Boom from '@hapi/boom';
|
||||
|
||||
import { GetFieldsResponse } from '../../../common/api';
|
||||
import { createDefaultMapping, formatFields } from './utils';
|
||||
import { CasesClientArgs } from '..';
|
||||
|
||||
interface ConfigurationGetFields {
|
||||
connectorId: string;
|
||||
connectorType: string;
|
||||
}
|
||||
|
||||
export const getFields = async (
|
||||
{ connectorType, connectorId }: ConfigurationGetFields,
|
||||
clientArgs: CasesClientArgs
|
||||
): Promise<GetFieldsResponse> => {
|
||||
const { actionsClient } = clientArgs;
|
||||
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) };
|
||||
};
|
|
@ -6,29 +6,25 @@
|
|||
*/
|
||||
|
||||
import { SavedObjectsFindResponse } from 'kibana/server';
|
||||
import { ConnectorMappings, ConnectorTypes } from '../../../common/api';
|
||||
import { ConnectorMappings } from '../../../common/api';
|
||||
import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server';
|
||||
import { createCaseError } from '../../common/error';
|
||||
import { CasesClientArgs } from '..';
|
||||
import { MappingsArgs } from './types';
|
||||
|
||||
export const getMappings = async (
|
||||
{ connectorType, connectorId }: MappingsArgs,
|
||||
{ connector }: MappingsArgs,
|
||||
clientArgs: CasesClientArgs
|
||||
): Promise<SavedObjectsFindResponse<ConnectorMappings>['saved_objects']> => {
|
||||
const { unsecuredSavedObjectsClient, connectorMappingsService, logger } = clientArgs;
|
||||
|
||||
try {
|
||||
if (connectorType === ConnectorTypes.none) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const myConnectorMappings = await connectorMappingsService.find({
|
||||
unsecuredSavedObjectsClient,
|
||||
options: {
|
||||
hasReference: {
|
||||
type: ACTION_SAVED_OBJECT_TYPE,
|
||||
id: connectorId,
|
||||
id: connector.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -36,7 +32,7 @@ export const getMappings = async (
|
|||
return myConnectorMappings.saved_objects;
|
||||
} catch (error) {
|
||||
throw createCaseError({
|
||||
message: `Failed to retrieve mapping connector id: ${connectorId} type: ${connectorType}: ${error}`,
|
||||
message: `Failed to retrieve mapping connector id: ${connector.id} type: ${connector.type}: ${error}`,
|
||||
error,
|
||||
logger,
|
||||
});
|
||||
|
|
|
@ -1,657 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ConnectorField, ConnectorMappingsAttributes, ConnectorTypes } from '../../../common';
|
||||
import {
|
||||
JiraGetFieldsResponse,
|
||||
ResilientGetFieldsResponse,
|
||||
ServiceNowGetFieldsResponse,
|
||||
} from './utils.test';
|
||||
interface TestMappings {
|
||||
[key: string]: ConnectorMappingsAttributes[];
|
||||
}
|
||||
export const mappings: TestMappings = {
|
||||
[ConnectorTypes.jira]: [
|
||||
{
|
||||
source: 'title',
|
||||
target: 'summary',
|
||||
action_type: 'overwrite',
|
||||
},
|
||||
{
|
||||
source: 'description',
|
||||
target: 'description',
|
||||
action_type: 'overwrite',
|
||||
},
|
||||
{
|
||||
source: 'comments',
|
||||
target: 'comments',
|
||||
action_type: 'append',
|
||||
},
|
||||
],
|
||||
[`${ConnectorTypes.jira}-alt`]: [
|
||||
{
|
||||
source: 'title',
|
||||
target: 'title',
|
||||
action_type: 'overwrite',
|
||||
},
|
||||
{
|
||||
source: 'description',
|
||||
target: 'description',
|
||||
action_type: 'overwrite',
|
||||
},
|
||||
{
|
||||
source: 'comments',
|
||||
target: 'comments',
|
||||
action_type: 'append',
|
||||
},
|
||||
],
|
||||
[ConnectorTypes.resilient]: [
|
||||
{
|
||||
source: 'title',
|
||||
target: 'name',
|
||||
action_type: 'overwrite',
|
||||
},
|
||||
{
|
||||
source: 'description',
|
||||
target: 'description',
|
||||
action_type: 'overwrite',
|
||||
},
|
||||
{
|
||||
source: 'comments',
|
||||
target: 'comments',
|
||||
action_type: 'append',
|
||||
},
|
||||
],
|
||||
[ConnectorTypes.serviceNowITSM]: [
|
||||
{
|
||||
source: 'title',
|
||||
target: 'short_description',
|
||||
action_type: 'overwrite',
|
||||
},
|
||||
{
|
||||
source: 'description',
|
||||
target: 'description',
|
||||
action_type: 'overwrite',
|
||||
},
|
||||
{
|
||||
source: 'comments',
|
||||
target: 'work_notes',
|
||||
action_type: 'append',
|
||||
},
|
||||
],
|
||||
[ConnectorTypes.serviceNowSIR]: [
|
||||
{
|
||||
source: 'title',
|
||||
target: 'short_description',
|
||||
action_type: 'overwrite',
|
||||
},
|
||||
{
|
||||
source: 'description',
|
||||
target: 'description',
|
||||
action_type: 'overwrite',
|
||||
},
|
||||
{
|
||||
source: 'comments',
|
||||
target: 'work_notes',
|
||||
action_type: 'append',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
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',
|
||||
},
|
||||
];
|
||||
interface FormatFieldsTestData {
|
||||
expected: ConnectorField[];
|
||||
fields: JiraGetFieldsResponse | ResilientGetFieldsResponse | ServiceNowGetFieldsResponse;
|
||||
type: ConnectorTypes;
|
||||
}
|
||||
export const formatFieldsTestData: 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.serviceNowITSM,
|
||||
},
|
||||
{
|
||||
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.serviceNowSIR,
|
||||
},
|
||||
];
|
||||
export const mockGetFieldsResponse = {
|
||||
status: 'ok',
|
||||
data: jiraFields,
|
||||
actionId: '123',
|
||||
};
|
||||
|
||||
export const actionsErrResponse = {
|
||||
status: 'error',
|
||||
serviceMessage: 'this is an actions error',
|
||||
};
|
|
@ -5,9 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { CaseConnector } from '../../../common';
|
||||
|
||||
export interface MappingsArgs {
|
||||
connectorType: string;
|
||||
connectorId: string;
|
||||
connector: CaseConnector;
|
||||
}
|
||||
|
||||
export interface CreateMappingsArgs extends MappingsArgs {
|
||||
|
@ -17,8 +18,3 @@ export interface CreateMappingsArgs extends MappingsArgs {
|
|||
export interface UpdateMappingsArgs extends MappingsArgs {
|
||||
mappingId: string;
|
||||
}
|
||||
|
||||
export interface ConfigurationGetFields {
|
||||
connectorId: string;
|
||||
connectorType: string;
|
||||
}
|
||||
|
|
|
@ -5,40 +5,33 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ConnectorMappingsAttributes, ConnectorTypes } from '../../../common/api';
|
||||
import { ConnectorMappingsAttributes } from '../../../common/api';
|
||||
import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server';
|
||||
import { createCaseError } from '../../common/error';
|
||||
import { CasesClientArgs, CasesClientInternal } from '..';
|
||||
import { CasesClientArgs } from '..';
|
||||
import { UpdateMappingsArgs } from './types';
|
||||
import { casesConnectors } from '../../connectors';
|
||||
|
||||
export const updateMappings = async (
|
||||
{ connectorType, connectorId, mappingId }: UpdateMappingsArgs,
|
||||
clientArgs: CasesClientArgs,
|
||||
casesClientInternal: CasesClientInternal
|
||||
{ connector, mappingId }: UpdateMappingsArgs,
|
||||
clientArgs: CasesClientArgs
|
||||
): Promise<ConnectorMappingsAttributes[]> => {
|
||||
const { unsecuredSavedObjectsClient, connectorMappingsService, logger } = clientArgs;
|
||||
|
||||
try {
|
||||
if (connectorType === ConnectorTypes.none) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const res = await casesClientInternal.configuration.getFields({
|
||||
connectorId,
|
||||
connectorType,
|
||||
});
|
||||
const mappings = casesConnectors.get(connector.type)?.getMapping() ?? [];
|
||||
|
||||
const theMapping = await connectorMappingsService.update({
|
||||
unsecuredSavedObjectsClient,
|
||||
mappingId,
|
||||
attributes: {
|
||||
mappings: res.defaultMappings,
|
||||
mappings,
|
||||
},
|
||||
references: [
|
||||
{
|
||||
type: ACTION_SAVED_OBJECT_TYPE,
|
||||
name: `associated-${ACTION_SAVED_OBJECT_TYPE}`,
|
||||
id: connectorId,
|
||||
id: connector.id,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -46,7 +39,7 @@ export const updateMappings = async (
|
|||
return theMapping.attributes.mappings ?? [];
|
||||
} catch (error) {
|
||||
throw createCaseError({
|
||||
message: `Failed to create mapping connector id: ${connectorId} type: ${connectorType}: ${error}`,
|
||||
message: `Failed to create mapping connector id: ${connector.id} type: ${connector.type}: ${error}`,
|
||||
error,
|
||||
logger,
|
||||
});
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export {
|
||||
JiraGetFieldsResponse,
|
||||
ResilientGetFieldsResponse,
|
||||
ServiceNowGetFieldsResponse,
|
||||
} from '../../../../actions/server/types';
|
||||
import { createDefaultMapping, formatFields } from './utils';
|
||||
import { mappings, formatFieldsTestData } from './mock';
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('createDefaultMapping', () => {
|
||||
formatFieldsTestData.forEach(({ expected, fields, type }) => {
|
||||
it(`normalizes ${type} fields to common type ConnectorField`, () => {
|
||||
const result = createDefaultMapping(expected, type);
|
||||
expect(result).toEqual(mappings[type]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,125 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ConnectorField, ConnectorMappingsAttributes, ConnectorTypes } from '../../../common';
|
||||
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.serviceNowITSM:
|
||||
return normalizeServiceNowFields(theData as ServiceNowGetFieldsResponse);
|
||||
case ConnectorTypes.serviceNowSIR:
|
||||
return normalizeServiceNowFields(theData as ServiceNowGetFieldsResponse);
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const getPreferredFields = (theType: string) => {
|
||||
let title: string = '';
|
||||
let description: string = '';
|
||||
let comments: string = '';
|
||||
|
||||
if (theType === ConnectorTypes.jira) {
|
||||
title = 'summary';
|
||||
description = 'description';
|
||||
comments = 'comments';
|
||||
} else if (theType === ConnectorTypes.resilient) {
|
||||
title = 'name';
|
||||
description = 'description';
|
||||
comments = 'comments';
|
||||
} else if (
|
||||
theType === ConnectorTypes.serviceNowITSM ||
|
||||
theType === ConnectorTypes.serviceNowSIR
|
||||
) {
|
||||
title = 'short_description';
|
||||
description = 'description';
|
||||
comments = 'work_notes';
|
||||
}
|
||||
|
||||
return { title, description, comments };
|
||||
};
|
||||
|
||||
export const createDefaultMapping = (
|
||||
fields: ConnectorField[],
|
||||
theType: string
|
||||
): ConnectorMappingsAttributes[] => {
|
||||
const { description, title, comments } = getPreferredFields(theType);
|
||||
return [
|
||||
{
|
||||
source: 'title',
|
||||
target: title,
|
||||
action_type: 'overwrite',
|
||||
},
|
||||
{
|
||||
source: 'description',
|
||||
target: description,
|
||||
action_type: 'overwrite',
|
||||
},
|
||||
{
|
||||
source: 'comments',
|
||||
target: comments,
|
||||
action_type: 'append',
|
||||
},
|
||||
];
|
||||
};
|
28
x-pack/plugins/cases/server/connectors/factory.ts
Normal file
28
x-pack/plugins/cases/server/connectors/factory.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ConnectorTypes } from '../../common/api';
|
||||
import { getCaseConnector as getJiraCaseConnector } from './jira';
|
||||
import { getCaseConnector as getResilientCaseConnector } from './resilient';
|
||||
import { getServiceNowITSMCaseConnector, getServiceNowSIRCaseConnector } from './servicenow';
|
||||
import { ICasesConnector, CasesConnectorsMap } from './types';
|
||||
|
||||
const mapping: Record<ConnectorTypes, ICasesConnector | null> = {
|
||||
[ConnectorTypes.jira]: getJiraCaseConnector(),
|
||||
[ConnectorTypes.serviceNowITSM]: getServiceNowITSMCaseConnector(),
|
||||
[ConnectorTypes.serviceNowSIR]: getServiceNowSIRCaseConnector(),
|
||||
[ConnectorTypes.resilient]: getResilientCaseConnector(),
|
||||
[ConnectorTypes.none]: null,
|
||||
};
|
||||
|
||||
const isConnectorTypeSupported = (type: string): type is ConnectorTypes =>
|
||||
Object.values(ConnectorTypes).includes(type as ConnectorTypes);
|
||||
|
||||
export const casesConnectors: CasesConnectorsMap = {
|
||||
get: (type: string): ICasesConnector | undefined | null =>
|
||||
isConnectorTypeSupported(type) ? mapping[type] : undefined,
|
||||
};
|
|
@ -7,20 +7,16 @@
|
|||
|
||||
import {
|
||||
RegisterConnectorsArgs,
|
||||
ExternalServiceFormatterMapper,
|
||||
CommentSchemaType,
|
||||
ContextTypeGeneratedAlertType,
|
||||
ContextTypeAlertSchemaType,
|
||||
} from './types';
|
||||
import { getActionType as getCaseConnector } from './case';
|
||||
import { serviceNowITSMExternalServiceFormatter } from './servicenow/itsm_formatter';
|
||||
import { serviceNowSIRExternalServiceFormatter } from './servicenow/sir_formatter';
|
||||
import { jiraExternalServiceFormatter } from './jira/external_service_formatter';
|
||||
import { resilientExternalServiceFormatter } from './resilient/external_service_formatter';
|
||||
import { CommentRequest, CommentType } from '../../common';
|
||||
import { CommentRequest, CommentType } from '../../common/api';
|
||||
|
||||
export * from './types';
|
||||
export { transformConnectorComment } from './case';
|
||||
export { casesConnectors } from './factory';
|
||||
|
||||
/**
|
||||
* Separator used for creating a json parsable array from the mustache syntax that the alerting framework
|
||||
|
@ -41,13 +37,6 @@ export const registerConnectors = ({
|
|||
);
|
||||
};
|
||||
|
||||
export const externalServiceFormatters: ExternalServiceFormatterMapper = {
|
||||
'.servicenow': serviceNowITSMExternalServiceFormatter,
|
||||
'.servicenow-sir': serviceNowSIRExternalServiceFormatter,
|
||||
'.jira': jiraExternalServiceFormatter,
|
||||
'.resilient': resilientExternalServiceFormatter,
|
||||
};
|
||||
|
||||
export const isCommentGeneratedAlert = (
|
||||
comment: CommentSchemaType | CommentRequest
|
||||
): comment is ContextTypeGeneratedAlertType => {
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { CaseResponse } from '../../../common';
|
||||
import { jiraExternalServiceFormatter } from './external_service_formatter';
|
||||
import { CaseResponse } from '../../../common/api';
|
||||
import { format } from './format';
|
||||
|
||||
describe('Jira formatter', () => {
|
||||
const theCase = {
|
||||
|
@ -15,21 +15,18 @@ describe('Jira formatter', () => {
|
|||
} as CaseResponse;
|
||||
|
||||
it('it formats correctly', async () => {
|
||||
const res = await jiraExternalServiceFormatter.format(theCase, []);
|
||||
const res = await format(theCase, []);
|
||||
expect(res).toEqual({ ...theCase.connector.fields, labels: theCase.tags });
|
||||
});
|
||||
|
||||
it('it formats correctly when fields do not exist ', async () => {
|
||||
const invalidFields = { tags: ['tag'], connector: { fields: null } } as CaseResponse;
|
||||
const res = await jiraExternalServiceFormatter.format(invalidFields, []);
|
||||
const res = await format(invalidFields, []);
|
||||
expect(res).toEqual({ priority: null, issueType: null, parent: null, labels: theCase.tags });
|
||||
});
|
||||
|
||||
it('it replace white spaces with hyphens on tags', async () => {
|
||||
const res = await jiraExternalServiceFormatter.format(
|
||||
{ ...theCase, tags: ['a tag with spaces'] },
|
||||
[]
|
||||
);
|
||||
const res = await format({ ...theCase, tags: ['a tag with spaces'] }, []);
|
||||
expect(res).toEqual({ ...theCase.connector.fields, labels: ['a-tag-with-spaces'] });
|
||||
});
|
||||
});
|
|
@ -5,14 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { JiraFieldsType, ConnectorJiraTypeFields } from '../../../common';
|
||||
import { ExternalServiceFormatter } from '../types';
|
||||
import { ConnectorJiraTypeFields } from '../../../common/api';
|
||||
import { Format } from './types';
|
||||
|
||||
interface ExternalServiceParams extends JiraFieldsType {
|
||||
labels: string[];
|
||||
}
|
||||
|
||||
const format: ExternalServiceFormatter<ExternalServiceParams>['format'] = (theCase) => {
|
||||
export const format: Format = (theCase, alerts) => {
|
||||
const { priority = null, issueType = null, parent = null } =
|
||||
(theCase.connector.fields as ConnectorJiraTypeFields['fields']) ?? {};
|
||||
return {
|
||||
|
@ -23,7 +19,3 @@ const format: ExternalServiceFormatter<ExternalServiceParams>['format'] = (theCa
|
|||
parent,
|
||||
};
|
||||
};
|
||||
|
||||
export const jiraExternalServiceFormatter: ExternalServiceFormatter<ExternalServiceParams> = {
|
||||
format,
|
||||
};
|
15
x-pack/plugins/cases/server/connectors/jira/index.ts
Normal file
15
x-pack/plugins/cases/server/connectors/jira/index.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getMapping } from './mapping';
|
||||
import { format } from './format';
|
||||
import { JiraCaseConnector } from './types';
|
||||
|
||||
export const getCaseConnector = (): JiraCaseConnector => ({
|
||||
getMapping,
|
||||
format,
|
||||
});
|
28
x-pack/plugins/cases/server/connectors/jira/mapping.ts
Normal file
28
x-pack/plugins/cases/server/connectors/jira/mapping.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { GetMapping } from './types';
|
||||
|
||||
export const getMapping: GetMapping = () => {
|
||||
return [
|
||||
{
|
||||
source: 'title',
|
||||
target: 'summary',
|
||||
action_type: 'overwrite',
|
||||
},
|
||||
{
|
||||
source: 'description',
|
||||
target: 'description',
|
||||
action_type: 'overwrite',
|
||||
},
|
||||
{
|
||||
source: 'comments',
|
||||
target: 'comments',
|
||||
action_type: 'append',
|
||||
},
|
||||
];
|
||||
};
|
17
x-pack/plugins/cases/server/connectors/jira/types.ts
Normal file
17
x-pack/plugins/cases/server/connectors/jira/types.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { JiraFieldsType } from '../../../common/api';
|
||||
import { ICasesConnector } from '../types';
|
||||
|
||||
interface ExternalServiceFormatterParams extends JiraFieldsType {
|
||||
labels: string[];
|
||||
}
|
||||
|
||||
export type JiraCaseConnector = ICasesConnector<ExternalServiceFormatterParams>;
|
||||
export type Format = ICasesConnector<ExternalServiceFormatterParams>['format'];
|
||||
export type GetMapping = ICasesConnector<ExternalServiceFormatterParams>['getMapping'];
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { CaseResponse } from '../../../common';
|
||||
import { resilientExternalServiceFormatter } from './external_service_formatter';
|
||||
import { format } from './format';
|
||||
|
||||
describe('IBM Resilient formatter', () => {
|
||||
const theCase = {
|
||||
|
@ -14,13 +14,13 @@ describe('IBM Resilient formatter', () => {
|
|||
} as CaseResponse;
|
||||
|
||||
it('it formats correctly', async () => {
|
||||
const res = await resilientExternalServiceFormatter.format(theCase, []);
|
||||
const res = await format(theCase, []);
|
||||
expect(res).toEqual({ ...theCase.connector.fields });
|
||||
});
|
||||
|
||||
it('it formats correctly when fields do not exist ', async () => {
|
||||
const invalidFields = { tags: ['a tag'], connector: { fields: null } } as CaseResponse;
|
||||
const res = await resilientExternalServiceFormatter.format(invalidFields, []);
|
||||
const res = await format(invalidFields, []);
|
||||
expect(res).toEqual({ incidentTypes: null, severityCode: null });
|
||||
});
|
||||
});
|
|
@ -5,15 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ResilientFieldsType, ConnectorResillientTypeFields } from '../../../common';
|
||||
import { ExternalServiceFormatter } from '../types';
|
||||
import { ConnectorResillientTypeFields } from '../../../common/api';
|
||||
import { Format } from './types';
|
||||
|
||||
const format: ExternalServiceFormatter<ResilientFieldsType>['format'] = (theCase) => {
|
||||
export const format: Format = (theCase, alerts) => {
|
||||
const { incidentTypes = null, severityCode = null } =
|
||||
(theCase.connector.fields as ConnectorResillientTypeFields['fields']) ?? {};
|
||||
return { incidentTypes, severityCode };
|
||||
};
|
||||
|
||||
export const resilientExternalServiceFormatter: ExternalServiceFormatter<ResilientFieldsType> = {
|
||||
format,
|
||||
};
|
15
x-pack/plugins/cases/server/connectors/resilient/index.ts
Normal file
15
x-pack/plugins/cases/server/connectors/resilient/index.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getMapping } from './mapping';
|
||||
import { format } from './format';
|
||||
import { ResilientCaseConnector } from './types';
|
||||
|
||||
export const getCaseConnector = (): ResilientCaseConnector => ({
|
||||
getMapping,
|
||||
format,
|
||||
});
|
28
x-pack/plugins/cases/server/connectors/resilient/mapping.ts
Normal file
28
x-pack/plugins/cases/server/connectors/resilient/mapping.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { GetMapping } from './types';
|
||||
|
||||
export const getMapping: GetMapping = () => {
|
||||
return [
|
||||
{
|
||||
source: 'title',
|
||||
target: 'name',
|
||||
action_type: 'overwrite',
|
||||
},
|
||||
{
|
||||
source: 'description',
|
||||
target: 'description',
|
||||
action_type: 'overwrite',
|
||||
},
|
||||
{
|
||||
source: 'comments',
|
||||
target: 'comments',
|
||||
action_type: 'append',
|
||||
},
|
||||
];
|
||||
};
|
13
x-pack/plugins/cases/server/connectors/resilient/types.ts
Normal file
13
x-pack/plugins/cases/server/connectors/resilient/types.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ResilientFieldsType } from '../../../common/api';
|
||||
import { ICasesConnector } from '../types';
|
||||
|
||||
export type ResilientCaseConnector = ICasesConnector<ResilientFieldsType>;
|
||||
export type Format = ICasesConnector<ResilientFieldsType>['format'];
|
||||
export type GetMapping = ICasesConnector<ResilientFieldsType>['getMapping'];
|
23
x-pack/plugins/cases/server/connectors/servicenow/index.ts
Normal file
23
x-pack/plugins/cases/server/connectors/servicenow/index.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getMapping as getServiceNowITSMMapping } from './itsm_mapping';
|
||||
import { format as formatServiceNowITSM } from './itsm_format';
|
||||
import { getMapping as getServiceNowSIRMapping } from './sir_mapping';
|
||||
import { format as formatServiceNowSIR } from './sir_format';
|
||||
|
||||
import { ServiceNowITSMCasesConnector, ServiceNowSIRCasesConnector } from './types';
|
||||
|
||||
export const getServiceNowITSMCaseConnector = (): ServiceNowITSMCasesConnector => ({
|
||||
getMapping: getServiceNowITSMMapping,
|
||||
format: formatServiceNowITSM,
|
||||
});
|
||||
|
||||
export const getServiceNowSIRCaseConnector = (): ServiceNowSIRCasesConnector => ({
|
||||
getMapping: getServiceNowSIRMapping,
|
||||
format: formatServiceNowSIR,
|
||||
});
|
|
@ -5,8 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { CaseResponse } from '../../../common';
|
||||
import { serviceNowITSMExternalServiceFormatter } from './itsm_formatter';
|
||||
import { CaseResponse } from '../../../common/api';
|
||||
import { format } from './itsm_format';
|
||||
|
||||
describe('ITSM formatter', () => {
|
||||
const theCase = {
|
||||
|
@ -16,13 +16,13 @@ describe('ITSM formatter', () => {
|
|||
} as CaseResponse;
|
||||
|
||||
it('it formats correctly', async () => {
|
||||
const res = await serviceNowITSMExternalServiceFormatter.format(theCase, []);
|
||||
const res = await format(theCase, []);
|
||||
expect(res).toEqual(theCase.connector.fields);
|
||||
});
|
||||
|
||||
it('it formats correctly when fields do not exist ', async () => {
|
||||
const invalidFields = { connector: { fields: null } } as CaseResponse;
|
||||
const res = await serviceNowITSMExternalServiceFormatter.format(invalidFields, []);
|
||||
const res = await format(invalidFields, []);
|
||||
expect(res).toEqual({
|
||||
severity: null,
|
||||
urgency: null,
|
|
@ -5,15 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ServiceNowITSMFieldsType, ConnectorServiceNowITSMTypeFields } from '../../../common';
|
||||
import { ExternalServiceFormatter } from '../types';
|
||||
import { ConnectorServiceNowITSMTypeFields } from '../../../common/api';
|
||||
import { ServiceNowITSMFormat } from './types';
|
||||
|
||||
const format: ExternalServiceFormatter<ServiceNowITSMFieldsType>['format'] = (theCase) => {
|
||||
export const format: ServiceNowITSMFormat = (theCase, alerts) => {
|
||||
const { severity = null, urgency = null, impact = null, category = null, subcategory = null } =
|
||||
(theCase.connector.fields as ConnectorServiceNowITSMTypeFields['fields']) ?? {};
|
||||
return { severity, urgency, impact, category, subcategory };
|
||||
};
|
||||
|
||||
export const serviceNowITSMExternalServiceFormatter: ExternalServiceFormatter<ServiceNowITSMFieldsType> = {
|
||||
format,
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ServiceNowITSMGetMapping } from './types';
|
||||
|
||||
export const getMapping: ServiceNowITSMGetMapping = () => {
|
||||
return [
|
||||
{
|
||||
source: 'title',
|
||||
target: 'short_description',
|
||||
action_type: 'overwrite',
|
||||
},
|
||||
{
|
||||
source: 'description',
|
||||
target: 'description',
|
||||
action_type: 'overwrite',
|
||||
},
|
||||
{
|
||||
source: 'comments',
|
||||
target: 'work_notes',
|
||||
action_type: 'append',
|
||||
},
|
||||
];
|
||||
};
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { CaseResponse } from '../../../common';
|
||||
import { serviceNowSIRExternalServiceFormatter } from './sir_formatter';
|
||||
import { format } from './sir_format';
|
||||
|
||||
describe('ITSM formatter', () => {
|
||||
const theCase = {
|
||||
|
@ -24,7 +24,7 @@ describe('ITSM formatter', () => {
|
|||
} as CaseResponse;
|
||||
|
||||
it('it formats correctly without alerts', async () => {
|
||||
const res = await serviceNowSIRExternalServiceFormatter.format(theCase, []);
|
||||
const res = await format(theCase, []);
|
||||
expect(res).toEqual({
|
||||
dest_ip: null,
|
||||
source_ip: null,
|
||||
|
@ -38,7 +38,7 @@ describe('ITSM formatter', () => {
|
|||
|
||||
it('it formats correctly when fields do not exist ', async () => {
|
||||
const invalidFields = { connector: { fields: null } } as CaseResponse;
|
||||
const res = await serviceNowSIRExternalServiceFormatter.format(invalidFields, []);
|
||||
const res = await format(invalidFields, []);
|
||||
expect(res).toEqual({
|
||||
dest_ip: null,
|
||||
source_ip: null,
|
||||
|
@ -73,7 +73,7 @@ describe('ITSM formatter', () => {
|
|||
url: { full: 'https://attack.com/api' },
|
||||
},
|
||||
];
|
||||
const res = await serviceNowSIRExternalServiceFormatter.format(theCase, alerts);
|
||||
const res = await format(theCase, alerts);
|
||||
expect(res).toEqual({
|
||||
dest_ip: '192.168.1.1,192.168.1.4',
|
||||
source_ip: '192.168.1.2,192.168.1.3',
|
||||
|
@ -109,7 +109,7 @@ describe('ITSM formatter', () => {
|
|||
url: { full: 'https://attack.com/api' },
|
||||
},
|
||||
];
|
||||
const res = await serviceNowSIRExternalServiceFormatter.format(theCase, alerts);
|
||||
const res = await format(theCase, alerts);
|
||||
expect(res).toEqual({
|
||||
dest_ip: '192.168.1.1',
|
||||
source_ip: '192.168.1.2,192.168.1.3',
|
||||
|
@ -150,7 +150,7 @@ describe('ITSM formatter', () => {
|
|||
connector: { fields: { ...theCase.connector.fields, destIp: false, malwareHash: false } },
|
||||
} as CaseResponse;
|
||||
|
||||
const res = await serviceNowSIRExternalServiceFormatter.format(newCase, alerts);
|
||||
const res = await format(newCase, alerts);
|
||||
expect(res).toEqual({
|
||||
dest_ip: null,
|
||||
source_ip: '192.168.1.2,192.168.1.3',
|
|
@ -5,23 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import { get } from 'lodash/fp';
|
||||
import { ConnectorServiceNowSIRTypeFields } from '../../../common';
|
||||
import { ExternalServiceFormatter } from '../types';
|
||||
interface ExternalServiceParams {
|
||||
dest_ip: string | null;
|
||||
source_ip: string | null;
|
||||
category: string | null;
|
||||
subcategory: string | null;
|
||||
malware_hash: string | null;
|
||||
malware_url: string | null;
|
||||
priority: string | null;
|
||||
}
|
||||
type SirFieldKey = 'dest_ip' | 'source_ip' | 'malware_hash' | 'malware_url';
|
||||
type AlertFieldMappingAndValues = Record<
|
||||
string,
|
||||
{ alertPath: string; sirFieldKey: SirFieldKey; add: boolean }
|
||||
>;
|
||||
const format: ExternalServiceFormatter<ExternalServiceParams>['format'] = (theCase, alerts) => {
|
||||
import { ConnectorServiceNowSIRTypeFields } from '../../../common/api';
|
||||
import { ServiceNowSIRFormat, SirFieldKey, AlertFieldMappingAndValues } from './types';
|
||||
|
||||
export const format: ServiceNowSIRFormat = (theCase, alerts) => {
|
||||
const {
|
||||
destIp = null,
|
||||
sourceIp = null,
|
||||
|
@ -83,6 +70,3 @@ const format: ExternalServiceFormatter<ExternalServiceParams>['format'] = (theCa
|
|||
priority,
|
||||
};
|
||||
};
|
||||
export const serviceNowSIRExternalServiceFormatter: ExternalServiceFormatter<ExternalServiceParams> = {
|
||||
format,
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ServiceNowSIRGetMapping } from './types';
|
||||
|
||||
export const getMapping: ServiceNowSIRGetMapping = () => {
|
||||
return [
|
||||
{
|
||||
source: 'title',
|
||||
target: 'short_description',
|
||||
action_type: 'overwrite',
|
||||
},
|
||||
{
|
||||
source: 'description',
|
||||
target: 'description',
|
||||
action_type: 'overwrite',
|
||||
},
|
||||
{
|
||||
source: 'comments',
|
||||
target: 'work_notes',
|
||||
action_type: 'append',
|
||||
},
|
||||
];
|
||||
};
|
35
x-pack/plugins/cases/server/connectors/servicenow/types.ts
Normal file
35
x-pack/plugins/cases/server/connectors/servicenow/types.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ServiceNowITSMFieldsType } from '../../../common/api';
|
||||
import { ICasesConnector } from '../types';
|
||||
|
||||
export interface ServiceNowSIRFieldsType {
|
||||
dest_ip: string | null;
|
||||
source_ip: string | null;
|
||||
category: string | null;
|
||||
subcategory: string | null;
|
||||
malware_hash: string | null;
|
||||
malware_url: string | null;
|
||||
priority: string | null;
|
||||
}
|
||||
|
||||
export type SirFieldKey = 'dest_ip' | 'source_ip' | 'malware_hash' | 'malware_url';
|
||||
export type AlertFieldMappingAndValues = Record<
|
||||
string,
|
||||
{ alertPath: string; sirFieldKey: SirFieldKey; add: boolean }
|
||||
>;
|
||||
|
||||
// ServiceNow ITSM
|
||||
export type ServiceNowITSMCasesConnector = ICasesConnector<ServiceNowITSMFieldsType>;
|
||||
export type ServiceNowITSMFormat = ICasesConnector<ServiceNowITSMFieldsType>['format'];
|
||||
export type ServiceNowITSMGetMapping = ICasesConnector<ServiceNowITSMFieldsType>['getMapping'];
|
||||
|
||||
// ServiceNow SIR
|
||||
export type ServiceNowSIRCasesConnector = ICasesConnector<ServiceNowSIRFieldsType>;
|
||||
export type ServiceNowSIRFormat = ICasesConnector<ServiceNowSIRFieldsType>['format'];
|
||||
export type ServiceNowSIRGetMapping = ICasesConnector<ServiceNowSIRFieldsType>['getMapping'];
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { Logger } from 'kibana/server';
|
||||
import { CaseResponse, ConnectorTypes } from '../../common/api';
|
||||
import { CaseResponse, ConnectorMappingsAttributes } from '../../common/api';
|
||||
import { CasesClientGetAlertsResponse } from '../client/alerts/types';
|
||||
import { CasesClientFactory } from '../client/factory';
|
||||
import { RegisterActionType } from '../types';
|
||||
|
@ -26,12 +26,11 @@ export interface RegisterConnectorsArgs extends GetActionTypeParams {
|
|||
registerActionType: RegisterActionType;
|
||||
}
|
||||
|
||||
export type FormatterConnectorTypes = Exclude<ConnectorTypes, ConnectorTypes.none>;
|
||||
|
||||
export interface ExternalServiceFormatter<TExternalServiceParams = {}> {
|
||||
export interface ICasesConnector<TExternalServiceParams = {}> {
|
||||
format: (theCase: CaseResponse, alerts: CasesClientGetAlertsResponse) => TExternalServiceParams;
|
||||
getMapping: () => ConnectorMappingsAttributes[];
|
||||
}
|
||||
|
||||
export type ExternalServiceFormatterMapper = {
|
||||
[x in FormatterConnectorTypes]: ExternalServiceFormatter;
|
||||
};
|
||||
export interface CasesConnectorsMap {
|
||||
get: (type: string) => ICasesConnector | undefined | null;
|
||||
}
|
||||
|
|
|
@ -10,20 +10,13 @@ import {
|
|||
AssociationType,
|
||||
CaseStatuses,
|
||||
CaseType,
|
||||
CaseUserActionAttributes,
|
||||
CommentAttributes,
|
||||
CommentType,
|
||||
ConnectorMappings,
|
||||
ConnectorTypes,
|
||||
ESCaseAttributes,
|
||||
ESCasesConfigureAttributes,
|
||||
} from '../../../../common';
|
||||
import {
|
||||
CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT,
|
||||
CASE_USER_ACTION_SAVED_OBJECT,
|
||||
SECURITY_SOLUTION_OWNER,
|
||||
} from '../../../../common/constants';
|
||||
import { mappings } from '../../../client/configure/mock';
|
||||
import { SECURITY_SOLUTION_OWNER } from '../../../../common/constants';
|
||||
|
||||
export const mockCases: Array<SavedObject<ESCaseAttributes>> = [
|
||||
{
|
||||
|
@ -485,79 +478,3 @@ export const mockCaseConfigure: Array<SavedObject<ESCasesConfigureAttributes>> =
|
|||
version: 'WzYsMV0=',
|
||||
},
|
||||
];
|
||||
|
||||
export const mockCaseMappings: Array<SavedObject<ConnectorMappings>> = [
|
||||
{
|
||||
type: CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT,
|
||||
id: 'mock-mappings-1',
|
||||
attributes: {
|
||||
mappings: mappings[ConnectorTypes.jira],
|
||||
owner: SECURITY_SOLUTION_OWNER,
|
||||
},
|
||||
references: [],
|
||||
},
|
||||
];
|
||||
|
||||
export const mockCaseMappingsResilient: Array<SavedObject<ConnectorMappings>> = [
|
||||
{
|
||||
type: CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT,
|
||||
id: 'mock-mappings-1',
|
||||
attributes: {
|
||||
mappings: mappings[ConnectorTypes.resilient],
|
||||
owner: SECURITY_SOLUTION_OWNER,
|
||||
},
|
||||
references: [],
|
||||
},
|
||||
];
|
||||
|
||||
export const mockCaseMappingsBad: Array<SavedObject<Partial<ConnectorMappings>>> = [
|
||||
{
|
||||
type: CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT,
|
||||
id: 'mock-mappings-bad',
|
||||
attributes: {},
|
||||
references: [],
|
||||
},
|
||||
];
|
||||
|
||||
export const mockUserActions: Array<SavedObject<CaseUserActionAttributes>> = [
|
||||
{
|
||||
type: CASE_USER_ACTION_SAVED_OBJECT,
|
||||
id: 'mock-user-actions-1',
|
||||
attributes: {
|
||||
action_field: ['description', 'status', 'tags', 'title', 'connector', 'settings'],
|
||||
action: 'create',
|
||||
action_at: '2021-02-03T17:41:03.771Z',
|
||||
action_by: {
|
||||
email: 'elastic@elastic.co',
|
||||
full_name: 'Elastic',
|
||||
username: 'elastic',
|
||||
},
|
||||
new_value:
|
||||
'{"title":"A case","tags":["case"],"description":"Yeah!","connector":{"id":"connector-od","name":"My Connector","type":".servicenow-sir","fields":{"category":"Denial of Service","destIp":true,"malwareHash":true,"malwareUrl":true,"priority":"2","sourceIp":true,"subcategory":"45"}},"settings":{"syncAlerts":true}}',
|
||||
old_value: null,
|
||||
owner: SECURITY_SOLUTION_OWNER,
|
||||
},
|
||||
version: 'WzYsMV0=',
|
||||
references: [],
|
||||
},
|
||||
{
|
||||
type: CASE_USER_ACTION_SAVED_OBJECT,
|
||||
id: 'mock-user-actions-2',
|
||||
attributes: {
|
||||
action_field: ['comment'],
|
||||
action: 'create',
|
||||
action_at: '2021-02-03T17:44:21.067Z',
|
||||
action_by: {
|
||||
email: 'elastic@elastic.co',
|
||||
full_name: 'Elastic',
|
||||
username: 'elastic',
|
||||
},
|
||||
new_value:
|
||||
'{"type":"alert","alertId":"cec3da90fb37a44407145adf1593f3b0d5ad94c4654201f773d63b5d4706128e","index":".siem-signals-default-000008"}',
|
||||
old_value: null,
|
||||
owner: SECURITY_SOLUTION_OWNER,
|
||||
},
|
||||
version: 'WzYsMV0=',
|
||||
references: [],
|
||||
},
|
||||
];
|
||||
|
|
|
@ -8,14 +8,16 @@
|
|||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib';
|
||||
import { ConnectorTypes } from '../../../../../../plugins/cases/common/api';
|
||||
|
||||
import {
|
||||
getConfigurationRequest,
|
||||
removeServerGeneratedPropertiesFromSavedObject,
|
||||
getConfigurationOutput,
|
||||
deleteConfiguration,
|
||||
createConfiguration,
|
||||
updateConfiguration,
|
||||
getConfigurationRequest,
|
||||
getConfiguration,
|
||||
} from '../../../../common/lib/utils';
|
||||
import {
|
||||
secOnly,
|
||||
|
@ -52,6 +54,39 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
expect(data).to.eql({ ...getConfigurationOutput(true), closure_type: 'close-by-pushing' });
|
||||
});
|
||||
|
||||
it('should update mapping when changing connector', async () => {
|
||||
const configuration = await createConfiguration(supertest);
|
||||
await updateConfiguration(supertest, configuration.id, {
|
||||
connector: {
|
||||
id: 'serviceNowITSM',
|
||||
name: 'ServiceNow ITSM',
|
||||
type: ConnectorTypes.serviceNowITSM,
|
||||
fields: null,
|
||||
},
|
||||
version: configuration.version,
|
||||
});
|
||||
const newConfiguration = await getConfiguration({ supertest });
|
||||
|
||||
expect(configuration.mappings).to.eql([]);
|
||||
expect(newConfiguration[0].mappings).to.eql([
|
||||
{
|
||||
action_type: 'overwrite',
|
||||
source: 'title',
|
||||
target: 'short_description',
|
||||
},
|
||||
{
|
||||
action_type: 'overwrite',
|
||||
source: 'description',
|
||||
target: 'description',
|
||||
},
|
||||
{
|
||||
action_type: 'append',
|
||||
source: 'comments',
|
||||
target: 'work_notes',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not patch a configuration with unsupported connector type', async () => {
|
||||
const configuration = await createConfiguration(supertest);
|
||||
await updateConfiguration(
|
||||
|
|
|
@ -60,18 +60,133 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
expect(configuration.length).to.be(1);
|
||||
});
|
||||
|
||||
it('should return an error when failing to get mapping', async () => {
|
||||
it('should return an empty mapping when they type is none', async () => {
|
||||
const postRes = await createConfiguration(
|
||||
supertest,
|
||||
getConfigurationRequest({
|
||||
id: 'not-exists',
|
||||
name: 'not-exists',
|
||||
type: ConnectorTypes.none,
|
||||
})
|
||||
);
|
||||
|
||||
expect(postRes.mappings).to.eql([]);
|
||||
});
|
||||
|
||||
it('should return the correct mapping for Jira', async () => {
|
||||
const postRes = await createConfiguration(
|
||||
supertest,
|
||||
getConfigurationRequest({
|
||||
id: 'jira',
|
||||
name: 'Jira',
|
||||
type: ConnectorTypes.jira,
|
||||
})
|
||||
);
|
||||
|
||||
expect(postRes.error).to.not.be(null);
|
||||
expect(postRes.mappings).to.eql([]);
|
||||
expect(postRes.mappings).to.eql([
|
||||
{
|
||||
action_type: 'overwrite',
|
||||
source: 'title',
|
||||
target: 'summary',
|
||||
},
|
||||
{
|
||||
action_type: 'overwrite',
|
||||
source: 'description',
|
||||
target: 'description',
|
||||
},
|
||||
{
|
||||
action_type: 'append',
|
||||
source: 'comments',
|
||||
target: 'comments',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return the correct mapping for IBM Resilient', async () => {
|
||||
const postRes = await createConfiguration(
|
||||
supertest,
|
||||
getConfigurationRequest({
|
||||
id: 'resilient',
|
||||
name: 'Resilient',
|
||||
type: ConnectorTypes.resilient,
|
||||
})
|
||||
);
|
||||
|
||||
expect(postRes.mappings).to.eql([
|
||||
{
|
||||
action_type: 'overwrite',
|
||||
source: 'title',
|
||||
target: 'name',
|
||||
},
|
||||
{
|
||||
action_type: 'overwrite',
|
||||
source: 'description',
|
||||
target: 'description',
|
||||
},
|
||||
{
|
||||
action_type: 'append',
|
||||
source: 'comments',
|
||||
target: 'comments',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return the correct mapping for ServiceNow ITSM', async () => {
|
||||
const postRes = await createConfiguration(
|
||||
supertest,
|
||||
getConfigurationRequest({
|
||||
id: 'serviceNowITSM',
|
||||
name: 'ServiceNow ITSM',
|
||||
type: ConnectorTypes.serviceNowITSM,
|
||||
})
|
||||
);
|
||||
|
||||
expect(postRes.mappings).to.eql([
|
||||
{
|
||||
action_type: 'overwrite',
|
||||
source: 'title',
|
||||
target: 'short_description',
|
||||
},
|
||||
{
|
||||
action_type: 'overwrite',
|
||||
source: 'description',
|
||||
target: 'description',
|
||||
},
|
||||
{
|
||||
action_type: 'append',
|
||||
source: 'comments',
|
||||
target: 'work_notes',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return the correct mapping for ServiceNow SecOps', async () => {
|
||||
const postRes = await createConfiguration(
|
||||
supertest,
|
||||
getConfigurationRequest({
|
||||
id: 'serviceNowSIR',
|
||||
name: 'ServiceNow SecOps',
|
||||
type: ConnectorTypes.serviceNowSIR,
|
||||
})
|
||||
);
|
||||
|
||||
expect(postRes.mappings).to.eql([
|
||||
{
|
||||
action_type: 'overwrite',
|
||||
source: 'title',
|
||||
target: 'short_description',
|
||||
},
|
||||
{
|
||||
action_type: 'overwrite',
|
||||
source: 'description',
|
||||
target: 'description',
|
||||
},
|
||||
{
|
||||
action_type: 'append',
|
||||
source: 'comments',
|
||||
target: 'work_notes',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not create a configuration when missing connector.id', async () => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue