diff --git a/docs/reference/connectors-kibana/thehive-action-type.md b/docs/reference/connectors-kibana/thehive-action-type.md index f38ac1d075e2e..f8ae8f32f66df 100644 --- a/docs/reference/connectors-kibana/thehive-action-type.md +++ b/docs/reference/connectors-kibana/thehive-action-type.md @@ -70,6 +70,10 @@ Description Severity : The severity of the incident: `LOW`, `MEDIUM`, `HIGH` or `CRITICAL`. + ::::{note} + While creating an alert, use the Keep severity from rule toggle to create an alert with the rule's severity. If the rule does not have a defined severity, the alert will have the default MEDIUM severity. + :::: + TLP : The traffic light protocol designation for the incident: `CLEAR`, `GREEN`, `AMBER`, `AMBER+STRICT` or `RED`. @@ -88,6 +92,27 @@ Source Source reference : A source reference for the alert. +Body +: A Json payload specifying additional parameter, such as observables and procedures. It can be populated using a predefined template or customized using the `Custom Template` option. For example: + + ```json + { + "observables": [ + { + "dataType": "url", + "data": "http://example.org" + } + ], + "procedures": [ + { + "patternId": "TA0001", + "occurDate": 1640000000000, + "tactic": "tactic-name" + } + ] + } + ``` + ## Connector networking configuration [thehive-connector-networking-configuration] Use the [Action configuration settings](/reference/configuration-reference/alerting-settings.md#action-settings) to customize connector networking configurations, such as proxies, certificates, or TLS settings. You can set configurations that apply to all your connectors or use `xpack.actions.customHostSettings` to set per-host configurations. diff --git a/x-pack/platform/plugins/shared/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap b/x-pack/platform/plugins/shared/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap index a7c22bc8dcaee..2d3a80816366e 100644 --- a/x-pack/platform/plugins/shared/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap +++ b/x-pack/platform/plugins/shared/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap @@ -37801,6 +37801,44 @@ Object { "presence": "optional", }, "keys": Object { + "body": Object { + "flags": Object { + "default": null, + "error": [Function], + "presence": "optional", + }, + "matches": Array [ + Object { + "schema": Object { + "flags": Object { + "error": [Function], + }, + "rules": Array [ + Object { + "args": Object { + "method": [Function], + }, + "name": "custom", + }, + ], + "type": "string", + }, + }, + Object { + "schema": Object { + "allow": Array [ + null, + ], + "flags": Object { + "error": [Function], + "only": true, + }, + "type": "any", + }, + }, + ], + "type": "alternatives", + }, "description": Object { "flags": Object { "error": [Function], @@ -37815,6 +37853,38 @@ Object { ], "type": "string", }, + "isRuleSeverity": Object { + "flags": Object { + "default": null, + "error": [Function], + "presence": "optional", + }, + "matches": Array [ + Object { + "schema": Object { + "flags": Object { + "default": false, + "error": [Function], + "presence": "optional", + }, + "type": "boolean", + }, + }, + Object { + "schema": Object { + "allow": Array [ + null, + ], + "flags": Object { + "error": [Function], + "only": true, + }, + "type": "any", + }, + }, + ], + "type": "alternatives", + }, "severity": Object { "flags": Object { "default": null, diff --git a/x-pack/platform/plugins/shared/stack_connectors/common/thehive/schema.ts b/x-pack/platform/plugins/shared/stack_connectors/common/thehive/schema.ts index e880ca900591a..07d48a5c6e8d9 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/common/thehive/schema.ts +++ b/x-pack/platform/plugins/shared/stack_connectors/common/thehive/schema.ts @@ -55,8 +55,10 @@ export const ExecutorSubActionCreateAlertParamsSchema = schema.object({ source: schema.string(), sourceRef: schema.string(), severity: schema.nullable(schema.number({ defaultValue: TheHiveSeverity.MEDIUM })), + isRuleSeverity: schema.nullable(schema.boolean({ defaultValue: false })), tlp: schema.nullable(schema.number({ defaultValue: TheHiveTLP.AMBER })), tags: schema.nullable(schema.arrayOf(schema.string())), + body: schema.nullable(schema.string()), }); export const ExecutorParamsSchema = schema.oneOf([ diff --git a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/thehive/params.test.tsx b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/thehive/params.test.tsx index d69080938fc26..3a9e02484b40b 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/thehive/params.test.tsx +++ b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/thehive/params.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { fireEvent, render } from '@testing-library/react'; import { ActionConnector } from '@kbn/triggers-actions-ui-plugin/public/types'; import TheHiveParamsFields from './params'; -import { SUB_ACTION } from '../../../common/thehive/constants'; +import { SUB_ACTION, TheHiveSeverity } from '../../../common/thehive/constants'; import { ExecutorParams, ExecutorSubActionPushParams } from '../../../common/thehive/types'; describe('TheHiveParamsFields renders', () => { @@ -69,7 +69,7 @@ describe('TheHiveParamsFields renders', () => { 'subActionParams', { tlp: 2, - severity: 2, + severity: TheHiveSeverity.MEDIUM, tags: [], sourceRef: '{{alert.uuid}}', }, diff --git a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/thehive/params.tsx b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/thehive/params.tsx index f0221ce7a460b..be22819a88938 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/thehive/params.tsx +++ b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/thehive/params.tsx @@ -9,7 +9,7 @@ import React, { useState, useEffect, useRef, useMemo } from 'react'; import { ActionParamsProps, ActionConnectorMode } from '@kbn/triggers-actions-ui-plugin/public'; import { EuiFormRow, EuiSelect } from '@elastic/eui'; import { eventActionOptions } from './constants'; -import { SUB_ACTION } from '../../../common/thehive/constants'; +import { SUB_ACTION, TheHiveSeverity } from '../../../common/thehive/constants'; import { ExecutorParams } from '../../../common/thehive/types'; import { TheHiveParamsAlertFields } from './params_alert'; import { TheHiveParamsCaseFields } from './params_case'; @@ -80,7 +80,7 @@ const TheHiveParamsFields: React.FunctionComponent )} diff --git a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/thehive/params_alert.test.tsx b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/thehive/params_alert.test.tsx index 138595bd52690..dfaaa1a7d1351 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/thehive/params_alert.test.tsx +++ b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/thehive/params_alert.test.tsx @@ -18,10 +18,12 @@ describe('TheHiveParamsFields renders', () => { description: 'description test', tlp: 2, severity: 2, + isRuleSeverity: false, tags: ['test1'], source: 'source test', type: 'sourceType test', sourceRef: 'sourceRef test', + body: null, }; const actionParams: ExecutorParams = { subAction: SUB_ACTION.CREATE_ALERT, diff --git a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/thehive/params_alert.tsx b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/thehive/params_alert.tsx index 868298ef98a7c..cbf0a85e5b409 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/thehive/params_alert.tsx +++ b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/thehive/params_alert.tsx @@ -10,8 +10,10 @@ import { TextFieldWithMessageVariables, TextAreaWithMessageVariables, ActionParamsProps, + JsonEditorWithMessageVariables, + ActionConnectorMode, } from '@kbn/triggers-actions-ui-plugin/public'; -import { EuiFormRow, EuiSelect, EuiComboBox } from '@elastic/eui'; +import { EuiFormRow, EuiSelect, EuiComboBox, EuiSwitch } from '@elastic/eui'; import { ExecutorParams, ExecutorSubActionCreateAlertParams } from '../../../common/thehive/types'; import { severityOptions, tlpOptions } from './constants'; import * as translations from './translations'; @@ -22,6 +24,7 @@ export const TheHiveParamsAlertFields: React.FC { const alert = useMemo( () => @@ -33,12 +36,14 @@ export const TheHiveParamsAlertFields: React.FC>( alert.tags?.map((tag) => ({ label: tag })) ?? [] ); + const [isRuleSeverity, setIsRuleSeverity] = useState(Boolean(alert.isRuleSeverity)); const onCreateOption = (searchValue: string) => { setSelected([...selectedOptions, { label: searchValue }]); @@ -149,22 +154,46 @@ export const TheHiveParamsAlertFields: React.FC - - { - editAction( - 'subActionParams', - { ...alert, severity: parseInt(e.target.value, 10) }, - index - ); - setSeverity(parseInt(e.target.value, 10)); - }} - /> - + {!isTest && Boolean(isRuleSeverity) && ( + + { + setIsRuleSeverity(e.target.checked); + editAction( + 'subActionParams', + { + ...alert, + isRuleSeverity: e.target.checked, + }, + index + ); + }} + /> + + )} + {!Boolean(isRuleSeverity) && ( + + { + editAction( + 'subActionParams', + { ...alert, severity: parseInt(e.target.value, 10) }, + index + ); + setSeverity(parseInt(e.target.value, 10)); + }} + /> + + )} + {alert.body != null && ( + + editAction('subActionParams', { ...alert, body: json }, index) + } + dataTestSubj="thehive-body" + onBlur={() => { + if (!alert.body) { + editAction('subActionParams', { ...alert, body: null }, index); + } + }} + isOptionalField + /> + )} ); }; diff --git a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/thehive/thehive.test.tsx b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/thehive/thehive.test.tsx index 654324d22e153..7923da52afbd7 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/thehive/thehive.test.tsx +++ b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/thehive/thehive.test.tsx @@ -92,6 +92,19 @@ describe('thehive createAlert action params validation', () => { type: 'type test', source: 'source test', sourceRef: 'source reference test', + body: JSON.stringify( + { + observables: [ + { + dataType: 'ip', + data: '127.0.0.1', + tags: ['source.ip'], + }, + ], + }, + null, + 2 + ), }, comments: [], }; diff --git a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/thehive/translations.ts b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/thehive/translations.ts index cd2c1ffcf9a63..16f11db04bef0 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/thehive/translations.ts +++ b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/thehive/translations.ts @@ -60,6 +60,13 @@ export const TLP_LABEL = i18n.translate( } ); +export const IS_RULE_SEVERITY_LABEL = i18n.translate( + 'xpack.stackConnectors.components.thehive.isRuleSeverityToggleLabel', + { + defaultMessage: 'Use severity assigned to the rule', + } +); + export const SEVERITY_LABEL = i18n.translate( 'xpack.stackConnectors.components.thehive.severitySelectFieldLabel', { @@ -102,6 +109,34 @@ export const SOURCE_REF_LABEL = i18n.translate( } ); +export const TEMPLATE_LABEL = i18n.translate( + 'xpack.stackConnectors.components.thehive.templateFieldLabel', + { + defaultMessage: 'Template', + } +); + +export const BODY_LABEL = i18n.translate( + 'xpack.stackConnectors.components.thehive.bodyFieldLabel', + { + defaultMessage: 'Body', + } +); + +export const BODY_DESCRIPTION = i18n.translate( + 'xpack.stackConnectors.components.thehive.bodyFieldDescription', + { + defaultMessage: 'Code Editor', + } +); + +export const SELECT_BODY_TEMPLATE_POPOVER_BUTTON = i18n.translate( + 'xpack.stackConnectors.components.thehive.selectBodyTemplatePopoverButton', + { + defaultMessage: 'Select body template', + } +); + export const TITLE_REQUIRED = i18n.translate( 'xpack.stackConnectors.components.thehive.requiredTitleText', { diff --git a/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/thehive/index.ts b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/thehive/index.ts index c1cda3a75bf62..fe4d09e2225e2 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/thehive/index.ts +++ b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/thehive/index.ts @@ -22,6 +22,7 @@ import { } from '../../../common/thehive/schema'; import { THEHIVE_CONNECTOR_ID, THEHIVE_TITLE } from '../../../common/thehive/constants'; import type { TheHiveConfig, TheHiveSecrets } from '../../../common/thehive/types'; +import { renderParameterTemplates } from './render'; export type TheHiveConnectorType = SubActionConnectorType; @@ -41,6 +42,7 @@ export function getConnectorType(): TheHiveConnectorType { config: TheHiveConfigSchema, secrets: TheHiveSecretsSchema, }, + renderParameterTemplates, validators: [{ type: ValidatorType.CONFIG, validator: urlAllowListValidator('url') }], }; } diff --git a/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/thehive/render.test.ts b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/thehive/render.test.ts new file mode 100644 index 0000000000000..3bafc4f5bb5d9 --- /dev/null +++ b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/thehive/render.test.ts @@ -0,0 +1,90 @@ +/* + * 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 { loggingSystemMock } from '@kbn/core/server/mocks'; +import { renderParameterTemplates } from './render'; +import { SUB_ACTION } from '../../../common/thehive/constants'; +import Mustache from 'mustache'; + +const params = { + subAction: SUB_ACTION.CREATE_ALERT, + subActionParams: { + title: 'title', + description: 'description', + type: 'type', + source: 'source', + sourceRef: '{{alert.uuid}}', + tlp: 2, + severity: 1, + isRuleSeverity: true, + body: '{"observables":[{"datatype":"url","data":"{{url}}"}],"tags":["test"]}', + }, +}; + +const variables = { + url: 'https://example.com', + context: { rule: { severity: 'high' } }, + alert: { uuid: 'test123' }, +}; +const logger = loggingSystemMock.createLogger(); + +describe('TheHive - renderParameterTemplates', () => { + it('should rendered subActionParams with variables', () => { + const result = renderParameterTemplates(logger, params, variables); + + expect(result.subActionParams).toEqual({ + title: 'title', + description: 'description', + type: 'type', + source: 'source', + sourceRef: variables.alert.uuid, + tlp: 2, + severity: 3, + isRuleSeverity: true, + body: `{"observables":[{"datatype":"url","data":"${variables.url}"}],"tags":["test"]}`, + }); + }); + + it('should not use rule severity if isRuleSeverity is false', () => { + const paramswithoutRuleSeverity = { + ...params, + subActionParams: { ...params.subActionParams, isRuleSeverity: false }, + }; + const result = renderParameterTemplates(logger, paramswithoutRuleSeverity, variables); + + expect(result.subActionParams).toEqual({ + title: 'title', + description: 'description', + type: 'type', + source: 'source', + sourceRef: variables.alert.uuid, + tlp: 2, + severity: 1, + isRuleSeverity: false, + body: `{"observables":[{"datatype":"url","data":"${variables.url}"}],"tags":["test"]}`, + }); + }); + + it('should render error body', () => { + const errorMessage = 'test error'; + jest.spyOn(Mustache, 'render').mockImplementation(() => { + throw new Error(errorMessage); + }); + const result = renderParameterTemplates(logger, params, variables); + expect(result.subActionParams).toEqual({ + body: 'error rendering mustache template "{"observables":[{"datatype":"url","data":"{{url}}"}],"tags":["test"]}": test error', + description: 'error rendering mustache template "description": test error', + severity: 2, + isRuleSeverity: true, + source: 'error rendering mustache template "source": test error', + sourceRef: 'error rendering mustache template "{{alert.uuid}}": test error', + title: 'error rendering mustache template "title": test error', + tlp: 2, + type: 'error rendering mustache template "type": test error', + }); + }); +}); diff --git a/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/thehive/render.ts b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/thehive/render.ts new file mode 100644 index 0000000000000..4dbfb500b5713 --- /dev/null +++ b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/thehive/render.ts @@ -0,0 +1,52 @@ +/* + * 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 type { ExecutorParams } from '@kbn/actions-plugin/server/sub_action_framework/types'; +import { + renderMustacheObject, + renderMustacheString, +} from '@kbn/actions-plugin/server/lib/mustache_renderer'; +import type { RenderParameterTemplates } from '@kbn/actions-plugin/server/types'; +import { SUB_ACTION } from '../../../common/thehive/constants'; + +function mapSeverity(severity: string): number { + switch (severity) { + case 'low': + return 1; + case 'medium': + return 2; + case 'high': + return 3; + case 'critical': + return 4; + default: + return 2; + } +} + +export const renderParameterTemplates: RenderParameterTemplates = ( + logger, + params, + variables +) => { + if (params?.subAction === SUB_ACTION.PUSH_TO_SERVICE) { + return renderMustacheObject(logger, params, variables); + } else { + return { + ...params, + subActionParams: { + ...renderMustacheObject(logger, params.subActionParams, variables), + severity: + params.subActionParams.isRuleSeverity === true + ? mapSeverity( + renderMustacheString(logger, '{{context.rule.severity}}', variables, 'json') + ) + : params.subActionParams.severity, + }, + }; + } +}; diff --git a/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/thehive/thehive.test.ts b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/thehive/thehive.test.ts index 5972d5da570ef..f24115b434081 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/thehive/thehive.test.ts +++ b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/thehive/thehive.test.ts @@ -390,7 +390,7 @@ describe('TheHiveConnector', () => { papLabel: 'AMBER', follow: true, customFields: [], - observableCount: 0, + observableCount: 1, status: 'New', stage: 'New', extraData: {}, @@ -413,10 +413,34 @@ describe('TheHiveConnector', () => { source: 'alert source', sourceRef: 'test123', severity: 1, + isRuleSeverity: false, tlp: 2, tags: ['tag1', 'tag2'], + body: JSON.stringify( + { + observables: [ + { + dataType: 'url', + data: 'http://example.com', + tags: ['url'], + }, + ], + procedures: [ + { + patternId: 'T1132', + occurDate: 1640000000000, + tactic: 'command-and-control', + }, + ], + }, + null, + 2 + ), }; + const { body, isRuleSeverity, ...restOfAlert } = alert; + const expectedAlertBody = { ...JSON.parse(body || '{}'), ...restOfAlert }; + it('TheHive API call is successful with correct parameters', async () => { await connector.createAlert(alert, connectorUsageCollector); expect(mockRequest).toBeCalledTimes(1); @@ -425,7 +449,7 @@ describe('TheHiveConnector', () => { url: 'https://example.com/api/v1/alert', method: 'post', responseSchema: TheHiveCreateAlertResponseSchema, - data: alert, + data: expectedAlertBody, headers: { Authorization: 'Bearer test123', 'X-Organisation': null, @@ -439,9 +463,9 @@ describe('TheHiveConnector', () => { // @ts-ignore connector.request = mockError; - await expect(connector.createAlert(alert, connectorUsageCollector)).rejects.toThrow( - 'API Error' - ); + await expect( + connector.createAlert(expectedAlertBody, connectorUsageCollector) + ).rejects.toThrow('API Error'); }); }); }); diff --git a/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/thehive/thehive.ts b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/thehive/thehive.ts index fd729b368539e..3f35c9f325310 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/thehive/thehive.ts +++ b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/thehive/thehive.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { i18n } from '@kbn/i18n'; import type { ServiceParams } from '@kbn/actions-plugin/server'; import { CaseConnector } from '@kbn/actions-plugin/server'; import type { AxiosError } from 'axios'; @@ -154,15 +155,35 @@ export class TheHiveConnector extends CaseConnector< return res.data; } + private formatAlertBody(alert: ExecutorSubActionCreateAlertParams) { + try { + const { body, isRuleSeverity, ...restOfAlert } = alert; + const bodyJson = JSON.parse(body || '{}'); + const mergedAlertBody = { ...bodyJson, ...restOfAlert }; + + return mergedAlertBody; + } catch (err) { + throw new Error( + i18n.translate('xpack.stackConnectors.thehive.alertBodyParsingError', { + defaultMessage: 'Error parsing alert body for thehive: {err}', + values: { + err: err.toString(), + }, + }) + ); + } + } + public async createAlert( alert: ExecutorSubActionCreateAlertParams, connectorUsageCollector: ConnectorUsageCollector ) { + const mergedAlertBody = this.formatAlertBody(alert); await this.request( { method: 'post', url: `${this.url}/api/${API_VERSION}/alert`, - data: alert, + data: mergedAlertBody, headers: this.getAuthHeaders(), responseSchema: TheHiveCreateAlertResponseSchema, },