-
Notifications
You must be signed in to change notification settings - Fork 8.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Security Solution] Webhook - Case Management Connector (#131762)
- Loading branch information
1 parent
967ce6d
commit 4f3e554
Showing
78 changed files
with
6,380 additions
and
101 deletions.
There are no files selected for viewing
214 changes: 214 additions & 0 deletions
214
x-pack/plugins/actions/server/builtin_action_types/cases_webhook/api.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
}, | ||
}); | ||
}); | ||
}); | ||
}); |
61 changes: 61 additions & 0 deletions
61
x-pack/plugins/actions/server/builtin_action_types/cases_webhook/api.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; |
117 changes: 117 additions & 0 deletions
117
x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; | ||
} |
Oops, something went wrong.