[Cases] Improve connectors mapping (#101145)

This commit is contained in:
Christos Nasikas 2021-06-10 11:45:25 +03:00 committed by GitHub
parent 0f5620e5c4
commit 144e014dbf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 516 additions and 1121 deletions

View file

@ -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]),

View file

@ -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.'
);
});
});

View file

@ -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 => {

View file

@ -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({

View file

@ -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'));

View file

@ -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) {

View file

@ -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({

View file

@ -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,
});

View file

@ -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) };
};

View file

@ -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,
});

View file

@ -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',
};

View file

@ -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;
}

View file

@ -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,
});

View file

@ -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]);
});
});
});
});

View file

@ -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',
},
];
};

View 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,
};

View file

@ -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 => {

View file

@ -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'] });
});
});

View file

@ -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,
};

View 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,
});

View 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',
},
];
};

View 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'];

View file

@ -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 });
});
});

View file

@ -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,
};

View 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,
});

View 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',
},
];
};

View 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'];

View 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,
});

View file

@ -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,

View file

@ -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,
};

View 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 { 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',
},
];
};

View file

@ -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',

View file

@ -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,
};

View 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 { 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',
},
];
};

View 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'];

View file

@ -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;
}

View file

@ -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: [],
},
];

View file

@ -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(

View file

@ -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 () => {