[Security Solution][Case] Push ITSM comments as work notes (#93916)

* Push ITSM comments as work notes

* Fix cases mapping

* Improve error messages

* Fix tests
This commit is contained in:
Christos Nasikas 2021-03-08 16:38:04 +02:00 committed by GitHub
parent 8de91b506e
commit faae074607
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 110 additions and 13 deletions

View file

@ -78,7 +78,9 @@ describe('ServiceNow', () => {
services,
} as unknown) as ServiceNowActionTypeExecutorOptions;
await actionType.executor(executorOptions);
expect((api.pushToService as jest.Mock).mock.calls[0][0].commentFieldKey).toBe('comments');
expect((api.pushToService as jest.Mock).mock.calls[0][0].commentFieldKey).toBe(
'work_notes'
);
});
});
});

View file

@ -76,7 +76,12 @@ export function getServiceNowITSMActionType(params: GetActionTypeParams): Servic
}),
params: ExecutorParamsSchemaITSM,
},
executor: curry(executor)({ logger, configurationUtilities, table: serviceNowITSMTable }),
executor: curry(executor)({
logger,
configurationUtilities,
table: serviceNowITSMTable,
commentFieldKey: 'work_notes',
}),
};
}

View file

@ -11,7 +11,11 @@ import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } fr
import * as i18n from './translations';
import { Logger } from '../../../../../../src/core/server';
import { ServiceNowPublicConfigurationType, ServiceNowSecretConfigurationType } from './types';
import {
ServiceNowPublicConfigurationType,
ServiceNowSecretConfigurationType,
ResponseError,
} from './types';
import { request, getErrorMessage, addTimeZoneToDate, patch } from '../lib/axios_utils';
import { ActionsConfigurationUtilities } from '../../actions_config';
@ -62,6 +66,15 @@ export const createExternalService = (
}
};
const createErrorMessage = (errorResponse: ResponseError): string => {
if (errorResponse == null) {
return '';
}
const { error } = errorResponse;
return error != null ? `${error?.message}: ${error?.detail}` : '';
};
const getIncident = async (id: string) => {
try {
const res = await request({
@ -76,7 +89,9 @@ export const createExternalService = (
throw new Error(
getErrorMessage(
i18n.SERVICENOW,
`Unable to get incident with id ${id}. Error: ${error.message}`
`Unable to get incident with id ${id}. Error: ${
error.message
} Reason: ${createErrorMessage(error.response?.data)}`
)
);
}
@ -97,7 +112,9 @@ export const createExternalService = (
throw new Error(
getErrorMessage(
i18n.SERVICENOW,
`Unable to find incidents by query. Error: ${error.message}`
`Unable to find incidents by query. Error: ${error.message} Reason: ${createErrorMessage(
error.response?.data
)}`
)
);
}
@ -122,7 +139,12 @@ export const createExternalService = (
};
} catch (error) {
throw new Error(
getErrorMessage(i18n.SERVICENOW, `Unable to create incident. Error: ${error.message}`)
getErrorMessage(
i18n.SERVICENOW,
`Unable to create incident. Error: ${error.message} Reason: ${createErrorMessage(
error.response?.data
)}`
)
);
}
};
@ -147,7 +169,9 @@ export const createExternalService = (
throw new Error(
getErrorMessage(
i18n.SERVICENOW,
`Unable to update incident with id ${incidentId}. Error: ${error.message}`
`Unable to update incident with id ${incidentId}. Error: ${
error.message
} Reason: ${createErrorMessage(error.response?.data)}`
)
);
}
@ -165,7 +189,12 @@ export const createExternalService = (
return res.data.result.length > 0 ? res.data.result : [];
} catch (error) {
throw new Error(
getErrorMessage(i18n.SERVICENOW, `Unable to get fields. Error: ${error.message}`)
getErrorMessage(
i18n.SERVICENOW,
`Unable to get fields. Error: ${error.message} Reason: ${createErrorMessage(
error.response?.data
)}`
)
);
}
};
@ -182,7 +211,12 @@ export const createExternalService = (
return res.data.result;
} catch (error) {
throw new Error(
getErrorMessage(i18n.SERVICENOW, `Unable to get choices. Error: ${error.message}`)
getErrorMessage(
i18n.SERVICENOW,
`Unable to get choices. Error: ${error.message} Reason: ${createErrorMessage(
error.response?.data
)}`
)
);
}
};

View file

@ -171,3 +171,12 @@ export interface ExternalServiceCommentResponse {
pushedDate: string;
externalCommentId?: string;
}
type TypeNullOrUndefined<T> = T | null | undefined;
export interface ResponseError {
error: TypeNullOrUndefined<{
message: TypeNullOrUndefined<string>;
detail: TypeNullOrUndefined<string>;
}>;
status: TypeNullOrUndefined<string>;
}

View file

@ -83,7 +83,24 @@ export const mappings: TestMappings = {
},
{
source: 'comments',
target: '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',
},
],
@ -613,6 +630,24 @@ export const formatFieldsTestData: FormatFieldsTestData[] = [
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',

View file

@ -93,21 +93,26 @@ const findTextAreaField = (fields: ConnectorField[]): string =>
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 };
return { title, description, comments };
};
const getRemainingFields = (fields: ConnectorField[], titleTarget: string) =>
@ -143,9 +148,16 @@ export const createDefaultMapping = (
theType: string
): ConnectorMappingsAttributes[] => {
const { description: dynamicDescription, title: dynamicTitle } = getDynamicFields(fields);
const { description: preferredDescription, title: preferredTitle } = getPreferredFields(theType);
const {
description: preferredDescription,
title: preferredTitle,
comments: preferredComments,
} = getPreferredFields(theType);
let titleTarget = dynamicTitle;
let descriptionTarget = dynamicDescription;
if (preferredTitle.length > 0 && preferredDescription.length > 0) {
if (shouldTargetBePreferred(fields, dynamicTitle, preferredTitle)) {
const { description: dynamicDescriptionOverwrite } = getDynamicFields(fields, preferredTitle);
@ -169,7 +181,7 @@ export const createDefaultMapping = (
},
{
source: 'comments',
target: 'comments',
target: preferredComments,
action_type: 'append',
},
];