Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution][Case] ServiceNow SIR Connector #88655

Merged
merged 47 commits into from
Feb 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
ab58f88
Move settings to connectors
cnasikas Jan 19, 2021
558ee10
Rename connector settings to a more proper name
cnasikas Jan 19, 2021
d4ed389
Create ServiceNow SIR case connector fields
cnasikas Jan 19, 2021
0491f72
Dynamic fields for ServiceNow connector
cnasikas Jan 21, 2021
48a21c8
Add mapping for SIR connector
cnasikas Jan 21, 2021
c46c373
Rename fields
cnasikas Jan 21, 2021
bfa35a7
Add SIR formatter
cnasikas Jan 21, 2021
c905917
Add tests
cnasikas Jan 21, 2021
bd1e9ea
Get information from action type
cnasikas Jan 21, 2021
8b7e4f0
Rename IM to ITSM
cnasikas Feb 1, 2021
ba169e3
Get case client method
cnasikas Feb 1, 2021
c280c28
Get user actions client method
cnasikas Feb 1, 2021
0358636
Create connector external service formatters
cnasikas Feb 1, 2021
0a71eb0
Get alerts client method
cnasikas Feb 1, 2021
a014c79
Move push logic to the backend
cnasikas Feb 1, 2021
b7ff2d3
Format alerts on the backend
cnasikas Feb 2, 2021
a0e32b6
Fix comments to be pushed
cnasikas Feb 2, 2021
d7d515b
Fix when fields are null
cnasikas Feb 2, 2021
dd24333
Add ips to sir
cnasikas Feb 2, 2021
861d416
Fix types
cnasikas Feb 2, 2021
9e2fa73
Use set instead of array
cnasikas Feb 2, 2021
0791160
Convert alert fields to checkbox
cnasikas Feb 2, 2021
efc7e74
Change name of SN SIR
cnasikas Feb 2, 2021
b9cafd9
Merge push case with configure push
cnasikas Feb 2, 2021
4bba908
Push comments to work notes
cnasikas Feb 2, 2021
5a79c82
Throw when res from execute is an error
cnasikas Feb 3, 2021
16d3b8a
Create push client method
cnasikas Feb 3, 2021
79a0bef
Do not query with no ids
cnasikas Feb 3, 2021
28452c3
Fix SIR description
cnasikas Feb 3, 2021
7468be1
Fix types
cnasikas Feb 3, 2021
8be8191
Fix tests
cnasikas Feb 3, 2021
1c51755
Fix license
cnasikas Feb 4, 2021
98f9b73
Refactor case client
cnasikas Feb 4, 2021
a5f022b
Fix tests
cnasikas Feb 4, 2021
2d4dc6d
Fix i18n errors
cnasikas Feb 4, 2021
2706414
Fix integration tests
cnasikas Feb 4, 2021
d4e5bca
Mock useGetChoices
cnasikas Feb 4, 2021
0b4b14e
Fix cypress tests
cnasikas Feb 4, 2021
6a3447e
Fix bug when changing connectors
cnasikas Feb 4, 2021
c441677
Add tests
cnasikas Feb 4, 2021
82fe896
Jira: Replace white spaces with hyphens on tags
cnasikas Feb 6, 2021
3e3bb25
PR Feedback
cnasikas Feb 8, 2021
11c583c
PR Feedback
cnasikas Feb 8, 2021
11f713f
christos and I figure out how to get rid of the useEffect to re-init …
XavierM Feb 8, 2021
6eb6b15
Merge branch 'cases_servicenow_sir_fields' of github.com:cnasikas/kib…
cnasikas Feb 8, 2021
ca083b4
Merge branch 'master' into cases_servicenow_sir_fields
cnasikas Feb 8, 2021
47ef8e5
Fix tests
cnasikas Feb 9, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ describe('api', () => {

beforeEach(() => {
externalService = externalServiceMock.create();
jest.clearAllMocks();
});

describe('create incident', () => {
Expand All @@ -26,6 +27,7 @@ describe('api', () => {
params,
secrets: {},
logger: mockedLogger,
commentFieldKey: 'comments',
});

expect(res).toEqual({
Expand Down Expand Up @@ -57,6 +59,7 @@ describe('api', () => {
params,
secrets: {},
logger: mockedLogger,
commentFieldKey: 'comments',
});

expect(res).toEqual({
Expand All @@ -77,6 +80,7 @@ describe('api', () => {
params,
secrets: { username: 'elastic', password: 'elastic' },
logger: mockedLogger,
commentFieldKey: 'comments',
});

expect(externalService.createIncident).toHaveBeenCalledWith({
Expand All @@ -99,6 +103,7 @@ describe('api', () => {
params,
secrets: {},
logger: mockedLogger,
commentFieldKey: 'comments',
});
expect(externalService.updateIncident).toHaveBeenCalledTimes(2);
expect(externalService.updateIncident).toHaveBeenNthCalledWith(1, {
Expand All @@ -125,6 +130,41 @@ describe('api', () => {
incidentId: 'incident-1',
});
});

test('it post comments to different comment field key', async () => {
const params = { ...apiParams, incident: { ...apiParams.incident, externalId: null } };
await api.pushToService({
externalService,
params,
secrets: {},
logger: mockedLogger,
commentFieldKey: 'work_notes',
});
expect(externalService.updateIncident).toHaveBeenCalledTimes(2);
expect(externalService.updateIncident).toHaveBeenNthCalledWith(1, {
incident: {
severity: '1',
urgency: '2',
impact: '3',
work_notes: 'A comment',
description: 'Incident description',
short_description: 'Incident title',
},
incidentId: 'incident-1',
});

expect(externalService.updateIncident).toHaveBeenNthCalledWith(2, {
incident: {
severity: '1',
urgency: '2',
impact: '3',
work_notes: 'Another comment',
description: 'Incident description',
short_description: 'Incident title',
},
incidentId: 'incident-1',
});
});
});

describe('update incident', () => {
Expand All @@ -134,6 +174,7 @@ describe('api', () => {
params: apiParams,
secrets: {},
logger: mockedLogger,
commentFieldKey: 'comments',
});

expect(res).toEqual({
Expand Down Expand Up @@ -161,6 +202,7 @@ describe('api', () => {
params,
secrets: {},
logger: mockedLogger,
commentFieldKey: 'comments',
});

expect(res).toEqual({
Expand All @@ -178,6 +220,7 @@ describe('api', () => {
params,
secrets: {},
logger: mockedLogger,
commentFieldKey: 'comments',
});

expect(externalService.updateIncident).toHaveBeenCalledWith({
Expand All @@ -200,6 +243,7 @@ describe('api', () => {
params,
secrets: {},
logger: mockedLogger,
commentFieldKey: 'comments',
});
expect(externalService.updateIncident).toHaveBeenCalledTimes(3);
expect(externalService.updateIncident).toHaveBeenNthCalledWith(1, {
Expand All @@ -225,6 +269,40 @@ describe('api', () => {
incidentId: 'incident-2',
});
});

test('it post comments to different comment field key', async () => {
const params = { ...apiParams };
await api.pushToService({
externalService,
params,
secrets: {},
logger: mockedLogger,
commentFieldKey: 'work_notes',
});
expect(externalService.updateIncident).toHaveBeenCalledTimes(3);
expect(externalService.updateIncident).toHaveBeenNthCalledWith(1, {
incident: {
severity: '1',
urgency: '2',
impact: '3',
description: 'Incident description',
short_description: 'Incident title',
},
incidentId: 'incident-3',
});

expect(externalService.updateIncident).toHaveBeenNthCalledWith(2, {
incident: {
severity: '1',
urgency: '2',
impact: '3',
work_notes: 'A comment',
description: 'Incident description',
short_description: 'Incident title',
},
incidentId: 'incident-2',
});
});
});

describe('getFields', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const pushToServiceHandler = async ({
externalService,
params,
secrets,
commentFieldKey,
}: PushToServiceApiHandlerArgs): Promise<PushToServiceResponse> => {
const { comments } = params;
let res: PushToServiceResponse;
Expand Down Expand Up @@ -53,7 +54,7 @@ const pushToServiceHandler = async ({
incidentId: res.id,
incident: {
...incident,
comments: currentComment.comment,
[commentFieldKey]: currentComment.comment,
},
});
res.comments = [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* 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 { actionsMock } from '../../mocks';
import { createActionTypeRegistry } from '../index.test';
import {
ServiceNowPublicConfigurationType,
ServiceNowSecretConfigurationType,
ExecutorParams,
PushToServiceResponse,
} from './types';
import {
ServiceNowActionType,
ServiceNowITSMActionTypeId,
ServiceNowSIRActionTypeId,
ServiceNowActionTypeExecutorOptions,
} from '.';
import { api } from './api';

jest.mock('./api', () => ({
api: {
getChoices: jest.fn(),
getFields: jest.fn(),
getIncident: jest.fn(),
handshake: jest.fn(),
pushToService: jest.fn(),
},
}));

const services = actionsMock.createServices();

describe('ServiceNow', () => {
const config = { apiUrl: 'https://instance.com' };
const secrets = { username: 'username', password: 'password' };
const params = {
subAction: 'pushToService',
subActionParams: {
incident: {
short_description: 'An incident',
description: 'This is serious',
},
},
};

beforeEach(() => {
(api.pushToService as jest.Mock).mockResolvedValue({ id: 'some-id' });
});

describe('ServiceNow ITSM', () => {
let actionType: ServiceNowActionType;

beforeAll(() => {
const { actionTypeRegistry } = createActionTypeRegistry();
actionType = actionTypeRegistry.get<
ServiceNowPublicConfigurationType,
ServiceNowSecretConfigurationType,
ExecutorParams,
PushToServiceResponse | {}
>(ServiceNowITSMActionTypeId);
});

describe('execute()', () => {
beforeEach(() => {
jest.clearAllMocks();
});

test('it pass the correct comment field key', async () => {
const actionId = 'some-action-id';
const executorOptions = ({
actionId,
config,
secrets,
params,
services,
} as unknown) as ServiceNowActionTypeExecutorOptions;
await actionType.executor(executorOptions);
expect((api.pushToService as jest.Mock).mock.calls[0][0].commentFieldKey).toBe('comments');
});
});
});

describe('ServiceNow SIR', () => {
let actionType: ServiceNowActionType;

beforeAll(() => {
const { actionTypeRegistry } = createActionTypeRegistry();
actionType = actionTypeRegistry.get<
ServiceNowPublicConfigurationType,
ServiceNowSecretConfigurationType,
ExecutorParams,
PushToServiceResponse | {}
>(ServiceNowSIRActionTypeId);
});

describe('execute()', () => {
beforeEach(() => {
jest.clearAllMocks();
});

test('it pass the correct comment field key', async () => {
const actionId = 'some-action-id';
const executorOptions = ({
actionId,
config,
secrets,
params,
services,
} as unknown) as ServiceNowActionTypeExecutorOptions;
await actionType.executor(executorOptions);
expect((api.pushToService as jest.Mock).mock.calls[0][0].commentFieldKey).toBe(
'work_notes'
);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,21 @@ const serviceNowSIRTable = 'sn_si_incident';
export const ServiceNowITSMActionTypeId = '.servicenow';
export const ServiceNowSIRActionTypeId = '.servicenow-sir';

// action type definition
export function getServiceNowITSMActionType(
params: GetActionTypeParams
): ActionType<
export type ServiceNowActionType = ActionType<
ServiceNowPublicConfigurationType,
ServiceNowSecretConfigurationType,
ExecutorParams,
PushToServiceResponse | {}
> {
>;

export type ServiceNowActionTypeExecutorOptions = ActionTypeExecutorOptions<
ServiceNowPublicConfigurationType,
ServiceNowSecretConfigurationType,
ExecutorParams
>;

// action type definition
export function getServiceNowITSMActionType(params: GetActionTypeParams): ServiceNowActionType {
const { logger, configurationUtilities } = params;
return {
id: ServiceNowITSMActionTypeId,
Expand All @@ -74,14 +80,7 @@ export function getServiceNowITSMActionType(
};
}

export function getServiceNowSIRActionType(
params: GetActionTypeParams
): ActionType<
ServiceNowPublicConfigurationType,
ServiceNowSecretConfigurationType,
ExecutorParams,
PushToServiceResponse | {}
> {
export function getServiceNowSIRActionType(params: GetActionTypeParams): ServiceNowActionType {
const { logger, configurationUtilities } = params;
return {
id: ServiceNowSIRActionTypeId,
Expand All @@ -96,7 +95,12 @@ export function getServiceNowSIRActionType(
}),
params: ExecutorParamsSchemaSIR,
},
executor: curry(executor)({ logger, configurationUtilities, table: serviceNowSIRTable }),
executor: curry(executor)({
logger,
configurationUtilities,
table: serviceNowSIRTable,
commentFieldKey: 'work_notes',
}),
};
}

Expand All @@ -107,12 +111,14 @@ async function executor(
logger,
configurationUtilities,
table,
}: { logger: Logger; configurationUtilities: ActionsConfigurationUtilities; table: string },
execOptions: ActionTypeExecutorOptions<
ServiceNowPublicConfigurationType,
ServiceNowSecretConfigurationType,
ExecutorParams
>
commentFieldKey = 'comments',
}: {
logger: Logger;
configurationUtilities: ActionsConfigurationUtilities;
table: string;
commentFieldKey?: string;
},
execOptions: ServiceNowActionTypeExecutorOptions
): Promise<ActionTypeExecutorResult<ServiceNowExecutorResultData | {}>> {
const { actionId, config, params, secrets } = execOptions;
const { subAction, subActionParams } = params;
Expand Down Expand Up @@ -147,6 +153,7 @@ async function executor(
params: pushToServiceParams,
secrets,
logger,
commentFieldKey,
});

logger.debug(`response push to service for incident id: ${data.id}`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const SERVICENOW_ITSM = i18n.translate('xpack.actions.builtin.serviceNowI
});

export const SERVICENOW_SIR = i18n.translate('xpack.actions.builtin.serviceNowSIRTitle', {
defaultMessage: 'ServiceNow SIR',
defaultMessage: 'ServiceNow SecOps',
});

export const ALLOWED_HOSTS_ERROR = (message: string) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ export interface PushToServiceApiHandlerArgs extends ExternalServiceApiHandlerAr
params: PushToServiceApiParams;
secrets: Record<string, unknown>;
logger: Logger;
commentFieldKey: string;
}

export interface GetIncidentApiHandlerArgs extends ExternalServiceApiHandlerArgs {
Expand Down
Loading