Skip to content

Commit

Permalink
[Security Solution][Case] Push ITSM comments as work notes (#93916) (#…
Browse files Browse the repository at this point in the history
…93947)

* Push ITSM comments as work notes

* Fix cases mapping

* Improve error messages

* Fix tests
  • Loading branch information
cnasikas authored Mar 8, 2021
1 parent b5ae4d5 commit c36677a
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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'
);
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}),
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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({
Expand All @@ -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)}`
)
);
}
Expand All @@ -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
)}`
)
);
}
Expand All @@ -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
)}`
)
);
}
};
Expand All @@ -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)}`
)
);
}
Expand All @@ -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
)}`
)
);
}
};
Expand All @@ -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
)}`
)
);
}
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
}
37 changes: 36 additions & 1 deletion x-pack/plugins/case/server/client/configure/mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
],
Expand Down Expand Up @@ -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',
Expand Down
18 changes: 15 additions & 3 deletions x-pack/plugins/case/server/client/configure/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down Expand Up @@ -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);
Expand All @@ -169,7 +181,7 @@ export const createDefaultMapping = (
},
{
source: 'comments',
target: 'comments',
target: preferredComments,
action_type: 'append',
},
];
Expand Down

0 comments on commit c36677a

Please sign in to comment.