Implement functionality to add observables, procedures and custom fields to alerts for TheHive (#207255)

## Summary

- Added a toggle to retain the severity from the rule. When enabled,
alerts generated from the rule will inherit its severity; otherwise,
users must manually select a severity level from the dropdown.

- Added a template selection menu with predefined basic templates. These
templates come with preset configurations, including observables and
procedures, which automatically populate the Body field upon selection.
Users also have the option to modify an existing template or create a
custom one using the `Custom Template` option.

## Screenshots
![image
(35)](https://github.com/user-attachments/assets/d7a7b6c8-ae27-4ef4-8396-6625ddbd960c)
![image
(36)](https://github.com/user-attachments/assets/85314883-a2aa-4a9c-b1e3-ebdd9a5c3e29)



### Checklist

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)
- [x]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [x] This was checked for breaking HTTP API changes, and any breaking
changes have been approved by the breaking-change committee. The
`release_note:breaking` label should be applied in these situations.
- [x] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [ ] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Brijesh Khunt 2025-06-19 19:42:59 +05:30 committed by GitHub
parent f857b34115
commit 884e51ae49
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 413 additions and 27 deletions

View file

@ -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.

View file

@ -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,

View file

@ -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([

View file

@ -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}}',
},

View file

@ -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<ActionParamsProps<ExecutorPar
eventActionType === SUB_ACTION.CREATE_ALERT
? {
tlp: 2,
severity: 2,
severity: TheHiveSeverity.MEDIUM,
tags: [],
sourceRef: isTest ? undefined : '{{alert.uuid}}',
}
@ -123,6 +123,7 @@ const TheHiveParamsFields: React.FunctionComponent<ActionParamsProps<ExecutorPar
index={index}
errors={errors}
messageVariables={messageVariables}
executionMode={executionMode}
/>
)}
</>

View file

@ -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,

View file

@ -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<ActionParamsProps<ExecutorParams
index,
errors,
messageVariables,
executionMode,
}) => {
const alert = useMemo(
() =>
@ -33,12 +36,14 @@ export const TheHiveParamsAlertFields: React.FC<ActionParamsProps<ExecutorParams
} as unknown as ExecutorSubActionCreateAlertParams),
[actionParams.subActionParams]
);
const isTest = executionMode === ActionConnectorMode.Test;
const [severity, setSeverity] = useState(alert.severity ?? severityOptions[1].value);
const [tlp, setTlp] = useState(alert.tlp ?? tlpOptions[2].value);
const [selectedOptions, setSelected] = useState<Array<{ label: string }>>(
alert.tags?.map((tag) => ({ label: tag })) ?? []
);
const [isRuleSeverity, setIsRuleSeverity] = useState<boolean>(Boolean(alert.isRuleSeverity));
const onCreateOption = (searchValue: string) => {
setSelected([...selectedOptions, { label: searchValue }]);
@ -149,22 +154,46 @@ export const TheHiveParamsAlertFields: React.FC<ActionParamsProps<ExecutorParams
}}
errors={errors['createAlertParam.sourceRef'] as string[]}
/>
<EuiFormRow fullWidth label={translations.SEVERITY_LABEL}>
<EuiSelect
fullWidth
data-test-subj="severitySelectInput"
value={severity}
options={severityOptions}
onChange={(e) => {
editAction(
'subActionParams',
{ ...alert, severity: parseInt(e.target.value, 10) },
index
);
setSeverity(parseInt(e.target.value, 10));
}}
/>
</EuiFormRow>
{!isTest && Boolean(isRuleSeverity) && (
<EuiFormRow fullWidth>
<EuiSwitch
label={translations.IS_RULE_SEVERITY_LABEL}
checked={Boolean(isRuleSeverity)}
compressed={true}
data-test-subj="rule-severity-toggle"
onChange={(e) => {
setIsRuleSeverity(e.target.checked);
editAction(
'subActionParams',
{
...alert,
isRuleSeverity: e.target.checked,
},
index
);
}}
/>
</EuiFormRow>
)}
{!Boolean(isRuleSeverity) && (
<EuiFormRow fullWidth label={translations.SEVERITY_LABEL}>
<EuiSelect
fullWidth
data-test-subj="severitySelectInput"
disabled={isRuleSeverity}
value={severity}
options={severityOptions}
onChange={(e) => {
editAction(
'subActionParams',
{ ...alert, severity: parseInt(e.target.value, 10) },
index
);
setSeverity(parseInt(e.target.value, 10));
}}
/>
</EuiFormRow>
)}
<EuiFormRow fullWidth label={translations.TLP_LABEL}>
<EuiSelect
fullWidth
@ -187,6 +216,26 @@ export const TheHiveParamsAlertFields: React.FC<ActionParamsProps<ExecutorParams
noSuggestions
/>
</EuiFormRow>
{alert.body != null && (
<JsonEditorWithMessageVariables
messageVariables={messageVariables}
paramsProperty={'body'}
inputTargetValue={alert.body}
label={translations.BODY_LABEL}
ariaLabel={translations.BODY_DESCRIPTION}
errors={errors.body as string[]}
onDocumentsChange={(json: string) =>
editAction('subActionParams', { ...alert, body: json }, index)
}
dataTestSubj="thehive-body"
onBlur={() => {
if (!alert.body) {
editAction('subActionParams', { ...alert, body: null }, index);
}
}}
isOptionalField
/>
)}
</>
);
};

View file

@ -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: [],
};

View file

@ -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',
{

View file

@ -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<TheHiveConfig, TheHiveSecrets>;
@ -41,6 +42,7 @@ export function getConnectorType(): TheHiveConnectorType {
config: TheHiveConfigSchema,
secrets: TheHiveSecretsSchema,
},
renderParameterTemplates,
validators: [{ type: ValidatorType.CONFIG, validator: urlAllowListValidator('url') }],
};
}

View file

@ -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',
});
});
});

View file

@ -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<ExecutorParams> = (
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,
},
};
}
};

View file

@ -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');
});
});
});

View file

@ -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,
},