TheHive Case Connector (#180138)

## Summary

TheHive is a new case connector, enabling users to seamlessly transfer
elastic cases to TheHive Security Incident Response Platform. This
connector facilitates sub-actions such as creating cases, updating
cases, and adding comments and creating alerts.

**create connector**

![thehive-connector](1e9a3fc5-c17a-40b5-8a49-87cd0fd74863)

**test connector**
1. **create case**


![thehive-params-case-test](2652ea5e-8b47-42d9-9b11-c055efe291b3)

2. **create alert**


![thehive-params-alert-test](8c8759c0-609c-4e34-bc21-35d648e684ab)


### Checklist

Delete any items that are not applicable to this PR.

- [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/packages/kbn-i18n/README.md)
- [ ]
[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] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [x] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [x] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [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 renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)

### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](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: Janki Salvi <jankigaurav.salvi@elastic.co>
Co-authored-by: Janki Salvi <117571355+js-jankisalvi@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Brijesh Khunt 2024-07-30 14:06:21 +05:30 committed by GitHub
parent 65069a5cf7
commit 696190db60
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 3440 additions and 4 deletions

View file

@ -92,6 +92,10 @@ a| <<swimlane-action-type,{swimlane}>>
| Create an incident in {swimlane}.
a| <<thehive-action-type,{thehive}>>
| Create cases and alerts in {thehive}.
a| <<tines-action-type,Tines>>
| Send events to a Tines Story.

View file

@ -0,0 +1,79 @@
[[thehive-action-type]]
== TheHive connector and action
++++
<titleabbrev>TheHive</titleabbrev>
++++
:frontmatter-description: Add a connector that can create cases and alerts in TheHive.
:frontmatter-tags-products: [kibana]
:frontmatter-tags-content-type: [how-to]
:frontmatter-tags-user-goals: [configure]
TheHive connector uses the https://docs.strangebee.com/thehive/api-docs/[TheHive (v1) REST API] to create cases and alerts.
[float]
[[define-thehive-ui]]
=== Create connectors in {kib}
You can create connectors in *{stack-manage-app} > {connectors-ui}*
or as needed when you're creating a rule. For example:
[role="screenshot"]
image::management/connectors/images/thehive-connector.png[TheHive connector]
// NOTE: This is an autogenerated screenshot. Do not edit it directly.
[float]
[[thehive-connector-configuration]]
==== Connector configuration
TheHive connectors have the following configuration properties:
Name:: The name of the connector.
Organisation:: Organisation name in which user intends to create cases or alerts.
URL:: TheHive instance URL.
API Key:: TheHive API key for authentication.
[float]
[[TheHive-action-configuration]]
=== Test connectors
You can test connectors for creating a case or an alert with the <<execute-connector-api,run connector API>> or
as you're creating or editing the connector in {kib}. For example:
[role="screenshot"]
image::management/connectors/images/thehive-params-case-test.png[TheHive case params test]
// NOTE: This is an autogenerated screenshot. Do not edit it directly.
[role="screenshot"]
image::management/connectors/images/thehive-params-alert-test.png[TheHive alert params test]
// NOTE: This is an autogenerated screenshot. Do not edit it directly.
TheHive actions have the following configuration properties.
Event Action:: Action that will be performed in thehive. Supported actions are Create Case (default) and Create Alert.
Title:: Title of the incident.
Description:: The details about the incident.
Severity:: Severity of the incident. This can be one of `LOW`, `MEDIUM`(default), `HIGH` or `CRITICAL`.
TLP:: Traffic Light Protocol designation for the incident. This can be one of `CLEAR`, `GREEN`, `AMBER`(default), `AMBER+STRICT` or `RED`.
Tags:: The keywords or tags about the incident.
Additional comments:: Additional information about the Case.
Type:: Type of the Alert.
Source:: Source of the Alert.
Source Reference:: Source reference of the Alert.
[float]
[[thehive-connector-networking-configuration]]
=== Connector networking configuration
Use the <<action-settings, Action configuration 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.
[float]
[[configure-thehive]]
=== Configure TheHive
To generate an API Key in TheHive:
1. Log in to your TheHive instance.
2. Open profile tab and select the settings.
3. Go to *API Key*.
4. Click *Create* if no API key has been created previously; otherwise, you can view the API key by clicking on *Reveal*.
5. Copy the *API key* value to configure the connector in {kib}.

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View file

@ -19,6 +19,7 @@ include::action-types/servicenow-sir.asciidoc[leveloffset=+1]
include::action-types/servicenow-itom.asciidoc[leveloffset=+1]
include::action-types/swimlane.asciidoc[leveloffset=+1]
include::action-types/slack.asciidoc[leveloffset=+1]
include::action-types/thehive.asciidoc[leveloffset=+1]
include::action-types/tines.asciidoc[leveloffset=+1]
include::action-types/torq.asciidoc[leveloffset=+1]
include::action-types/webhook.asciidoc[leveloffset=+1]

View file

@ -138,7 +138,7 @@ WARNING: This feature is available in {kib} 7.17.4 and 8.3.0 onwards but is not
A boolean value indicating that a footer with a relevant link should be added to emails sent as alerting actions. Default: true.
`xpack.actions.enabledActionTypes` {ess-icon}::
A list of action types that are enabled. It defaults to `["*"]`, enabling all types. The names for built-in {kib} action types are prefixed with a `.` and include: `.email`, `.index`, `.jira`, `.opsgenie`, `.pagerduty`, `.resilient`, `.server-log`, `.servicenow`, .`servicenow-itom`, `.servicenow-sir`, `.slack`, `.swimlane`, `.teams`, `.tines`, `.torq`, `.xmatters`, `.gen-ai`, `.bedrock`, `.gemini`, `.d3security`, and `.webhook`. An empty list `[]` will disable all action types.
A list of action types that are enabled. It defaults to `["*"]`, enabling all types. The names for built-in {kib} action types are prefixed with a `.` and include: `.email`, `.index`, `.jira`, `.opsgenie`, `.pagerduty`, `.resilient`, `.server-log`, `.servicenow`, .`servicenow-itom`, `.servicenow-sir`, `.slack`, `.swimlane`, `.teams`, `.thehive`, `.tines`, `.torq`, `.xmatters`, `.gen-ai`, `.bedrock`, `.gemini`, `.d3security`, and `.webhook`. An empty list `[]` will disable all action types.
+
Disabled action types will not appear as an option when creating new connectors, but existing connectors and actions of that type will remain in {kib} and will not function.

View file

@ -32415,6 +32415,616 @@ Object {
}
`;
exports[`Connector type config checks detect connector type changes for: .thehive 1`] = `
Object {
"flags": Object {
"default": Object {
"special": "deep",
},
"error": [Function],
"presence": "optional",
},
"keys": Object {
"comments": Object {
"flags": Object {
"default": null,
"error": [Function],
"presence": "optional",
},
"matches": Array [
Object {
"schema": Object {
"flags": Object {
"error": [Function],
},
"items": Array [
Object {
"flags": Object {
"default": Object {
"special": "deep",
},
"error": [Function],
"presence": "optional",
},
"keys": Object {
"comment": Object {
"flags": Object {
"error": [Function],
},
"rules": Array [
Object {
"args": Object {
"method": [Function],
},
"name": "custom",
},
],
"type": "string",
},
"commentId": Object {
"flags": Object {
"error": [Function],
},
"rules": Array [
Object {
"args": Object {
"method": [Function],
},
"name": "custom",
},
],
"type": "string",
},
},
"type": "object",
},
],
"type": "array",
},
},
Object {
"schema": Object {
"allow": Array [
null,
],
"flags": Object {
"error": [Function],
"only": true,
},
"type": "any",
},
},
],
"type": "alternatives",
},
"incident": Object {
"flags": Object {
"default": Object {
"special": "deep",
},
"error": [Function],
"presence": "optional",
},
"keys": Object {
"description": Object {
"flags": Object {
"error": [Function],
},
"rules": Array [
Object {
"args": Object {
"method": [Function],
},
"name": "custom",
},
],
"type": "string",
},
"externalId": 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",
},
"severity": Object {
"flags": Object {
"default": null,
"error": [Function],
"presence": "optional",
},
"matches": Array [
Object {
"schema": Object {
"flags": Object {
"error": [Function],
},
"type": "number",
},
},
Object {
"schema": Object {
"allow": Array [
null,
],
"flags": Object {
"error": [Function],
"only": true,
},
"type": "any",
},
},
],
"type": "alternatives",
},
"tags": Object {
"flags": Object {
"default": null,
"error": [Function],
"presence": "optional",
},
"matches": Array [
Object {
"schema": Object {
"flags": Object {
"error": [Function],
},
"items": Array [
Object {
"flags": Object {
"error": [Function],
"presence": "optional",
},
"rules": Array [
Object {
"args": Object {
"method": [Function],
},
"name": "custom",
},
],
"type": "string",
},
],
"type": "array",
},
},
Object {
"schema": Object {
"allow": Array [
null,
],
"flags": Object {
"error": [Function],
"only": true,
},
"type": "any",
},
},
],
"type": "alternatives",
},
"title": Object {
"flags": Object {
"error": [Function],
},
"rules": Array [
Object {
"args": Object {
"method": [Function],
},
"name": "custom",
},
],
"type": "string",
},
"tlp": Object {
"flags": Object {
"default": null,
"error": [Function],
"presence": "optional",
},
"matches": Array [
Object {
"schema": Object {
"flags": Object {
"error": [Function],
},
"type": "number",
},
},
Object {
"schema": Object {
"allow": Array [
null,
],
"flags": Object {
"error": [Function],
"only": true,
},
"type": "any",
},
},
],
"type": "alternatives",
},
},
"type": "object",
},
},
"type": "object",
}
`;
exports[`Connector type config checks detect connector type changes for: .thehive 2`] = `
Object {
"flags": Object {
"default": Object {
"special": "deep",
},
"error": [Function],
"presence": "optional",
},
"keys": Object {
"description": Object {
"flags": Object {
"error": [Function],
},
"rules": Array [
Object {
"args": Object {
"method": [Function],
},
"name": "custom",
},
],
"type": "string",
},
"severity": Object {
"flags": Object {
"default": null,
"error": [Function],
"presence": "optional",
},
"matches": Array [
Object {
"schema": Object {
"flags": Object {
"default": 2,
"error": [Function],
"presence": "optional",
},
"type": "number",
},
},
Object {
"schema": Object {
"allow": Array [
null,
],
"flags": Object {
"error": [Function],
"only": true,
},
"type": "any",
},
},
],
"type": "alternatives",
},
"source": Object {
"flags": Object {
"error": [Function],
},
"rules": Array [
Object {
"args": Object {
"method": [Function],
},
"name": "custom",
},
],
"type": "string",
},
"sourceRef": Object {
"flags": Object {
"error": [Function],
},
"rules": Array [
Object {
"args": Object {
"method": [Function],
},
"name": "custom",
},
],
"type": "string",
},
"tags": Object {
"flags": Object {
"default": null,
"error": [Function],
"presence": "optional",
},
"matches": Array [
Object {
"schema": Object {
"flags": Object {
"error": [Function],
},
"items": Array [
Object {
"flags": Object {
"error": [Function],
"presence": "optional",
},
"rules": Array [
Object {
"args": Object {
"method": [Function],
},
"name": "custom",
},
],
"type": "string",
},
],
"type": "array",
},
},
Object {
"schema": Object {
"allow": Array [
null,
],
"flags": Object {
"error": [Function],
"only": true,
},
"type": "any",
},
},
],
"type": "alternatives",
},
"title": Object {
"flags": Object {
"error": [Function],
},
"rules": Array [
Object {
"args": Object {
"method": [Function],
},
"name": "custom",
},
],
"type": "string",
},
"tlp": Object {
"flags": Object {
"default": null,
"error": [Function],
"presence": "optional",
},
"matches": Array [
Object {
"schema": Object {
"flags": Object {
"default": 2,
"error": [Function],
"presence": "optional",
},
"type": "number",
},
},
Object {
"schema": Object {
"allow": Array [
null,
],
"flags": Object {
"error": [Function],
"only": true,
},
"type": "any",
},
},
],
"type": "alternatives",
},
"type": Object {
"flags": Object {
"error": [Function],
},
"rules": Array [
Object {
"args": Object {
"method": [Function],
},
"name": "custom",
},
],
"type": "string",
},
},
"type": "object",
}
`;
exports[`Connector type config checks detect connector type changes for: .thehive 3`] = `
Object {
"flags": Object {
"default": Object {
"special": "deep",
},
"error": [Function],
"presence": "optional",
},
"keys": Object {
"organisation": 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",
},
"url": Object {
"flags": Object {
"error": [Function],
},
"rules": Array [
Object {
"args": Object {
"method": [Function],
},
"name": "custom",
},
],
"type": "string",
},
},
"type": "object",
}
`;
exports[`Connector type config checks detect connector type changes for: .thehive 4`] = `
Object {
"flags": Object {
"default": Object {
"special": "deep",
},
"error": [Function],
"presence": "optional",
},
"keys": Object {
"apiKey": Object {
"flags": Object {
"error": [Function],
},
"rules": Array [
Object {
"args": Object {
"method": [Function],
},
"name": "custom",
},
],
"type": "string",
},
},
"type": "object",
}
`;
exports[`Connector type config checks detect connector type changes for: .thehive 5`] = `
Object {
"flags": Object {
"default": Object {
"special": "deep",
},
"error": [Function],
"presence": "optional",
},
"keys": Object {
"subAction": Object {
"flags": Object {
"error": [Function],
},
"rules": Array [
Object {
"args": Object {
"method": [Function],
},
"name": "custom",
},
],
"type": "string",
},
"subActionParams": Object {
"flags": Object {
"default": Object {
"special": "deep",
},
"error": [Function],
"presence": "optional",
"unknown": true,
},
"keys": Object {},
"preferences": Object {
"stripUnknown": Object {
"objects": false,
},
},
"type": "object",
},
},
"type": "object",
}
`;
exports[`Connector type config checks detect connector type changes for: .tines 1`] = `
Object {
"flags": Object {

View file

@ -29,6 +29,7 @@ export const connectorTypes: string[] = [
'.gemini',
'.d3security',
'.resilient',
'.thehive',
'.sentinelone',
'.crowdstrike',
'.cases',

View file

@ -8,7 +8,7 @@
import { RecoveredActionGroup } from './builtin_action_groups';
const DisabledActionGroupsByActionType: Record<string, string[]> = {
[RecoveredActionGroup.id]: ['.jira', '.resilient'],
[RecoveredActionGroup.id]: ['.jira', '.resilient', '.thehive'],
};
export const DisabledActionTypeIdsForActionGroup: Map<string, string[]> = new Map(

View file

@ -0,0 +1,34 @@
/*
* 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 { i18n } from '@kbn/i18n';
export const THEHIVE_TITLE = i18n.translate(
'xpack.stackConnectors.components.thehive.connectorTypeTitle',
{
defaultMessage: 'TheHive',
}
);
export const THEHIVE_CONNECTOR_ID = '.thehive';
export enum SUB_ACTION {
PUSH_TO_SERVICE = 'pushToService',
CREATE_ALERT = 'createAlert',
}
export enum TheHiveSeverity {
LOW = 1,
MEDIUM = 2,
HIGH = 3,
CRITICAL = 4,
}
export enum TheHiveTLP {
CLEAR = 0,
GREEN = 1,
AMBER = 2,
AMBER_STRICT = 3,
RED = 4,
}

View file

@ -0,0 +1,186 @@
/*
* 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 { schema } from '@kbn/config-schema';
import { TheHiveSeverity, TheHiveTLP, SUB_ACTION } from './constants';
export const TheHiveConfigSchema = schema.object({
url: schema.string(),
organisation: schema.nullable(schema.string()),
});
export const TheHiveSecretsSchema = schema.object({
apiKey: schema.string(),
});
export const ExecutorSubActionPushParamsSchema = schema.object({
incident: schema.object({
title: schema.string(),
description: schema.string(),
externalId: schema.nullable(schema.string()),
severity: schema.nullable(schema.number({ defaultValue: TheHiveSeverity.MEDIUM })),
tlp: schema.nullable(schema.number({ defaultValue: TheHiveTLP.AMBER })),
tags: schema.nullable(schema.arrayOf(schema.string())),
}),
comments: schema.nullable(
schema.arrayOf(
schema.object({
comment: schema.string(),
commentId: schema.string(),
})
)
),
});
export const PushToServiceIncidentSchema = {
title: schema.string(),
description: schema.string(),
severity: schema.nullable(schema.number()),
tlp: schema.nullable(schema.number()),
tags: schema.nullable(schema.arrayOf(schema.string())),
};
export const ExecutorSubActionGetIncidentParamsSchema = schema.object({
externalId: schema.string(),
});
export const ExecutorSubActionCreateAlertParamsSchema = schema.object({
title: schema.string(),
description: schema.string(),
type: schema.string(),
source: schema.string(),
sourceRef: schema.string(),
severity: schema.nullable(schema.number({ defaultValue: TheHiveSeverity.MEDIUM })),
tlp: schema.nullable(schema.number({ defaultValue: TheHiveTLP.AMBER })),
tags: schema.nullable(schema.arrayOf(schema.string())),
});
export const ExecutorParamsSchema = schema.oneOf([
schema.object({
subAction: schema.literal(SUB_ACTION.PUSH_TO_SERVICE),
subActionParams: ExecutorSubActionPushParamsSchema,
}),
schema.object({
subAction: schema.literal(SUB_ACTION.CREATE_ALERT),
subActionParams: ExecutorSubActionCreateAlertParamsSchema,
}),
]);
export const TheHiveIncidentResponseSchema = schema.object(
{
_id: schema.string(),
_type: schema.string(),
_createdBy: schema.string(),
_updatedBy: schema.nullable(schema.string()),
_createdAt: schema.number(),
_updatedAt: schema.nullable(schema.number()),
number: schema.number(),
title: schema.string(),
description: schema.string(),
severity: schema.number(),
severityLabel: schema.string(),
startDate: schema.number(),
endDate: schema.nullable(schema.number()),
tags: schema.nullable(schema.arrayOf(schema.string())),
flag: schema.boolean(),
tlp: schema.number(),
tlpLabel: schema.string(),
pap: schema.number(),
papLabel: schema.string(),
status: schema.string(),
stage: schema.string(),
summary: schema.nullable(schema.string()),
impactStatus: schema.nullable(schema.string()),
assignee: schema.nullable(schema.string()),
customFields: schema.nullable(schema.arrayOf(schema.recordOf(schema.string(), schema.any()))),
userPermissions: schema.nullable(schema.arrayOf(schema.string())),
extraData: schema.object({}, { unknowns: 'allow' }),
newDate: schema.number(),
inProgressDate: schema.nullable(schema.number()),
closedDate: schema.nullable(schema.number()),
alertDate: schema.nullable(schema.number()),
alertNewDate: schema.nullable(schema.number()),
alertInProgressDate: schema.nullable(schema.number()),
alertImportedDate: schema.nullable(schema.number()),
timeToDetect: schema.number(),
timeToTriage: schema.nullable(schema.number()),
timeToQualify: schema.nullable(schema.number()),
timeToAcknowledge: schema.nullable(schema.number()),
timeToResolve: schema.nullable(schema.number()),
handlingDuration: schema.nullable(schema.number()),
},
{ unknowns: 'ignore' }
);
export const TheHiveUpdateIncidentResponseSchema = schema.any();
export const TheHiveAddCommentResponseSchema = schema.object(
{
_id: schema.string(),
_type: schema.string(),
createdBy: schema.string(),
createdAt: schema.number(),
updatedAt: schema.nullable(schema.number()),
updatedBy: schema.nullable(schema.string()),
message: schema.string(),
isEdited: schema.boolean(),
extraData: schema.object({}, { unknowns: 'allow' }),
},
{ unknowns: 'ignore' }
);
export const TheHiveCreateAlertResponseSchema = schema.object(
{
_id: schema.string(),
_type: schema.string(),
_createdBy: schema.string(),
_updatedBy: schema.nullable(schema.string()),
_createdAt: schema.number(),
_updatedAt: schema.nullable(schema.number()),
type: schema.string(),
source: schema.string(),
sourceRef: schema.string(),
externalLink: schema.nullable(schema.string()),
title: schema.string(),
description: schema.string(),
severity: schema.number(),
severityLabel: schema.string(),
date: schema.number(),
tags: schema.nullable(schema.arrayOf(schema.string())),
tlp: schema.number(),
tlpLabel: schema.string(),
pap: schema.number(),
papLabel: schema.string(),
follow: schema.nullable(schema.boolean()),
customFields: schema.nullable(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))),
caseTemplate: schema.nullable(schema.string()),
observableCount: schema.number(),
caseId: schema.nullable(schema.string()),
status: schema.string(),
stage: schema.string(),
assignee: schema.nullable(schema.string()),
summary: schema.nullable(schema.string()),
extraData: schema.object({}, { unknowns: 'allow' }),
newDate: schema.number(),
inProgressDate: schema.nullable(schema.number()),
closedDate: schema.nullable(schema.number()),
importedDate: schema.nullable(schema.number()),
timeToDetect: schema.number(),
timeToTriage: schema.nullable(schema.number()),
timeToQualify: schema.nullable(schema.number()),
timeToAcknowledge: schema.nullable(schema.number()),
},
{ unknowns: 'ignore' }
);
export const TheHiveFailureResponseSchema = schema.object(
{
type: schema.number(),
message: schema.string(),
},
{ unknowns: 'ignore' }
);

View file

@ -0,0 +1,39 @@
/*
* 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 { TypeOf } from '@kbn/config-schema';
import {
TheHiveConfigSchema,
TheHiveSecretsSchema,
ExecutorParamsSchema,
ExecutorSubActionPushParamsSchema,
ExecutorSubActionCreateAlertParamsSchema,
TheHiveFailureResponseSchema,
TheHiveIncidentResponseSchema,
} from './schema';
export type TheHiveConfig = TypeOf<typeof TheHiveConfigSchema>;
export type TheHiveSecrets = TypeOf<typeof TheHiveSecretsSchema>;
export type ExecutorParams = TypeOf<typeof ExecutorParamsSchema>;
export type ExecutorSubActionPushParams = TypeOf<typeof ExecutorSubActionPushParamsSchema>;
export type ExecutorSubActionCreateAlertParams = TypeOf<
typeof ExecutorSubActionCreateAlertParamsSchema
>;
export type TheHiveFailureResponse = TypeOf<typeof TheHiveFailureResponseSchema>;
export interface ExternalServiceIncidentResponse {
id: string;
title: string;
url: string;
pushedDate: string;
}
export type Incident = Omit<ExecutorSubActionPushParams['incident'], 'externalId'>;
export type GetIncidentResponse = TypeOf<typeof TheHiveIncidentResponseSchema>;

View file

@ -32,6 +32,7 @@ import { getXmattersConnectorType } from './xmatters';
import { getD3SecurityConnectorType } from './d3security';
import { ExperimentalFeaturesService } from '../common/experimental_features_service';
import { getSentinelOneConnectorType } from './sentinelone';
import { getTheHiveConnectorType } from './thehive';
import { getCrowdStrikeConnectorType } from './crowdstrike';
export interface RegistrationServices {
@ -71,6 +72,7 @@ export function registerConnectorTypes({
connectorTypeRegistry.register(getTorqConnectorType());
connectorTypeRegistry.register(getTinesConnectorType());
connectorTypeRegistry.register(getD3SecurityConnectorType());
connectorTypeRegistry.register(getTheHiveConnectorType());
if (ExperimentalFeaturesService.get().sentinelOneConnectorOn) {
connectorTypeRegistry.register(getSentinelOneConnectorType());

View file

@ -0,0 +1,113 @@
/*
* 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 React from 'react';
import TheHiveConnectorFields from './connector';
import { ConnectorFormTestProvider } from '../lib/test_utils';
import { act, render, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
jest.mock('@kbn/triggers-actions-ui-plugin/public/common/lib/kibana');
describe('TheHiveActionConnectorFields renders', () => {
const actionConnector = {
actionTypeId: '.thehive',
name: 'thehive',
config: {
url: 'https://test.com',
},
secrets: {
apiKey: 'apiKey',
},
isDeprecated: false,
};
it('TheHive connector fields are rendered', () => {
const { getByTestId } = render(
<ConnectorFormTestProvider connector={actionConnector}>
<TheHiveConnectorFields
readOnly={false}
isEdit={false}
registerPreSubmitValidator={() => {}}
/>
</ConnectorFormTestProvider>
);
expect(getByTestId('config.url-input')).toBeInTheDocument();
expect(getByTestId('secrets.apiKey-input')).toBeInTheDocument();
});
describe('Validation', () => {
const onSubmit = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
});
const tests: Array<[string, string]> = [
['config.url-input', 'not-valid'],
['secrets.apiKey-input', ''],
];
it('connector validation succeeds when connector config is valid', async () => {
const { getByTestId } = render(
<ConnectorFormTestProvider connector={actionConnector} onSubmit={onSubmit}>
<TheHiveConnectorFields
readOnly={false}
isEdit={false}
registerPreSubmitValidator={() => {}}
/>
</ConnectorFormTestProvider>
);
await act(async () => {
userEvent.click(getByTestId('form-test-provide-submit'));
});
waitFor(() => {
expect(onSubmit).toBeCalledWith({
data: {
actionTypeId: '.thehive',
name: 'thehive',
config: {
url: 'https://test.com',
},
secrets: {
apiKey: 'apiKey',
},
isDeprecated: false,
},
isValid: true,
});
});
});
it.each(tests)('validates correctly %p', async (field, value) => {
const res = render(
<ConnectorFormTestProvider connector={actionConnector} onSubmit={onSubmit}>
<TheHiveConnectorFields
readOnly={false}
isEdit={false}
registerPreSubmitValidator={() => {}}
/>
</ConnectorFormTestProvider>
);
await act(async () => {
await userEvent.type(res.getByTestId(field), `{selectall}{backspace}${value}`, {
delay: 10,
});
});
await act(async () => {
userEvent.click(res.getByTestId('form-test-provide-submit'));
});
expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false });
});
});
});

View file

@ -0,0 +1,51 @@
/*
* 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 React from 'react';
import { ActionConnectorFieldsProps } from '@kbn/triggers-actions-ui-plugin/public';
import {
ConfigFieldSchema,
SimpleConnectorForm,
SecretsFieldSchema,
} from '@kbn/triggers-actions-ui-plugin/public';
import {
URL_LABEL,
API_KEY_LABEL,
ORGANISATION_LABEL,
ORGANISATION_HELP_TEXT,
} from './translations';
const configFormSchema: ConfigFieldSchema[] = [
{
id: 'organisation',
label: ORGANISATION_LABEL,
isRequired: false,
helpText: ORGANISATION_HELP_TEXT,
},
{ id: 'url', label: URL_LABEL, isUrlField: true },
];
const secretsFormSchema: SecretsFieldSchema[] = [
{ id: 'apiKey', label: API_KEY_LABEL, isPasswordField: true },
];
const TheHiveConnectorFields: React.FC<ActionConnectorFieldsProps> = ({ readOnly, isEdit }) => {
return (
<>
<SimpleConnectorForm
isEdit={isEdit}
readOnly={readOnly}
configFormSchema={configFormSchema}
secretsFormSchema={secretsFormSchema}
/>
</>
);
};
// eslint-disable-next-line import/no-default-export
export { TheHiveConnectorFields as default };

View file

@ -0,0 +1,114 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { TheHiveSeverity, TheHiveTLP, SUB_ACTION } from '../../../common/thehive/constants';
export const eventActionOptions = [
{
value: SUB_ACTION.PUSH_TO_SERVICE,
text: i18n.translate(
'xpack.stackConnectors.components.thehive.eventSelectCreateCaseOptionLabel',
{
defaultMessage: 'Create Case',
}
),
},
{
value: SUB_ACTION.CREATE_ALERT,
text: i18n.translate(
'xpack.stackConnectors.components.thehive.eventSelectCreateAlertOptionLabel',
{
defaultMessage: 'Create Alert',
}
),
},
];
export const severityOptions = [
{
value: TheHiveSeverity.LOW,
text: i18n.translate(
'xpack.stackConnectors.components.thehive.eventSelectSeverityLowOptionLabel',
{
defaultMessage: 'LOW',
}
),
},
{
value: TheHiveSeverity.MEDIUM,
text: i18n.translate(
'xpack.stackConnectors.components.thehive.eventSelectSeverityMediumOptionLabel',
{
defaultMessage: 'MEDIUM',
}
),
},
{
value: TheHiveSeverity.HIGH,
text: i18n.translate(
'xpack.stackConnectors.components.thehive.eventSelectSeverityHighOptionLabel',
{
defaultMessage: 'HIGH',
}
),
},
{
value: TheHiveSeverity.CRITICAL,
text: i18n.translate(
'xpack.stackConnectors.components.thehive.eventSelectSeverityCriticalOptionLabel',
{
defaultMessage: 'CRITICAL',
}
),
},
];
export const tlpOptions = [
{
value: TheHiveTLP.CLEAR,
text: i18n.translate(
'xpack.stackConnectors.components.thehive.eventSelectTlpClearOptionLabel',
{
defaultMessage: 'CLEAR',
}
),
},
{
value: TheHiveTLP.GREEN,
text: i18n.translate(
'xpack.stackConnectors.components.thehive.eventSelectTlpGreenOptionLabel',
{
defaultMessage: 'GREEN',
}
),
},
{
value: TheHiveTLP.AMBER,
text: i18n.translate(
'xpack.stackConnectors.components.thehive.eventSelectTlpAmberOptionLabel',
{
defaultMessage: 'AMBER',
}
),
},
{
value: TheHiveTLP.AMBER_STRICT,
text: i18n.translate(
'xpack.stackConnectors.components.thehive.eventSelectTlpAmberStrictOptionLabel',
{
defaultMessage: 'AMBER+STRICT',
}
),
},
{
value: TheHiveTLP.RED,
text: i18n.translate('xpack.stackConnectors.components.thehive.eventSelectTlpRedOptionLabel', {
defaultMessage: 'RED',
}),
},
];

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export { getConnectorType as getTheHiveConnectorType } from './thehive';

View file

@ -0,0 +1,40 @@
/*
* 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 React from 'react';
import { LogoProps } from '../types';
const Logo = (props: LogoProps) => (
<svg
width="28"
height="33"
viewBox="0 0 28 33"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M13.8378 8.32438C15.6152 8.33125 17.125 10.0519 17.1204 11.4358C17.1158 13.0854 14.4143 13.2068 13.8881 13.2045C13.2957 13.2022 10.6604 13.1289 10.665 11.4128C10.5369 10.029 12.0581 8.3198 13.8378 8.32438ZM10.3562 25.4026L17.141 25.4255L17.7289 26.9445L9.75916 26.9171L10.3562 25.4026ZM11.2849 23.2283L11.882 21.7139L15.7022 21.7276L16.29 23.2466L11.2849 23.2283ZM12.7444 19.6037L13.8081 17.033L14.8535 19.6083L12.7444 19.6037ZM4.7449 28.0878C4.2851 28.0878 3.88936 28.0191 3.49362 27.8198C1.84889 27.0889 1.00021 25.1735 1.72994 23.5262C3.58969 19.0447 9.19871 15.8302 12.2297 15.3788L10.2418 20.2544L8.78009 23.879L7.85136 26.0533C7.25203 27.3043 6.0648 28.0924 4.7449 28.0878ZM13.7555 33.0001C11.5823 32.9932 9.80948 31.4031 9.0912 28.9608L18.4449 28.9928C17.7129 31.4971 15.9286 33.0092 13.7555 33.0001ZM24.0471 27.8885C23.6513 28.0855 23.1892 28.1497 22.7958 28.1474C21.4782 28.1428 20.2955 27.3478 19.7717 26.0922L18.8566 23.9134L17.4841 20.4789C17.4841 20.346 17.4178 20.2132 17.3537 20.0826L15.4597 15.3261C18.4884 15.8646 24.0768 19.1157 25.9068 23.5422C26.561 25.3247 25.7627 27.2355 24.0471 27.8885Z"
fill="#E9CF42"
/>
<path
d="M11.3399 8.25105C11.0768 8.25105 10.8229 8.10213 10.704 7.84781L9.61283 5.52917C9.44813 5.17862 9.5991 4.75934 9.94909 4.59209C10.3014 4.42713 10.7177 4.57834 10.8847 4.92889L11.9758 7.24753C12.1405 7.59808 11.9896 8.01736 11.6396 8.18461C11.5412 8.22814 11.4383 8.25105 11.3399 8.25105Z"
fill="#E9CF42"
/>
<path
d="M16.2627 8.25105C16.162 8.25105 16.0614 8.23043 15.963 8.18461C15.613 8.01965 15.462 7.60037 15.6267 7.24753L16.7179 4.92889C16.8826 4.57834 17.3012 4.42713 17.6535 4.59209C18.0035 4.75705 18.1545 5.17633 17.9898 5.52917L16.8986 7.84781C16.7797 8.10213 16.528 8.25105 16.2627 8.25105Z"
fill="#E9CF42"
/>
<path
d="M24.0013 17.9672C23.8847 17.9672 23.768 17.9397 23.6605 17.8778C23.322 17.69 23.2007 17.2615 23.3883 16.9201L26.0921 12.0583L19.925 1.40677H7.67755L1.51952 12.0469L4.45671 16.9018C4.65801 17.234 4.55278 17.667 4.22109 17.8687C3.8894 18.0703 3.45706 17.9649 3.25575 17.6327L0.101253 12.418C-0.0314234 12.1981 -0.0337109 11.9231 0.0943906 11.7009L6.66417 0.350546C6.78999 0.132887 7.02103 0 7.27266 0H20.3299C20.5792 0 20.8125 0.132887 20.9383 0.350546L27.5081 11.7009C27.6317 11.9163 27.6339 12.1797 27.515 12.3951L24.6144 17.6075C24.4863 17.8389 24.2484 17.9672 24.0013 17.9672Z"
fill="#E9CF42"
/>
</svg>
);
// eslint-disable-next-line import/no-default-export
export { Logo as default };

View file

@ -0,0 +1,105 @@
/*
* 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 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 { ExecutorParams, ExecutorSubActionPushParams } from '../../../common/thehive/types';
describe('TheHiveParamsFields renders', () => {
const subActionParams: ExecutorSubActionPushParams = {
incident: {
title: 'title {test}',
description: 'test description',
tlp: 2,
severity: 2,
tags: ['test1'],
externalId: null,
},
comments: [],
};
const actionParams: ExecutorParams = {
subAction: SUB_ACTION.PUSH_TO_SERVICE,
subActionParams,
};
const connector: ActionConnector = {
secrets: {},
config: {},
id: 'test',
actionTypeId: '.test',
name: 'Test',
isPreconfigured: false,
isDeprecated: false,
isSystemAction: false as const,
};
const editAction = jest.fn();
const defaultProps = {
actionConnector: connector,
actionParams,
editAction,
errors: { 'subActionParams.incident.title': [] },
index: 0,
messageVariables: [],
};
beforeEach(() => {
jest.clearAllMocks();
});
it('all Params fields is rendered', () => {
const { getByTestId } = render(<TheHiveParamsFields {...defaultProps} />);
expect(getByTestId('eventActionSelect')).toBeInTheDocument();
expect(getByTestId('eventActionSelect')).toHaveValue(SUB_ACTION.PUSH_TO_SERVICE);
});
it('calls editAction function with the correct arguments', () => {
const { getByTestId } = render(<TheHiveParamsFields {...defaultProps} />);
const eventActionEl = getByTestId('eventActionSelect');
fireEvent.change(eventActionEl, { target: { value: SUB_ACTION.CREATE_ALERT } });
expect(editAction).toHaveBeenCalledWith(
'subActionParams',
{
tlp: 2,
severity: 2,
tags: [],
sourceRef: '{{alert.uuid}}',
},
0
);
fireEvent.change(eventActionEl, { target: { value: SUB_ACTION.PUSH_TO_SERVICE } });
expect(editAction).toHaveBeenCalledWith(
'subActionParams',
{
incident: {
tlp: 2,
severity: 2,
tags: [],
},
comments: [],
},
0
);
});
it('handles the case when subAction is undefined', () => {
const newProps = {
...defaultProps,
actionParams: {
...actionParams,
subAction: undefined,
},
};
render(<TheHiveParamsFields {...newProps} />);
expect(editAction).toHaveBeenCalledWith('subAction', SUB_ACTION.PUSH_TO_SERVICE, 0);
});
});

View file

@ -0,0 +1,133 @@
/*
* 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 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 { ExecutorParams } from '../../../common/thehive/types';
import { TheHiveParamsAlertFields } from './params_alert';
import { TheHiveParamsCaseFields } from './params_case';
import * as translations from './translations';
const TheHiveParamsFields: React.FunctionComponent<ActionParamsProps<ExecutorParams>> = ({
actionConnector,
actionParams,
editAction,
index,
errors,
messageVariables,
executionMode,
}) => {
const [eventAction, setEventAction] = useState(
actionParams.subAction ?? SUB_ACTION.PUSH_TO_SERVICE
);
const actionConnectorRef = useRef(actionConnector?.id ?? '');
const isTest = useMemo(() => executionMode === ActionConnectorMode.Test, [executionMode]);
useEffect(() => {
if (actionConnector != null && actionConnectorRef.current !== actionConnector.id) {
actionConnectorRef.current = actionConnector.id;
editAction(
'subActionParams',
{
incident: {
tlp: 2,
severity: 2,
tags: [],
},
comments: [],
},
index
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [actionConnector]);
useEffect(() => {
if (!actionParams.subAction) {
editAction('subAction', SUB_ACTION.PUSH_TO_SERVICE, index);
}
if (!actionParams.subActionParams) {
editAction(
'subActionParams',
{
incident: {
tlp: 2,
severity: 2,
tags: [],
},
comments: [],
},
index
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [actionParams]);
useEffect(() => {
editAction('subAction', eventAction, index);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [eventAction]);
const setEventActionType = (eventActionType: SUB_ACTION) => {
const subActionParams =
eventActionType === SUB_ACTION.CREATE_ALERT
? {
tlp: 2,
severity: 2,
tags: [],
sourceRef: isTest ? undefined : '{{alert.uuid}}',
}
: {
incident: {
tlp: 2,
severity: 2,
tags: [],
},
comments: [],
};
setEventAction(eventActionType);
editAction('subActionParams', subActionParams, index);
};
return (
<>
<EuiFormRow fullWidth label={translations.EVENT_ACTION_LABEL}>
<EuiSelect
fullWidth
data-test-subj="eventActionSelect"
options={eventActionOptions}
value={eventAction}
onChange={(e) => setEventActionType(e.target.value as SUB_ACTION)}
/>
</EuiFormRow>
{eventAction === SUB_ACTION.PUSH_TO_SERVICE ? (
<TheHiveParamsCaseFields
actionParams={actionParams}
editAction={editAction}
index={index}
errors={errors}
messageVariables={messageVariables}
/>
) : (
<TheHiveParamsAlertFields
actionParams={actionParams}
editAction={editAction}
index={index}
errors={errors}
messageVariables={messageVariables}
/>
)}
</>
);
};
// eslint-disable-next-line import/no-default-export
export { TheHiveParamsFields as default };

View file

@ -0,0 +1,70 @@
/*
* 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 React from 'react';
import { render } from '@testing-library/react';
import { ActionConnector } from '@kbn/triggers-actions-ui-plugin/public/types';
import { TheHiveParamsAlertFields } from './params_alert';
import { SUB_ACTION } from '../../../common/thehive/constants';
import { ExecutorParams, ExecutorSubActionCreateAlertParams } from '../../../common/thehive/types';
describe('TheHiveParamsFields renders', () => {
const subActionParams: ExecutorSubActionCreateAlertParams = {
title: 'title {test}',
description: 'description test',
tlp: 2,
severity: 2,
tags: ['test1'],
source: 'source test',
type: 'sourceType test',
sourceRef: 'sourceRef test',
};
const actionParams: ExecutorParams = {
subAction: SUB_ACTION.CREATE_ALERT,
subActionParams,
};
const connector: ActionConnector = {
secrets: {},
config: {},
id: 'test',
actionTypeId: '.test',
name: 'Test',
isPreconfigured: false,
isDeprecated: false,
isSystemAction: false as const,
};
const editAction = jest.fn();
const defaultProps = {
actionConnector: connector,
actionParams,
editAction,
errors: { 'subActionParams.incident.title': [] },
index: 0,
messageVariables: [],
};
beforeEach(() => {
jest.clearAllMocks();
});
it('all Params fields is rendered', () => {
const { getByTestId } = render(<TheHiveParamsAlertFields {...defaultProps} />);
expect(getByTestId('titleInput')).toBeInTheDocument();
expect(getByTestId('descriptionTextArea')).toBeInTheDocument();
expect(getByTestId('tagsInput')).toBeInTheDocument();
expect(getByTestId('severitySelectInput')).toBeInTheDocument();
expect(getByTestId('tlpSelectInput')).toBeInTheDocument();
expect(getByTestId('typeInput')).toBeInTheDocument();
expect(getByTestId('sourceInput')).toBeInTheDocument();
expect(getByTestId('sourceRefInput')).toBeInTheDocument();
expect(getByTestId('severitySelectInput')).toHaveValue('2');
expect(getByTestId('tlpSelectInput')).toHaveValue('2');
});
});

View file

@ -0,0 +1,192 @@
/*
* 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 React, { useState, useMemo } from 'react';
import {
TextFieldWithMessageVariables,
TextAreaWithMessageVariables,
ActionParamsProps,
} from '@kbn/triggers-actions-ui-plugin/public';
import { EuiFormRow, EuiSelect, EuiComboBox } from '@elastic/eui';
import { ExecutorParams, ExecutorSubActionCreateAlertParams } from '../../../common/thehive/types';
import { severityOptions, tlpOptions } from './constants';
import * as translations from './translations';
export const TheHiveParamsAlertFields: React.FC<ActionParamsProps<ExecutorParams>> = ({
actionParams,
editAction,
index,
errors,
messageVariables,
}) => {
const alert = useMemo(
() =>
(actionParams.subActionParams as ExecutorSubActionCreateAlertParams) ??
({
tlp: 2,
severity: 2,
tags: [],
} as unknown as ExecutorSubActionCreateAlertParams),
[actionParams.subActionParams]
);
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 onCreateOption = (searchValue: string) => {
setSelected([...selectedOptions, { label: searchValue }]);
editAction('subActionParams', { ...alert, tags: [...(alert.tags ?? []), searchValue] }, index);
};
const onChange = (selectedOptionList: Array<{ label: string }>) => {
setSelected(selectedOptionList);
editAction(
'subActionParams',
{ ...alert, tags: selectedOptionList.map((option) => option.label) },
index
);
};
return (
<>
<TextFieldWithMessageVariables
index={index}
editAction={(key, value) => {
editAction('subActionParams', { ...alert, [key]: value }, index);
}}
messageVariables={messageVariables}
paramsProperty={'title'}
inputTargetValue={alert.title ?? undefined}
wrapField={true}
formRowProps={{
label: translations.TITLE_LABEL,
fullWidth: true,
helpText: '',
isInvalid:
errors['createAlertParam.title'] !== undefined &&
errors['createAlertParam.title'].length > 0 &&
alert.title !== undefined,
error: errors['createAlertParam.title'] as string,
}}
errors={errors['createAlertParam.title'] as string[]}
/>
<TextAreaWithMessageVariables
index={index}
label={translations.DESCRIPTION_LABEL}
editAction={(key, value) => {
editAction('subActionParams', { ...alert, [key]: value }, index);
}}
messageVariables={messageVariables}
paramsProperty={'description'}
inputTargetValue={alert.description ?? undefined}
errors={errors['createAlertParam.description'] as string[]}
/>
<TextFieldWithMessageVariables
index={index}
editAction={(key, value) => {
editAction('subActionParams', { ...alert, [key]: value }, index);
}}
paramsProperty={'type'}
inputTargetValue={alert.type ?? undefined}
wrapField={true}
formRowProps={{
label: translations.TYPE_LABEL,
fullWidth: true,
helpText: '',
isInvalid:
errors['createAlertParam.type'] !== undefined &&
errors['createAlertParam.type'].length > 0 &&
alert.type !== undefined,
error: errors['createAlertParam.type'] as string,
}}
errors={errors['createAlertParam.type'] as string[]}
/>
<TextFieldWithMessageVariables
index={index}
editAction={(key, value) => {
editAction('subActionParams', { ...alert, [key]: value }, index);
}}
paramsProperty={'source'}
inputTargetValue={alert.source ?? undefined}
wrapField={true}
formRowProps={{
label: translations.SOURCE_LABEL,
fullWidth: true,
helpText: '',
isInvalid:
errors['createAlertParam.source'] !== undefined &&
errors['createAlertParam.source'].length > 0 &&
alert.source !== undefined,
error: errors['createAlertParam.source'] as string,
}}
errors={errors['createAlertParam.source'] as string[]}
/>
<TextFieldWithMessageVariables
index={index}
editAction={(key, value) => {
editAction('subActionParams', { ...alert, [key]: value }, index);
}}
messageVariables={messageVariables}
paramsProperty={'sourceRef'}
inputTargetValue={alert.sourceRef ?? undefined}
wrapField={true}
formRowProps={{
label: translations.SOURCE_REF_LABEL,
fullWidth: true,
helpText: '',
isInvalid:
errors['createAlertParam.sourceRef'] !== undefined &&
errors['createAlertParam.sourceRef'].length > 0 &&
alert.sourceRef !== undefined,
error: errors['createAlertParam.sourceRef'] as string,
}}
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>
<EuiFormRow fullWidth label={translations.TLP_LABEL}>
<EuiSelect
fullWidth
data-test-subj="tlpSelectInput"
value={tlp}
options={tlpOptions}
onChange={(e) => {
editAction('subActionParams', { ...alert, tlp: parseInt(e.target.value, 10) }, index);
setTlp(parseInt(e.target.value, 10));
}}
/>
</EuiFormRow>
<EuiFormRow fullWidth label={translations.TAGS_LABEL}>
<EuiComboBox
data-test-subj="tagsInput"
fullWidth
selectedOptions={selectedOptions}
onCreateOption={onCreateOption}
onChange={onChange}
noSuggestions
/>
</EuiFormRow>
</>
);
};

View file

@ -0,0 +1,69 @@
/*
* 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 React from 'react';
import { 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 { ExecutorParams, ExecutorSubActionPushParams } from '../../../common/thehive/types';
describe('TheHiveParamsFields renders', () => {
const subActionParams: ExecutorSubActionPushParams = {
incident: {
title: 'title {test}',
description: 'test description',
tlp: 2,
severity: 2,
tags: ['test1'],
externalId: null,
},
comments: [],
};
const actionParams: ExecutorParams = {
subAction: SUB_ACTION.PUSH_TO_SERVICE,
subActionParams,
};
const connector: ActionConnector = {
secrets: {},
config: {},
id: 'test',
actionTypeId: '.test',
name: 'Test',
isPreconfigured: false,
isDeprecated: false,
isSystemAction: false as const,
};
const editAction = jest.fn();
const defaultProps = {
actionConnector: connector,
actionParams,
editAction,
errors: { 'subActionParams.incident.title': [] },
index: 0,
messageVariables: [],
};
beforeEach(() => {
jest.clearAllMocks();
});
it('all Params fields is rendered', () => {
const { getByTestId } = render(<TheHiveParamsFields {...defaultProps} />);
expect(getByTestId('titleInput')).toBeInTheDocument();
expect(getByTestId('descriptionTextArea')).toBeInTheDocument();
expect(getByTestId('tagsInput')).toBeInTheDocument();
expect(getByTestId('severitySelectInput')).toBeInTheDocument();
expect(getByTestId('tlpSelectInput')).toBeInTheDocument();
expect(getByTestId('commentsTextArea')).toBeInTheDocument();
expect(getByTestId('severitySelectInput')).toHaveValue('2');
expect(getByTestId('tlpSelectInput')).toHaveValue('2');
});
});

View file

@ -0,0 +1,154 @@
/*
* 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 React, { useState, useMemo, useCallback } from 'react';
import {
TextFieldWithMessageVariables,
TextAreaWithMessageVariables,
ActionParamsProps,
} from '@kbn/triggers-actions-ui-plugin/public';
import { EuiFormRow, EuiSelect, EuiComboBox } from '@elastic/eui';
import { ExecutorParams, ExecutorSubActionPushParams } from '../../../common/thehive/types';
import { severityOptions, tlpOptions } from './constants';
import * as translations from './translations';
export const TheHiveParamsCaseFields: React.FC<ActionParamsProps<ExecutorParams>> = ({
actionParams,
editAction,
index,
errors,
messageVariables,
}) => {
const { incident, comments } = useMemo(
() =>
(actionParams.subActionParams as ExecutorSubActionPushParams) ??
({
incident: {
tlp: 2,
severity: 2,
tags: [],
},
comments: [],
} as unknown as ExecutorSubActionPushParams),
[actionParams.subActionParams]
);
const [severity, setSeverity] = useState(incident.severity ?? severityOptions[1].value);
const [tlp, setTlp] = useState(incident.tlp ?? tlpOptions[2].value);
const [selectedOptions, setSelected] = useState<Array<{ label: string }>>(
incident.tags?.map((tag) => ({ label: tag })) ?? []
);
const editSubActionProperty = useCallback(
(key: string, value: any) => {
const newProps =
key !== 'comments'
? {
incident: { ...incident, [key]: value },
comments,
}
: { incident, [key]: value };
editAction('subActionParams', newProps, index);
},
[comments, editAction, incident, index]
);
const editComment = useCallback(
(key, value) => {
editSubActionProperty(key, [{ commentId: '1', comment: value }]);
},
[editSubActionProperty]
);
const onCreateOption = (searchValue: string) => {
setSelected([...selectedOptions, { label: searchValue }]);
editSubActionProperty('tags', [...(incident.tags ?? []), searchValue]);
};
const onChange = (selectedOptionList: Array<{ label: string }>) => {
setSelected(selectedOptionList);
editSubActionProperty(
'tags',
selectedOptionList.map((option) => option.label)
);
};
return (
<>
<TextFieldWithMessageVariables
index={index}
editAction={editSubActionProperty}
messageVariables={messageVariables}
paramsProperty={'title'}
inputTargetValue={incident.title ?? undefined}
wrapField={true}
formRowProps={{
label: translations.TITLE_LABEL,
fullWidth: true,
helpText: '',
isInvalid:
errors['pushToServiceParam.incident.title'] !== undefined &&
errors['pushToServiceParam.incident.title'].length > 0 &&
incident.title !== undefined,
error: errors['pushToServiceParam.incident.title'] as string,
}}
errors={errors['pushToServiceParam.incident.title'] as string[]}
/>
<TextAreaWithMessageVariables
index={index}
label={translations.DESCRIPTION_LABEL}
editAction={editSubActionProperty}
messageVariables={messageVariables}
paramsProperty={'description'}
inputTargetValue={incident.description ?? undefined}
errors={errors['pushToServiceParam.incident.description'] as string[]}
/>
<EuiFormRow fullWidth error={errors.severity} label={translations.SEVERITY_LABEL}>
<EuiSelect
fullWidth
data-test-subj="severitySelectInput"
value={severity}
options={severityOptions}
onChange={(e) => {
editSubActionProperty('severity', parseInt(e.target.value, 10));
setSeverity(parseInt(e.target.value, 10));
}}
/>
</EuiFormRow>
<EuiFormRow fullWidth error={errors.tlp} label={translations.TLP_LABEL}>
<EuiSelect
fullWidth
value={tlp}
data-test-subj="tlpSelectInput"
options={tlpOptions}
onChange={(e) => {
editSubActionProperty('tlp', parseInt(e.target.value, 10));
setTlp(parseInt(e.target.value, 10));
}}
/>
</EuiFormRow>
<EuiFormRow fullWidth label={translations.TAGS_LABEL}>
<EuiComboBox
data-test-subj="tagsInput"
fullWidth
selectedOptions={selectedOptions}
onCreateOption={onCreateOption}
onChange={onChange}
noSuggestions
/>
</EuiFormRow>
<TextAreaWithMessageVariables
index={index}
editAction={editComment}
messageVariables={messageVariables}
paramsProperty={'comments'}
inputTargetValue={comments && comments.length > 0 ? comments[0].comment : undefined}
label={translations.COMMENTS_LABEL}
/>
</>
);
};

View file

@ -0,0 +1,137 @@
/*
* 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 { TypeRegistry } from '@kbn/triggers-actions-ui-plugin/public/application/type_registry';
import { registerConnectorTypes } from '..';
import { ActionTypeModel as ConnectorTypeModel } from '@kbn/triggers-actions-ui-plugin/public/types';
import { experimentalFeaturesMock, registrationServicesMock } from '../../mocks';
import { SUB_ACTION } from '../../../common/thehive/constants';
import { ExperimentalFeaturesService } from '../../common/experimental_features_service';
const CONNECTOR_TYPE_ID = '.thehive';
let connectorTypeModel: ConnectorTypeModel;
beforeAll(() => {
const connectorTypeRegistry = new TypeRegistry<ConnectorTypeModel>();
ExperimentalFeaturesService.init({ experimentalFeatures: experimentalFeaturesMock });
registerConnectorTypes({ connectorTypeRegistry, services: registrationServicesMock });
const getResult = connectorTypeRegistry.get(CONNECTOR_TYPE_ID);
if (getResult !== null) {
connectorTypeModel = getResult;
}
});
describe('actionTypeRegistry.get() works', () => {
test('action type static data is as expected', () => {
expect(connectorTypeModel.id).toEqual(CONNECTOR_TYPE_ID);
});
});
describe('thehive pushToService action params validation', () => {
test('pushToService action params validation succeeds when action params is valid', async () => {
const actionParams = {
subAction: SUB_ACTION.PUSH_TO_SERVICE,
subActionParams: {
incident: {
title: 'title {test}',
description: 'test description',
},
},
comments: [],
};
expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
errors: {
'pushToServiceParam.incident.title': [],
'pushToServiceParam.incident.description': [],
'createAlertParam.title': [],
'createAlertParam.description': [],
'createAlertParam.type': [],
'createAlertParam.source': [],
'createAlertParam.sourceRef': [],
},
});
});
test('pushToService action params validation fails when Required fields is not valid', async () => {
const actionParams = {
subAction: SUB_ACTION.PUSH_TO_SERVICE,
subActionParams: {
incident: {
title: '',
description: '',
},
},
comments: [],
};
expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
errors: {
'pushToServiceParam.incident.title': ['Title is required.'],
'pushToServiceParam.incident.description': ['Description is required.'],
'createAlertParam.title': [],
'createAlertParam.description': [],
'createAlertParam.type': [],
'createAlertParam.source': [],
'createAlertParam.sourceRef': [],
},
});
});
});
describe('thehive createAlert action params validation', () => {
test('createAlert action params validation succeeds when action params is valid', async () => {
const actionParams = {
subAction: SUB_ACTION.CREATE_ALERT,
subActionParams: {
title: 'some title {test}',
description: 'some description {test}',
type: 'type test',
source: 'source test',
sourceRef: 'source reference test',
},
comments: [],
};
expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
errors: {
'pushToServiceParam.incident.title': [],
'pushToServiceParam.incident.description': [],
'createAlertParam.title': [],
'createAlertParam.description': [],
'createAlertParam.type': [],
'createAlertParam.source': [],
'createAlertParam.sourceRef': [],
},
});
});
test('params validation fails when Required fields is not valid', async () => {
const actionParams = {
subAction: SUB_ACTION.CREATE_ALERT,
subActionParams: {
title: '',
description: '',
type: '',
source: '',
sourceRef: '',
},
comments: [],
};
expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
errors: {
'pushToServiceParam.incident.title': [],
'pushToServiceParam.incident.description': [],
'createAlertParam.title': ['Title is required.'],
'createAlertParam.description': ['Description is required.'],
'createAlertParam.type': ['Type is required.'],
'createAlertParam.source': ['Source is required.'],
'createAlertParam.sourceRef': ['Source Reference is required.'],
},
});
});
});

View file

@ -0,0 +1,86 @@
/*
* 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 { lazy } from 'react';
import { i18n } from '@kbn/i18n';
import { GenericValidationResult } from '@kbn/triggers-actions-ui-plugin/public/types';
import { TheHiveConnector } from './types';
import { THEHIVE_CONNECTOR_ID, SUB_ACTION, THEHIVE_TITLE } from '../../../common/thehive/constants';
import {
ExecutorParams,
ExecutorSubActionPushParams,
ExecutorSubActionCreateAlertParams,
} from '../../../common/thehive/types';
export function getConnectorType(): TheHiveConnector {
return {
id: THEHIVE_CONNECTOR_ID,
iconClass: lazy(() => import('./logo')),
selectMessage: i18n.translate('xpack.stackConnectors.components.thehive.descriptionText', {
defaultMessage: 'Create cases and alerts in TheHive',
}),
actionTypeTitle: THEHIVE_TITLE,
hideInUi: true,
validateParams: async (
actionParams: ExecutorParams
): Promise<GenericValidationResult<unknown>> => {
const translations = await import('./translations');
const errors = {
'pushToServiceParam.incident.title': new Array<string>(),
'pushToServiceParam.incident.description': new Array<string>(),
'createAlertParam.title': new Array<string>(),
'createAlertParam.description': new Array<string>(),
'createAlertParam.type': new Array<string>(),
'createAlertParam.source': new Array<string>(),
'createAlertParam.sourceRef': new Array<string>(),
};
const validationResult = {
errors,
};
const { subAction, subActionParams } = actionParams;
if (subAction === SUB_ACTION.PUSH_TO_SERVICE) {
const pushToServiceParam = subActionParams as ExecutorSubActionPushParams;
if (pushToServiceParam && pushToServiceParam.incident) {
if (!pushToServiceParam.incident.title?.length) {
errors['pushToServiceParam.incident.title'].push(translations.TITLE_REQUIRED);
}
if (!pushToServiceParam.incident.description?.length) {
errors['pushToServiceParam.incident.description'].push(
translations.DESCRIPTION_REQUIRED
);
}
}
} else if (subAction === SUB_ACTION.CREATE_ALERT) {
const createAlertParam = subActionParams as ExecutorSubActionCreateAlertParams;
if (createAlertParam) {
if (!createAlertParam.title?.length) {
errors['createAlertParam.title'].push(translations.TITLE_REQUIRED);
}
if (!createAlertParam.description?.length) {
errors['createAlertParam.description'].push(translations.DESCRIPTION_REQUIRED);
}
if (!createAlertParam.type?.length) {
errors['createAlertParam.type'].push(translations.TYPE_REQUIRED);
}
if (!createAlertParam.source?.length) {
errors['createAlertParam.source'].push(translations.SOURCE_REQUIRED);
}
if (!createAlertParam.sourceRef?.length) {
errors['createAlertParam.sourceRef'].push(translations.SOURCE_REF_REQUIRED);
}
}
}
return validationResult;
},
actionConnectorFields: lazy(() => import('./connector')),
actionParamsFields: lazy(() => import('./params')),
};
}

View file

@ -0,0 +1,138 @@
/*
* 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 { i18n } from '@kbn/i18n';
export const URL_LABEL = i18n.translate('xpack.stackConnectors.components.thehive.urlFieldLabel', {
defaultMessage: 'URL',
});
export const ORGANISATION_LABEL = i18n.translate(
'xpack.stackConnectors.components.thehive.organisationFieldLabel',
{
defaultMessage: 'Organisation',
}
);
export const ORGANISATION_HELP_TEXT = i18n.translate(
'xpack.stackConnectors.components.thehive.organisationFieldHelpText',
{
defaultMessage: `By default, the user's default organization will be considered.`,
}
);
export const API_KEY_LABEL = i18n.translate(
'xpack.stackConnectors.components.thehive.apiKeyFieldLabel',
{
defaultMessage: 'API Key',
}
);
export const EVENT_ACTION_LABEL = i18n.translate(
'xpack.stackConnectors.components.thehive.eventActionSelectFieldLabel',
{
defaultMessage: 'Event Action',
}
);
export const TITLE_LABEL = i18n.translate(
'xpack.stackConnectors.components.thehive.titleFieldLabel',
{
defaultMessage: 'Title*',
}
);
export const DESCRIPTION_LABEL = i18n.translate(
'xpack.stackConnectors.components.thehive.descriptionFieldLabel',
{
defaultMessage: 'Description*',
}
);
export const TLP_LABEL = i18n.translate(
'xpack.stackConnectors.components.thehive.tlpSelectFieldLabel',
{
defaultMessage: 'TLP',
}
);
export const SEVERITY_LABEL = i18n.translate(
'xpack.stackConnectors.components.thehive.severitySelectFieldLabel',
{
defaultMessage: 'Severity',
}
);
export const TAGS_LABEL = i18n.translate(
'xpack.stackConnectors.components.thehive.TagsMultiSelectFieldLabel',
{
defaultMessage: 'Tags',
}
);
export const COMMENTS_LABEL = i18n.translate(
'xpack.stackConnectors.components.thehive.commentsTextAreaFieldLabel',
{
defaultMessage: 'Additional comments',
}
);
export const TYPE_LABEL = i18n.translate(
'xpack.stackConnectors.components.thehive.typeFieldLabel',
{
defaultMessage: 'Type*',
}
);
export const SOURCE_LABEL = i18n.translate(
'xpack.stackConnectors.components.thehive.sourceFieldLabel',
{
defaultMessage: 'Source*',
}
);
export const SOURCE_REF_LABEL = i18n.translate(
'xpack.stackConnectors.components.thehive.sourceRefFieldLabel',
{
defaultMessage: 'Source Reference*',
}
);
export const TITLE_REQUIRED = i18n.translate(
'xpack.stackConnectors.components.thehive.requiredTitleText',
{
defaultMessage: 'Title is required.',
}
);
export const DESCRIPTION_REQUIRED = i18n.translate(
'xpack.stackConnectors.components.thehive.requiredDescriptionText',
{
defaultMessage: 'Description is required.',
}
);
export const TYPE_REQUIRED = i18n.translate(
'xpack.stackConnectors.components.thehive.requiredTypeText',
{
defaultMessage: 'Type is required.',
}
);
export const SOURCE_REQUIRED = i18n.translate(
'xpack.stackConnectors.components.thehive.requiredSourceText',
{
defaultMessage: 'Source is required.',
}
);
export const SOURCE_REF_REQUIRED = i18n.translate(
'xpack.stackConnectors.components.thehive.requiredSourceRefText',
{
defaultMessage: 'Source Reference is required.',
}
);

View file

@ -0,0 +1,11 @@
/*
* 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 { ActionTypeModel as ConnectorTypeModel } from '@kbn/triggers-actions-ui-plugin/public';
import { TheHiveConfig, TheHiveSecrets, ExecutorParams } from '../../../common/thehive/types';
export type TheHiveConnector = ConnectorTypeModel<TheHiveConfig, TheHiveSecrets, ExecutorParams>;

View file

@ -29,6 +29,7 @@ import { getConnectorType as getWebhookConnectorType } from './webhook';
import { getConnectorType as getXmattersConnectorType } from './xmatters';
import { getConnectorType as getTeamsConnectorType } from './teams';
import { getConnectorType as getD3SecurityConnectorType } from './d3security';
import { getConnectorType as getTheHiveConnectorType } from './thehive';
import { getOpsgenieConnectorType } from './opsgenie';
import type { ActionParamsType as ServiceNowITSMActionParams } from './servicenow_itsm';
import type { ActionParamsType as ServiceNowSIRActionParams } from './servicenow_sir';
@ -109,6 +110,7 @@ export function registerConnectorTypes({
actions.registerSubActionConnectorType(getGeminiConnectorType());
actions.registerSubActionConnectorType(getD3SecurityConnectorType());
actions.registerSubActionConnectorType(getResilientConnectorType());
actions.registerSubActionConnectorType(getTheHiveConnectorType());
if (experimentalFeatures.sentinelOneConnectorOn) {
actions.registerSubActionConnectorType(getSentinelOneConnectorType());

View file

@ -0,0 +1,20 @@
/*
* 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 { TheHiveConnectorType, getConnectorType } from '.';
let connectorType: TheHiveConnectorType;
describe('TheHive Connector', () => {
beforeEach(() => {
connectorType = getConnectorType();
});
test('exposes the connector as `TheHive` with id `.thehive`', () => {
expect(connectorType.id).toEqual('.thehive');
expect(connectorType.name).toEqual('TheHive');
});
});

View file

@ -0,0 +1,46 @@
/*
* 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 {
SubActionConnectorType,
ValidatorType,
} from '@kbn/actions-plugin/server/sub_action_framework/types';
import {
AlertingConnectorFeatureId,
SecurityConnectorFeatureId,
UptimeConnectorFeatureId,
} from '@kbn/actions-plugin/common/types';
import { urlAllowListValidator } from '@kbn/actions-plugin/server';
import { TheHiveConnector } from './thehive';
import {
TheHiveConfigSchema,
TheHiveSecretsSchema,
PushToServiceIncidentSchema,
} from '../../../common/thehive/schema';
import { THEHIVE_CONNECTOR_ID, THEHIVE_TITLE } from '../../../common/thehive/constants';
import { TheHiveConfig, TheHiveSecrets } from '../../../common/thehive/types';
export type TheHiveConnectorType = SubActionConnectorType<TheHiveConfig, TheHiveSecrets>;
export function getConnectorType(): TheHiveConnectorType {
return {
id: THEHIVE_CONNECTOR_ID,
minimumLicenseRequired: 'platinum',
name: THEHIVE_TITLE,
getService: (params) => new TheHiveConnector(params, PushToServiceIncidentSchema),
supportedFeatureIds: [
AlertingConnectorFeatureId,
SecurityConnectorFeatureId,
UptimeConnectorFeatureId,
],
schema: {
config: TheHiveConfigSchema,
secrets: TheHiveSecretsSchema,
},
validators: [{ type: ValidatorType.CONFIG, validator: urlAllowListValidator('url') }],
};
}

View file

@ -0,0 +1,409 @@
/*
* 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 { TheHiveConnector } from './thehive';
import { actionsConfigMock } from '@kbn/actions-plugin/server/actions_config.mock';
import { THEHIVE_CONNECTOR_ID } from '../../../common/thehive/constants';
import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
import { actionsMock } from '@kbn/actions-plugin/server/mocks';
import {
TheHiveIncidentResponseSchema,
TheHiveUpdateIncidentResponseSchema,
TheHiveAddCommentResponseSchema,
TheHiveCreateAlertResponseSchema,
PushToServiceIncidentSchema,
} from '../../../common/thehive/schema';
import type { ExecutorSubActionCreateAlertParams, Incident } from '../../../common/thehive/types';
const mockTime = new Date('2024-04-03T09:10:30.000');
describe('TheHiveConnector', () => {
const connector = new TheHiveConnector(
{
configurationUtilities: actionsConfigMock.create(),
connector: { id: '1', type: THEHIVE_CONNECTOR_ID },
config: { url: 'https://example.com', organisation: null },
secrets: { apiKey: 'test123' },
logger: loggingSystemMock.createLogger(),
services: actionsMock.createServices(),
},
PushToServiceIncidentSchema
);
let mockRequest: jest.Mock;
let mockError: jest.Mock;
beforeAll(() => {
jest.useFakeTimers();
jest.setSystemTime(mockTime);
});
afterAll(() => {
jest.useRealTimers();
});
beforeEach(() => {
mockError = jest.fn().mockImplementation(() => {
throw new Error('API Error');
});
jest.clearAllMocks();
});
describe('createIncident', () => {
const mockResponse = {
data: {
_id: '~172064',
_type: 'Case',
_createdBy: 'user1@thehive.local',
_createdAt: 1712128153041,
number: 67,
title: 'title',
description: 'description',
severity: 1,
severityLabel: 'LOW',
startDate: 1712128153029,
tags: ['tag1', 'tag2'],
flag: false,
tlp: 2,
tlpLabel: 'AMBER',
pap: 2,
papLabel: 'AMBER',
status: 'New',
stage: 'New',
assignee: 'user1@thehive.local',
customFields: [],
userPermissions: [
'manageCase/create',
'manageAlert/update',
'manageProcedure',
'managePage',
'manageObservable',
'manageCase/delete',
'manageAlert/create',
'manageCaseReport',
'manageAlert/delete',
'accessTheHiveFS',
'manageKnowledgeBase',
'manageAction',
'manageShare',
'manageAnalyse',
'manageFunction/invoke',
'manageTask',
'manageCase/merge',
'manageCustomEvent',
'manageAlert/import',
'manageCase/changeOwnership',
'manageComment',
'manageAlert/reopen',
'manageCase/update',
'manageCase/reopen',
],
extraData: {},
newDate: 1712128153029,
timeToDetect: 0,
},
};
beforeEach(() => {
mockRequest = jest.fn().mockResolvedValue(mockResponse);
// @ts-ignore
connector.request = mockRequest;
jest.clearAllMocks();
});
const incident: Incident = {
title: 'title',
description: 'description',
severity: 1,
tlp: 2,
tags: ['tag1', 'tag2'],
};
it('TheHive API call is successful with correct parameters', async () => {
const response = await connector.createIncident(incident);
expect(mockRequest).toBeCalledTimes(1);
expect(mockRequest).toHaveBeenCalledWith({
url: 'https://example.com/api/v1/case',
method: 'post',
responseSchema: TheHiveIncidentResponseSchema,
data: incident,
headers: {
Authorization: 'Bearer test123',
'X-Organisation': null,
},
});
expect(response).toEqual({
id: '~172064',
url: 'https://example.com/cases/~172064/details',
title: 'title',
pushedDate: '2024-04-03T07:09:13.041Z',
});
});
it('errors during API calls are properly handled', async () => {
// @ts-ignore
connector.request = mockError;
await expect(connector.createIncident(incident)).rejects.toThrow('API Error');
});
});
describe('updateIncident', () => {
const mockResponse = {
data: null,
};
beforeEach(() => {
mockRequest = jest.fn().mockResolvedValue(mockResponse);
// @ts-ignore
connector.request = mockRequest;
jest.clearAllMocks();
});
const incident: Incident = {
title: 'new title',
description: 'new description',
severity: 3,
tlp: 1,
tags: ['tag3'],
};
it('TheHive API call is successful with correct parameters', async () => {
const response = await connector.updateIncident({ incidentId: '~172064', incident });
expect(mockRequest).toBeCalledTimes(1);
expect(mockRequest).toHaveBeenCalledWith({
url: 'https://example.com/api/v1/case/~172064',
method: 'patch',
responseSchema: TheHiveUpdateIncidentResponseSchema,
data: incident,
headers: {
Authorization: 'Bearer test123',
'X-Organisation': null,
},
});
expect(response).toEqual({
id: '~172064',
url: 'https://example.com/cases/~172064/details',
title: 'new title',
pushedDate: mockTime.toISOString(),
});
});
it('errors during API calls are properly handled', async () => {
// @ts-ignore
connector.request = mockError;
await expect(connector.updateIncident({ incidentId: '~172064', incident })).rejects.toThrow(
'API Error'
);
});
});
describe('addComment', () => {
const mockResponse = {
data: {
_id: '~41156688',
_type: 'Comment',
createdBy: 'user1@thehive.local',
createdAt: 1712158280100,
message: 'test comment',
isEdited: false,
extraData: {},
},
};
beforeEach(() => {
mockRequest = jest.fn().mockResolvedValue(mockResponse);
// @ts-ignore
connector.request = mockRequest;
jest.clearAllMocks();
});
it('TheHive API call is successful with correct parameters', async () => {
await connector.addComment({
incidentId: '~172064',
comment: 'test comment',
});
expect(mockRequest).toBeCalledTimes(1);
expect(mockRequest).toHaveBeenCalledWith({
url: 'https://example.com/api/v1/case/~172064/comment',
method: 'post',
responseSchema: TheHiveAddCommentResponseSchema,
data: { message: 'test comment' },
headers: {
Authorization: 'Bearer test123',
'X-Organisation': null,
},
});
});
it('errors during API calls are properly handled', async () => {
// @ts-ignore
connector.request = mockError;
await expect(
connector.addComment({ incidentId: '~172064', comment: 'test comment' })
).rejects.toThrow('API Error');
});
});
describe('getIncident', () => {
const mockResponse = {
data: {
_id: '~172064',
_type: 'Case',
_createdBy: 'user1@thehive.local',
_createdAt: 1712128153041,
number: 67,
title: 'title',
description: 'description',
severity: 1,
severityLabel: 'LOW',
startDate: 1712128153029,
tags: ['tag1', 'tag2'],
flag: false,
tlp: 2,
tlpLabel: 'AMBER',
pap: 2,
papLabel: 'AMBER',
status: 'New',
stage: 'New',
assignee: 'user1@thehive.local',
customFields: [],
userPermissions: [
'manageCase/create',
'manageAlert/update',
'manageProcedure',
'managePage',
'manageObservable',
'manageCase/delete',
'manageAlert/create',
'manageCaseReport',
'manageAlert/delete',
'accessTheHiveFS',
'manageKnowledgeBase',
'manageAction',
'manageShare',
'manageAnalyse',
'manageFunction/invoke',
'manageTask',
'manageCase/merge',
'manageCustomEvent',
'manageAlert/import',
'manageCase/changeOwnership',
'manageComment',
'manageAlert/reopen',
'manageCase/update',
'manageCase/reopen',
],
extraData: {},
newDate: 1712128153029,
timeToDetect: 0,
},
};
beforeEach(() => {
mockRequest = jest.fn().mockResolvedValue(mockResponse);
// @ts-ignore
connector.request = mockRequest;
jest.clearAllMocks();
});
it('TheHive API call is successful with correct parameters', async () => {
const response = await connector.getIncident({ id: '~172064' });
expect(mockRequest).toBeCalledTimes(1);
expect(mockRequest).toHaveBeenCalledWith({
url: 'https://example.com/api/v1/case/~172064',
responseSchema: TheHiveIncidentResponseSchema,
headers: {
Authorization: 'Bearer test123',
'X-Organisation': null,
},
});
expect(response).toEqual(mockResponse.data);
});
it('errors during API calls are properly handled', async () => {
// @ts-ignore
connector.request = mockError;
await expect(connector.getIncident({ id: '~172064' })).rejects.toThrow('API Error');
});
});
describe('createAlert', () => {
const mockResponse = {
data: {
_id: '~41128088',
_type: 'Alert',
_createdBy: 'user1@thehive.local',
_createdAt: 1712161128982,
type: 'alert type',
source: 'alert source',
sourceRef: 'test123',
title: 'title',
description: 'description',
severity: 1,
severityLabel: 'LOW',
date: 1712161128964,
tags: ['tag1', 'tag2'],
tlp: 2,
tlpLabel: 'AMBER',
pap: 2,
papLabel: 'AMBER',
follow: true,
customFields: [],
observableCount: 0,
status: 'New',
stage: 'New',
extraData: {},
newDate: 1712161128967,
timeToDetect: 0,
},
};
beforeEach(() => {
mockRequest = jest.fn().mockResolvedValue(mockResponse);
// @ts-ignore
connector.request = mockRequest;
jest.clearAllMocks();
});
const alert: ExecutorSubActionCreateAlertParams = {
title: 'title',
description: 'description',
type: 'alert type',
source: 'alert source',
sourceRef: 'test123',
severity: 1,
tlp: 2,
tags: ['tag1', 'tag2'],
};
it('TheHive API call is successful with correct parameters', async () => {
await connector.createAlert(alert);
expect(mockRequest).toBeCalledTimes(1);
expect(mockRequest).toHaveBeenCalledWith({
url: 'https://example.com/api/v1/alert',
method: 'post',
responseSchema: TheHiveCreateAlertResponseSchema,
data: alert,
headers: {
Authorization: 'Bearer test123',
'X-Organisation': null,
},
});
});
it('errors during API calls are properly handled', async () => {
// @ts-ignore
connector.request = mockError;
await expect(connector.createAlert(alert)).rejects.toThrow('API Error');
});
});
});

View file

@ -0,0 +1,140 @@
/*
* 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 { ServiceParams, CaseConnector } from '@kbn/actions-plugin/server';
import type { AxiosError } from 'axios';
import { Type } from '@kbn/config-schema';
import { SUB_ACTION } from '../../../common/thehive/constants';
import {
TheHiveIncidentResponseSchema,
TheHiveUpdateIncidentResponseSchema,
TheHiveAddCommentResponseSchema,
TheHiveCreateAlertResponseSchema,
ExecutorSubActionCreateAlertParamsSchema,
} from '../../../common/thehive/schema';
import type {
TheHiveConfig,
TheHiveSecrets,
ExecutorSubActionCreateAlertParams,
TheHiveFailureResponse,
ExternalServiceIncidentResponse,
Incident,
GetIncidentResponse,
} from '../../../common/thehive/types';
export const API_VERSION = 'v1';
export class TheHiveConnector extends CaseConnector<
TheHiveConfig,
TheHiveSecrets,
Incident,
GetIncidentResponse
> {
private url: string;
private apiKey: string;
private organisation: string | null;
private urlWithoutTrailingSlash: string;
constructor(
params: ServiceParams<TheHiveConfig, TheHiveSecrets>,
pushToServiceParamsExtendedSchema: Record<string, Type<unknown>>
) {
super(params, pushToServiceParamsExtendedSchema);
this.registerSubAction({
name: SUB_ACTION.CREATE_ALERT,
method: 'createAlert',
schema: ExecutorSubActionCreateAlertParamsSchema,
});
this.url = this.config.url;
this.organisation = this.config.organisation;
this.apiKey = this.secrets.apiKey;
this.urlWithoutTrailingSlash = this.url?.endsWith('/') ? this.url.slice(0, -1) : this.url;
}
private getAuthHeaders() {
return { Authorization: `Bearer ${this.apiKey}`, 'X-Organisation': this.organisation };
}
protected getResponseErrorMessage(error: AxiosError<TheHiveFailureResponse>): string {
if (!error.response?.status) {
return 'Unknown API Error';
}
return `API Error: ${error.response?.data?.type} - ${error.response?.data?.message}`;
}
public async createIncident(incident: Incident): Promise<ExternalServiceIncidentResponse> {
const res = await this.request({
method: 'post',
url: `${this.url}/api/${API_VERSION}/case`,
data: incident,
headers: this.getAuthHeaders(),
responseSchema: TheHiveIncidentResponseSchema,
});
return {
id: res.data._id,
title: res.data.title,
url: `${this.urlWithoutTrailingSlash}/cases/${res.data._id}/details`,
pushedDate: new Date(res.data._createdAt).toISOString(),
};
}
public async addComment({ incidentId, comment }: { incidentId: string; comment: string }) {
await this.request({
method: 'post',
url: `${this.url}/api/${API_VERSION}/case/${incidentId}/comment`,
data: { message: comment },
headers: this.getAuthHeaders(),
responseSchema: TheHiveAddCommentResponseSchema,
});
}
public async updateIncident({
incidentId,
incident,
}: {
incidentId: string;
incident: Incident;
}): Promise<ExternalServiceIncidentResponse> {
await this.request({
method: 'patch',
url: `${this.url}/api/${API_VERSION}/case/${incidentId}`,
data: incident,
headers: this.getAuthHeaders(),
responseSchema: TheHiveUpdateIncidentResponseSchema,
});
return {
id: incidentId,
title: incident.title,
url: `${this.urlWithoutTrailingSlash}/cases/${incidentId}/details`,
pushedDate: new Date().toISOString(),
};
}
public async getIncident({ id }: { id: string }): Promise<GetIncidentResponse> {
const res = await this.request({
url: `${this.url}/api/${API_VERSION}/case/${id}`,
headers: this.getAuthHeaders(),
responseSchema: TheHiveIncidentResponseSchema,
});
return res.data;
}
public async createAlert(alert: ExecutorSubActionCreateAlertParams) {
await this.request({
method: 'post',
url: `${this.url}/api/${API_VERSION}/alert`,
data: alert,
headers: this.getAuthHeaders(),
responseSchema: TheHiveCreateAlertResponseSchema,
});
}
}

View file

@ -131,7 +131,7 @@ describe('Stack Connectors Plugin', () => {
name: 'Torq',
})
);
expect(actionsSetup.registerSubActionConnectorType).toHaveBeenCalledTimes(9);
expect(actionsSetup.registerSubActionConnectorType).toHaveBeenCalledTimes(10);
expect(actionsSetup.registerSubActionConnectorType).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
@ -183,13 +183,20 @@ describe('Stack Connectors Plugin', () => {
);
expect(actionsSetup.registerSubActionConnectorType).toHaveBeenNthCalledWith(
8,
expect.objectContaining({
id: '.thehive',
name: 'TheHive',
})
);
expect(actionsSetup.registerSubActionConnectorType).toHaveBeenNthCalledWith(
9,
expect.objectContaining({
id: '.sentinelone',
name: 'Sentinel One',
})
);
expect(actionsSetup.registerSubActionConnectorType).toHaveBeenNthCalledWith(
9,
10,
expect.objectContaining({
id: '.crowdstrike',
name: 'CrowdStrike',

View file

@ -56,6 +56,7 @@ const enabledActionTypes = [
CROWDSTRIKE_CONNECTOR_ID,
'.slack',
'.slack_api',
'.thehive',
'.tines',
'.webhook',
'.xmatters',

View file

@ -0,0 +1,101 @@
/*
* 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 http from 'http';
import { ProxyArgs, Simulator } from './simulator';
export class TheHiveSimulator extends Simulator {
private readonly returnError: boolean;
constructor({ returnError = false, proxy }: { returnError?: boolean; proxy?: ProxyArgs }) {
super(proxy);
this.returnError = returnError;
}
public async handler(
request: http.IncomingMessage,
response: http.ServerResponse,
data: Record<string, unknown>
) {
if (this.returnError) {
return TheHiveSimulator.sendErrorResponse(response);
}
return TheHiveSimulator.sendResponse(response);
}
private static sendResponse(response: http.ServerResponse) {
response.statusCode = 201;
response.setHeader('Content-Type', 'application/json');
response.end(JSON.stringify(theHiveSuccessResponse, null, 4));
}
private static sendErrorResponse(response: http.ServerResponse) {
response.statusCode = 400;
response.setHeader('Content-Type', 'application/json');
response.end(JSON.stringify(theHiveFailedResponse, null, 4));
}
}
export const theHiveSuccessResponse = {
_id: '~172064',
_type: 'Case',
_createdBy: 'user1@thehive.local',
_createdAt: 1712128153041,
number: 67,
title: 'title',
description: 'description',
severity: 1,
severityLabel: 'LOW',
startDate: 1712128153029,
tags: ['tag1', 'tag2'],
flag: false,
tlp: 2,
tlpLabel: 'AMBER',
pap: 2,
papLabel: 'AMBER',
status: 'New',
stage: 'New',
assignee: 'user1@thehive.local',
customFields: [],
userPermissions: [
'manageCase/create',
'manageAlert/update',
'manageProcedure',
'managePage',
'manageObservable',
'manageCase/delete',
'manageAlert/create',
'manageCaseReport',
'manageAlert/delete',
'accessTheHiveFS',
'manageKnowledgeBase',
'manageAction',
'manageShare',
'manageAnalyse',
'manageFunction/invoke',
'manageTask',
'manageCase/merge',
'manageCustomEvent',
'manageAlert/import',
'manageCase/changeOwnership',
'manageComment',
'manageAlert/reopen',
'manageCase/update',
'manageCase/reopen',
],
extraData: {},
newDate: 1712128153029,
timeToDetect: 0,
};
export const theHiveFailedResponse = {
type: 'BadRequest',
message: 'Invalid json',
};

View file

@ -0,0 +1,330 @@
/*
* 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 expect from '@kbn/expect';
import { TheHiveSimulator } from '@kbn/actions-simulators-plugin/server/thehive_simulation';
import { TaskErrorSource } from '@kbn/task-manager-plugin/common';
import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
const connectorTypeId = '.thehive';
const name = 'TheHive action';
const secrets = {
apiKey: 'token12345',
};
// eslint-disable-next-line import/no-default-export
export default function theHiveTest({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const configService = getService('config');
const createConnector = async (url: string) => {
const { body } = await supertest
.post('/api/actions/connector')
.set('kbn-xsrf', 'foo')
.send({
name,
connector_type_id: connectorTypeId,
config: { url, organisation: null },
secrets,
})
.expect(200);
return body.id;
};
describe('TheHive', () => {
describe('action creation', () => {
const simulator = new TheHiveSimulator({
returnError: false,
proxy: {
config: configService.get('kbnTestServer.serverArgs'),
},
});
const config = { url: '', organisation: null };
before(async () => {
config.url = await simulator.start();
});
after(() => {
simulator.close();
});
it('should return 200 when creating the connector without organisation', async () => {
const { body: createdAction } = await supertest
.post('/api/actions/connector')
.set('kbn-xsrf', 'foo')
.send({
name,
connector_type_id: connectorTypeId,
config,
secrets,
})
.expect(200);
expect(createdAction).to.eql({
id: createdAction.id,
is_preconfigured: false,
is_system_action: false,
is_deprecated: false,
name,
connector_type_id: connectorTypeId,
is_missing_secrets: false,
config,
});
});
it('should return 200 when creating the connector with the organisation', async () => {
const { body: createdAction } = await supertest
.post('/api/actions/connector')
.set('kbn-xsrf', 'foo')
.send({
name,
connector_type_id: connectorTypeId,
config: { ...config, organisation: 'test-organisation' },
secrets,
})
.expect(200);
expect(createdAction).to.eql({
id: createdAction.id,
is_preconfigured: false,
is_system_action: false,
is_deprecated: false,
name,
connector_type_id: connectorTypeId,
is_missing_secrets: false,
config: { ...config, organisation: 'test-organisation' },
});
});
it('should return 400 Bad Request when creating the connector without the url', async () => {
await supertest
.post('/api/actions/connector')
.set('kbn-xsrf', 'foo')
.send({
name,
connector_type_id: connectorTypeId,
config: {},
secrets,
})
.expect(400)
.then((resp: any) => {
expect(resp.body).to.eql({
statusCode: 400,
error: 'Bad Request',
message:
'error validating action type config: [url]: expected value of type [string] but got [undefined]',
});
});
});
it('should return 400 Bad Request when creating the connector with a url that is not allowed', async () => {
await supertest
.post('/api/actions/connector')
.set('kbn-xsrf', 'foo')
.send({
name,
connector_type_id: connectorTypeId,
config: {
url: 'http://thehive.mynonexistent.com',
},
secrets,
})
.expect(400)
.then((resp: any) => {
expect(resp.body).to.eql({
statusCode: 400,
error: 'Bad Request',
message:
'error validating action type config: error validating url: target url "http://thehive.mynonexistent.com" is not added to the Kibana config xpack.actions.allowedHosts',
});
});
});
it('should return 400 Bad Request when creating the connector without secrets', async () => {
await supertest
.post('/api/actions/connector')
.set('kbn-xsrf', 'foo')
.send({
name,
connector_type_id: connectorTypeId,
config,
})
.expect(400)
.then((resp: any) => {
expect(resp.body).to.eql({
statusCode: 400,
error: 'Bad Request',
message:
'error validating action type secrets: [apiKey]: expected value of type [string] but got [undefined]',
});
});
});
});
describe('executor', () => {
describe('validation', () => {
const simulator = new TheHiveSimulator({
proxy: {
config: configService.get('kbnTestServer.serverArgs'),
},
});
let theHiveActionId: string;
before(async () => {
const url = await simulator.start();
theHiveActionId = await createConnector(url);
});
after(() => {
simulator.close();
});
it('should fail when the params is empty', async () => {
const { body } = await supertest
.post(`/api/actions/connector/${theHiveActionId}/_execute`)
.set('kbn-xsrf', 'foo')
.send({
params: {},
});
expect(200);
expect(body).to.eql({
status: 'error',
connector_id: theHiveActionId,
message:
'error validating action params: [subAction]: expected value of type [string] but got [undefined]',
retry: false,
errorSource: TaskErrorSource.FRAMEWORK,
});
});
it('should fail when the subAction is invalid', async () => {
const { body } = await supertest
.post(`/api/actions/connector/${theHiveActionId}/_execute`)
.set('kbn-xsrf', 'foo')
.send({
params: { subAction: 'invalidAction' },
})
.expect(200);
expect(body).to.eql({
connector_id: theHiveActionId,
status: 'error',
retry: true,
message: 'an error occurred while running the action',
errorSource: TaskErrorSource.FRAMEWORK,
service_message: `Sub action "invalidAction" is not registered. Connector id: ${theHiveActionId}. Connector name: TheHive. Connector type: .thehive`,
});
});
});
describe('execution', () => {
describe('successful response simulator', () => {
const simulator = new TheHiveSimulator({
proxy: {
config: configService.get('kbnTestServer.serverArgs'),
},
});
let url: string;
let theHiveActionId: string;
before(async () => {
url = await simulator.start();
theHiveActionId = await createConnector(url);
});
after(() => {
simulator.close();
});
it('should send a formatted JSON object', async () => {
const { body } = await supertest
.post(`/api/actions/connector/${theHiveActionId}/_execute`)
.set('kbn-xsrf', 'foo')
.send({
params: {
subAction: 'pushToService',
subActionParams: {
incident: {
title: 'title',
description: 'description',
tlp: 2,
severity: 1,
tags: ['tag1', 'tag2'],
},
comments: [],
},
},
})
.expect(200);
expect(simulator.requestData).to.eql({
title: 'title',
description: 'description',
tlp: 2,
severity: 1,
tags: ['tag1', 'tag2'],
});
expect(body).to.eql({
status: 'ok',
connector_id: theHiveActionId,
data: {
id: '~172064',
title: 'title',
url: `${url}/cases/~172064/details`,
pushedDate: new Date(1712128153041).toISOString(),
},
});
});
});
describe('error response simulator', () => {
const simulator = new TheHiveSimulator({
returnError: true,
proxy: {
config: configService.get('kbnTestServer.serverArgs'),
},
});
let theHiveActionId: string;
before(async () => {
const url = await simulator.start();
theHiveActionId = await createConnector(url);
});
after(() => {
simulator.close();
});
it('should return a failure when error happens', async () => {
const { body } = await supertest
.post(`/api/actions/connector/${theHiveActionId}/_execute`)
.set('kbn-xsrf', 'foo')
.send({
params: {},
})
.expect(200);
expect(body).to.eql({
status: 'error',
connector_id: theHiveActionId,
message:
'error validating action params: [subAction]: expected value of type [string] but got [undefined]',
retry: false,
errorSource: TaskErrorSource.FRAMEWORK,
});
});
});
});
});
});
}

View file

@ -43,6 +43,7 @@ export default function connectorsTests({ loadTestFile, getService }: FtrProvide
loadTestFile(require.resolve('./connector_types/torq'));
loadTestFile(require.resolve('./connector_types/openai'));
loadTestFile(require.resolve('./connector_types/d3security'));
loadTestFile(require.resolve('./connector_types/thehive'));
loadTestFile(require.resolve('./connector_types/bedrock'));
loadTestFile(require.resolve('./create'));
loadTestFile(require.resolve('./delete'));

View file

@ -46,6 +46,7 @@ export default function createRegisteredConnectorTypeTests({ getService }: FtrPr
'.observability-ai-assistant',
'.resilient',
'.teams',
'.thehive',
'.tines',
'.torq',
'.opsgenie',

View file

@ -75,6 +75,7 @@ export default function ({ getService }: FtrProviderContext) {
'actions:.slack_api',
'actions:.swimlane',
'actions:.teams',
'actions:.thehive',
'actions:.tines',
'actions:.torq',
'actions:.webhook',