Skip to content

Commit

Permalink
[Security Solution] Webhook - Case Management Connector (#131762)
Browse files Browse the repository at this point in the history
  • Loading branch information
stephmilovic authored Jul 26, 2022
1 parent 967ce6d commit 4f3e554
Show file tree
Hide file tree
Showing 78 changed files with 6,380 additions and 101 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
/*
* 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 { Logger } from '@kbn/core/server';
import { externalServiceMock, apiParams } from './mock';
import { ExternalService } from './types';
import { api } from './api';
let mockedLogger: jest.Mocked<Logger>;

describe('api', () => {
let externalService: jest.Mocked<ExternalService>;

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

describe('create incident - cases', () => {
test('it creates an incident', async () => {
const params = { ...apiParams, externalId: null };
const res = await api.pushToService({
externalService,
params,
logger: mockedLogger,
});

expect(res).toEqual({
id: 'incident-1',
title: 'CK-1',
pushedDate: '2020-04-27T10:59:46.202Z',
url: 'https://siem-kibana.atlassian.net/browse/CK-1',
comments: [
{
commentId: 'case-comment-1',
pushedDate: '2020-04-27T10:59:46.202Z',
},
{
commentId: 'case-comment-2',
pushedDate: '2020-04-27T10:59:46.202Z',
},
],
});
});

test('it creates an incident without comments', async () => {
const params = { ...apiParams, externalId: null, comments: [] };
const res = await api.pushToService({
externalService,
params,
logger: mockedLogger,
});

expect(res).toEqual({
id: 'incident-1',
title: 'CK-1',
pushedDate: '2020-04-27T10:59:46.202Z',
url: 'https://siem-kibana.atlassian.net/browse/CK-1',
});
});

test('it calls createIncident correctly', async () => {
const params = { ...apiParams, incident: { ...apiParams.incident, externalId: null } };
await api.pushToService({ externalService, params, logger: mockedLogger });

expect(externalService.createIncident).toHaveBeenCalledWith({
incident: {
tags: ['kibana', 'elastic'],
description: 'Incident description',
title: 'Incident title',
},
});
expect(externalService.updateIncident).not.toHaveBeenCalled();
});

test('it calls createComment correctly', async () => {
const params = { ...apiParams, incident: { ...apiParams.incident, externalId: null } };
await api.pushToService({ externalService, params, logger: mockedLogger });
expect(externalService.createComment).toHaveBeenCalledTimes(2);
expect(externalService.createComment).toHaveBeenNthCalledWith(1, {
incidentId: 'incident-1',
comment: {
commentId: 'case-comment-1',
comment: 'A comment',
},
});

expect(externalService.createComment).toHaveBeenNthCalledWith(2, {
incidentId: 'incident-1',
comment: {
commentId: 'case-comment-2',
comment: 'Another comment',
},
});
});
});

describe('update incident', () => {
test('it updates an incident', async () => {
const res = await api.pushToService({
externalService,
params: apiParams,
logger: mockedLogger,
});

expect(res).toEqual({
id: 'incident-1',
title: 'CK-1',
pushedDate: '2020-04-27T10:59:46.202Z',
url: 'https://siem-kibana.atlassian.net/browse/CK-1',
comments: [
{
commentId: 'case-comment-1',
pushedDate: '2020-04-27T10:59:46.202Z',
},
{
commentId: 'case-comment-2',
pushedDate: '2020-04-27T10:59:46.202Z',
},
],
});
});

test('it updates an incident without comments', async () => {
const params = { ...apiParams, comments: [] };
const res = await api.pushToService({
externalService,
params,
logger: mockedLogger,
});

expect(res).toEqual({
id: 'incident-1',
title: 'CK-1',
pushedDate: '2020-04-27T10:59:46.202Z',
url: 'https://siem-kibana.atlassian.net/browse/CK-1',
});
});

test('it calls updateIncident correctly', async () => {
const params = { ...apiParams };
await api.pushToService({ externalService, params, logger: mockedLogger });

expect(externalService.updateIncident).toHaveBeenCalledWith({
incidentId: 'incident-3',
incident: {
tags: ['kibana', 'elastic'],
description: 'Incident description',
title: 'Incident title',
},
});
expect(externalService.createIncident).not.toHaveBeenCalled();
});

test('it calls updateIncident correctly without mapping', async () => {
const params = { ...apiParams };
await api.pushToService({ externalService, params, logger: mockedLogger });

expect(externalService.updateIncident).toHaveBeenCalledWith({
incidentId: 'incident-3',
incident: {
description: 'Incident description',
title: 'Incident title',
tags: ['kibana', 'elastic'],
},
});
expect(externalService.createIncident).not.toHaveBeenCalled();
});

test('it calls createComment correctly', async () => {
const params = { ...apiParams };
await api.pushToService({ externalService, params, logger: mockedLogger });
expect(externalService.createComment).toHaveBeenCalledTimes(2);
expect(externalService.createComment).toHaveBeenNthCalledWith(1, {
incidentId: 'incident-1',
comment: {
commentId: 'case-comment-1',
comment: 'A comment',
},
});

expect(externalService.createComment).toHaveBeenNthCalledWith(2, {
incidentId: 'incident-1',
comment: {
commentId: 'case-comment-2',
comment: 'Another comment',
},
});
});

test('it calls createComment correctly without mapping', async () => {
const params = { ...apiParams };
await api.pushToService({ externalService, params, logger: mockedLogger });
expect(externalService.createComment).toHaveBeenCalledTimes(2);
expect(externalService.createComment).toHaveBeenNthCalledWith(1, {
incidentId: 'incident-1',
comment: {
commentId: 'case-comment-1',
comment: 'A comment',
},
});

expect(externalService.createComment).toHaveBeenNthCalledWith(2, {
incidentId: 'incident-1',
comment: {
commentId: 'case-comment-2',
comment: 'Another comment',
},
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* 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 {
ExternalServiceApi,
Incident,
PushToServiceApiHandlerArgs,
PushToServiceResponse,
} from './types';

const pushToServiceHandler = async ({
externalService,
params,
}: PushToServiceApiHandlerArgs): Promise<PushToServiceResponse> => {
const {
incident: { externalId, ...rest },
comments,
} = params;
const incident: Incident = rest;
let res: PushToServiceResponse;

if (externalId != null) {
res = await externalService.updateIncident({
incidentId: externalId,
incident,
});
} else {
res = await externalService.createIncident({
incident,
});
}

if (comments && Array.isArray(comments) && comments.length > 0) {
res.comments = [];
for (const currentComment of comments) {
if (!currentComment.comment) {
continue;
}
await externalService.createComment({
incidentId: res.id,
comment: currentComment,
});
res.comments = [
...(res.comments ?? []),
{
commentId: currentComment.commentId,
pushedDate: res.pushedDate,
},
];
}
}

return res;
};
export const api: ExternalServiceApi = {
pushToService: pushToServiceHandler,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* 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 { curry } from 'lodash';
import { schema } from '@kbn/config-schema';
import { Logger } from '@kbn/core/server';
import { CasesConnectorFeatureId } from '../../../common';
import {
CasesWebhookActionParamsType,
CasesWebhookExecutorResultData,
CasesWebhookPublicConfigurationType,
CasesWebhookSecretConfigurationType,
ExecutorParams,
ExecutorSubActionPushParams,
} from './types';
import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../../types';
import { ActionsConfigurationUtilities } from '../../actions_config';
import { createExternalService } from './service';
import {
ExecutorParamsSchema,
ExternalIncidentServiceConfiguration,
ExternalIncidentServiceSecretConfiguration,
} from './schema';
import { api } from './api';
import { validate } from './validators';
import * as i18n from './translations';

const supportedSubActions: string[] = ['pushToService'];
export type ActionParamsType = CasesWebhookActionParamsType;
export const ActionTypeId = '.cases-webhook';
// action type definition
export function getActionType({
logger,
configurationUtilities,
}: {
logger: Logger;
configurationUtilities: ActionsConfigurationUtilities;
}): ActionType<
CasesWebhookPublicConfigurationType,
CasesWebhookSecretConfigurationType,
ExecutorParams,
CasesWebhookExecutorResultData
> {
return {
id: ActionTypeId,
minimumLicenseRequired: 'gold',
name: i18n.NAME,
validate: {
config: schema.object(ExternalIncidentServiceConfiguration, {
validate: curry(validate.config)(configurationUtilities),
}),
secrets: schema.object(ExternalIncidentServiceSecretConfiguration, {
validate: curry(validate.secrets),
}),
params: ExecutorParamsSchema,
connector: validate.connector,
},
executor: curry(executor)({ logger, configurationUtilities }),
supportedFeatureIds: [CasesConnectorFeatureId],
};
}

// action executor
export async function executor(
{
logger,
configurationUtilities,
}: { logger: Logger; configurationUtilities: ActionsConfigurationUtilities },
execOptions: ActionTypeExecutorOptions<
CasesWebhookPublicConfigurationType,
CasesWebhookSecretConfigurationType,
CasesWebhookActionParamsType
>
): Promise<ActionTypeExecutorResult<CasesWebhookExecutorResultData>> {
const actionId = execOptions.actionId;
const { subAction, subActionParams } = execOptions.params;
let data: CasesWebhookExecutorResultData | undefined;

const externalService = createExternalService(
actionId,
{
config: execOptions.config,
secrets: execOptions.secrets,
},
logger,
configurationUtilities
);

if (!api[subAction]) {
const errorMessage = `[Action][ExternalService] Unsupported subAction type ${subAction}.`;
logger.error(errorMessage);
throw new Error(errorMessage);
}

if (!supportedSubActions.includes(subAction)) {
const errorMessage = `[Action][ExternalService] subAction ${subAction} not implemented.`;
logger.error(errorMessage);
throw new Error(errorMessage);
}

if (subAction === 'pushToService') {
const pushToServiceParams = subActionParams as ExecutorSubActionPushParams;
data = await api.pushToService({
externalService,
params: pushToServiceParams,
logger,
});

logger.debug(`response push to service for case id: ${data.id}`);
}

return { status: 'ok', data, actionId };
}
Loading

0 comments on commit 4f3e554

Please sign in to comment.