XSOAR Connector (#212049)

## Summary

XSOAR action connector, enabling users to send alerts generated by the
rule detection engine to Palo Alto XSOAR for automation and remediation.

### **create connector**

![xsoar-connector](https://github.com/user-attachments/assets/14d9791b-0242-42b5-b9e4-975d7f6826cc)

### **test connector**
1. **test page**

![xsoar-params-test](https://github.com/user-attachments/assets/2bdd3b79-7f5f-4d52-836b-f458c390e55c)

2. **select playbook**

![xsoar-select-playbook](https://github.com/user-attachments/assets/23787b24-31b0-4f56-b451-0e8b42c79797)

### Checklist

Check the PR satisfies following conditions. 

Reviewers should verify this PR satisfies this list as well.

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

### 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: Sergi Massaneda <sergi.massaneda@elastic.co>
Co-authored-by: Nastasha Solomon <79124755+nastasha-solomon@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Brijesh Khunt 2025-06-20 18:20:07 +05:30 committed by GitHub
parent 31fe87ae06
commit 3fcdc062fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 3368 additions and 3 deletions

View file

@ -54,6 +54,7 @@ subs:
bedrock: "Amazon Bedrock"
gemini: "Google Gemini"
hive: "TheHive"
xsoar: "XSOAR"
report-features: "reporting features"
ml: "machine learning"
ccs: "cross-cluster search"

View file

@ -41,6 +41,7 @@ Actions are instantiations of a connector that are linked to rules and run as ba
* [{{webhook}}](/reference/connectors-kibana/webhook-action-type.md): Send a request to a web service.
* [{{webhook-cm}}](/reference/connectors-kibana/cases-webhook-action-type.md): Send a request to a Case Management web service.
* [xMatters](/reference/connectors-kibana/xmatters-action-type.md): Send actionable alerts to on-call xMatters resources.
* [{{xsoar}}](/reference/connectors-kibana/xsoar-action-type.md): Create an incident in Cortex {{xsoar}}.
::::{note}
Some connector types are paid commercial features, while others are free. For a comparison of the Elastic subscription levels, go to [the subscription page](https://www.elastic.co/subscriptions).

View file

@ -0,0 +1,80 @@
---
navigation_title: "{{xsoar}}"
mapped_pages:
- https://www.elastic.co/guide/en/kibana/current/xsoar-action-type.html
---
# {{xsoar}} connector and action [xsoar-action-type]
{{xsoar}} connector uses the [{{xsoar}} REST API](https://cortex-panw.stoplight.io/docs/cortex-xsoar-8/m0qlgh9inh4vk-create-or-update-an-incident) to create Cortex {{xsoar}} incidents.
## Create connectors in {{kib}} [define-xsoar-ui]
You can create connectors in **{{stack-manage-app}} > {{connectors-ui}}** or as needed when youre creating a rule. For example:
% TO DO: Use `:class: screenshot`
![XSOAR connector](../images/xsoar-connector.png)
### Connector configuration [xsoar-connector-configuration]
{{xsoar}} connectors have the following configuration properties:
Name
: The name of the connector.
URL
: The {{xsoar}} instance URL.
API key
: The {{xsoar}} API key for authentication.
::::{note}
If you do not have an API key, refer to [Create a new API key](https://cortex-panw.stoplight.io/docs/cortex-xsoar-8/t09y7hrb5d14m-create-a-new-api-key) to make one for your {{xsoar}} instance.
::::
API key id
: The {{xsoar}} API key ID for authentication. (Mandatory for cloud instance users.)
## Test connectors [xsoar-action-configuration]
You can test connectors as youre creating or editing the connector in {{kib}}. For example:
% TO DO: Use `:class: screenshot`
![XSOAR params test](../images/xsoar-params-test.png)
{{xsoar}} actions have the following configuration properties.
Name
: The incident name.
Playbook
: The playbook to associate with the incident.
Start investigation
: If turned on, will automatically start the investigation process after the incident is created.
Severity
: The severity of the incident. Can be `Unknown`, `Informational`, `Low`, `Medium`, `High` or `Critical`.
::::{note}
Turn on `Keep severity from rule` to create an incident that inherits the rule's severity.
::::
Body
: A JSON payload that includes additional parameters to be included in the API request.
```json
{
"details": "This is an example incident",
"type": "Unclassified"
}
```
## Connector networking configuration [xsoar-connector-networking-configuration]
Use the [Action configuration settings](/reference/configuration-reference/alerting-settings.md#action-settings) to customize connector networking configurations, such as proxies, certificates, or TLS settings. You can set configurations that apply to all your connectors or use `xpack.actions.customHostSettings` to set per-host configurations.

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View file

@ -58,6 +58,7 @@ toc:
- file: connectors-kibana/webhook-action-type.md
- file: connectors-kibana/cases-webhook-action-type.md
- file: connectors-kibana/xmatters-action-type.md
- file: connectors-kibana/xsoar-action-type.md
- file: connectors-kibana/pre-configured-connectors.md
- file: kibana-plugins.md
- file: commands.md
@ -65,4 +66,4 @@ toc:
- file: commands/kibana-encryption-keys.md
- file: commands/kibana-verification-code.md
- file: osquery-exported-fields.md
- file: osquery-manager-prebuilt-packs.md
- file: osquery-manager-prebuilt-packs.md

View file

@ -39559,3 +39559,294 @@ Object {
"type": "object",
}
`;
exports[`Connector type config checks detect connector type changes for: .xsoar 1`] = `
Object {
"flags": Object {
"default": Object {
"special": "deep",
},
"error": [Function],
"presence": "optional",
},
"keys": Object {
"body": Object {
"flags": Object {
"default": null,
"error": [Function],
"presence": "optional",
},
"matches": Array [
Object {
"schema": Object {
"flags": Object {
"error": [Function],
},
"rules": Array [
Object {
"args": Object {
"method": [Function],
},
"name": "custom",
},
],
"type": "string",
},
},
Object {
"schema": Object {
"allow": Array [
null,
],
"flags": Object {
"error": [Function],
"only": true,
},
"type": "any",
},
},
],
"type": "alternatives",
},
"createInvestigation": Object {
"flags": Object {
"error": [Function],
},
"type": "boolean",
},
"isRuleSeverity": Object {
"flags": Object {
"default": null,
"error": [Function],
"presence": "optional",
},
"matches": Array [
Object {
"schema": Object {
"flags": Object {
"default": false,
"error": [Function],
"presence": "optional",
},
"type": "boolean",
},
},
Object {
"schema": Object {
"allow": Array [
null,
],
"flags": Object {
"error": [Function],
"only": true,
},
"type": "any",
},
},
],
"type": "alternatives",
},
"name": Object {
"flags": Object {
"error": [Function],
},
"rules": Array [
Object {
"args": Object {
"method": [Function],
},
"name": "custom",
},
],
"type": "string",
},
"playbookId": 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 {
"error": [Function],
},
"type": "number",
},
},
"type": "object",
}
`;
exports[`Connector type config checks detect connector type changes for: .xsoar 2`] = `
Object {
"flags": Object {
"default": Object {
"special": "deep",
},
"error": [Function],
"presence": "optional",
},
"keys": Object {
"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: .xsoar 3`] = `
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",
},
"apiKeyID": 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",
},
},
"type": "object",
}
`;
exports[`Connector type config checks detect connector type changes for: .xsoar 4`] = `
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",
}
`;

View file

@ -30,6 +30,7 @@ export const connectorTypes: string[] = [
'.d3security',
'.resilient',
'.thehive',
'.xsoar',
'.sentinelone',
'.crowdstrike',
'.inference',

View file

@ -0,0 +1,28 @@
/*
* 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 XSOAR_TITLE = i18n.translate(
'xpack.stackConnectors.components.xsoar.connectorTypeTitle',
{
defaultMessage: 'XSOAR',
}
);
export const XSOAR_CONNECTOR_ID = '.xsoar';
export enum SUB_ACTION {
PLAYBOOKS = 'getPlaybooks',
RUN = 'run',
}
export enum XSOARSeverity {
INFORMATIONAL = 0.5,
UNKNOWN = 0,
LOW = 1,
MEDIUM = 2,
HIGH = 3,
CRITICAL = 4,
}

View file

@ -0,0 +1,54 @@
/*
* 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 { SUB_ACTION } from './constants';
export const ConfigSchema = schema.object({
url: schema.string(),
});
export const SecretsSchema = schema.object({
apiKey: schema.string(),
apiKeyID: schema.nullable(schema.string()),
});
export const XSOARPlaybooksActionParamsSchema = null;
export const XSOARPlaybooksObjectSchema = schema.object(
{
id: schema.string(),
name: schema.string(),
},
{ unknowns: 'ignore' }
);
export const XSOARPlaybooksActionResponseSchema = schema.object(
{
playbooks: schema.arrayOf(XSOARPlaybooksObjectSchema),
},
{ unknowns: 'ignore' }
);
export const XSOARRunActionParamsSchema = schema.object({
name: schema.string(),
playbookId: schema.nullable(schema.string()),
createInvestigation: schema.boolean(),
severity: schema.number(),
isRuleSeverity: schema.nullable(schema.boolean({ defaultValue: false })),
body: schema.nullable(schema.string()),
});
export const XSOARRunActionResponseSchema = schema.object({}, { unknowns: 'ignore' });
export const ExecutorParamsSchema = schema.oneOf([
schema.object({
subAction: schema.literal(SUB_ACTION.PLAYBOOKS),
subActionParams: schema.literal(null), // this subaction not required any value as params
}),
schema.object({
subAction: schema.literal(SUB_ACTION.RUN),
subActionParams: XSOARRunActionParamsSchema,
}),
]);

View file

@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { TypeOf } from '@kbn/config-schema';
import type {
ConfigSchema,
SecretsSchema,
XSOARRunActionParamsSchema,
XSOARRunActionResponseSchema,
XSOARPlaybooksObjectSchema,
XSOARPlaybooksActionResponseSchema,
ExecutorParamsSchema,
} from './schema';
export type Config = TypeOf<typeof ConfigSchema>;
export type Secrets = TypeOf<typeof SecretsSchema>;
export type XSOARRunActionParams = TypeOf<typeof XSOARRunActionParamsSchema>;
export type XSOARRunActionResponse = TypeOf<typeof XSOARRunActionResponseSchema>;
export type XSOARPlaybooksActionParams = void;
export type XSOARPlaybooksObject = TypeOf<typeof XSOARPlaybooksObjectSchema>;
export type XSOARPlaybooksActionResponse = TypeOf<typeof XSOARPlaybooksActionResponseSchema>;
export type ExecutorParams = TypeOf<typeof ExecutorParamsSchema>;

View file

@ -36,6 +36,7 @@ import { ExperimentalFeaturesService } from '../common/experimental_features_ser
import { getSentinelOneConnectorType } from './sentinelone';
import { getTheHiveConnectorType } from './thehive';
import { getCrowdStrikeConnectorType } from './crowdstrike';
import { getXSOARConnectorType } from './xsoar';
export interface RegistrationServices {
validateEmailAddresses: (
@ -75,6 +76,7 @@ export function registerConnectorTypes({
connectorTypeRegistry.register(getTinesConnectorType());
connectorTypeRegistry.register(getD3SecurityConnectorType());
connectorTypeRegistry.register(getTheHiveConnectorType());
connectorTypeRegistry.register(getXSOARConnectorType());
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 XSOARConnectorFields 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('XSOARActionConnectorFields renders', () => {
const actionConnector = {
actionTypeId: '.xsoar',
name: 'XSOAR',
config: {
url: 'https://test.com',
},
secrets: {
apiKey: 'apiKey',
},
isDeprecated: false,
};
it('XSOAR connector fields are rendered', () => {
const { getByTestId } = render(
<ConnectorFormTestProvider connector={actionConnector}>
<XSOARConnectorFields
readOnly={false}
isEdit={false}
registerPreSubmitValidator={() => {}}
/>
</ConnectorFormTestProvider>
);
expect(getByTestId('config.url-input')).toBeInTheDocument();
expect(getByTestId('secrets.apiKey-input')).toBeInTheDocument();
expect(getByTestId('secrets.apiKeyID-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}>
<XSOARConnectorFields
readOnly={false}
isEdit={false}
registerPreSubmitValidator={() => {}}
/>
</ConnectorFormTestProvider>
);
await act(async () => {
await userEvent.click(getByTestId('form-test-provide-submit'));
});
waitFor(() => {
expect(onSubmit).toBeCalledWith({
data: {
actionTypeId: '.xsoar',
name: 'XSOAR',
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}>
<XSOARConnectorFields
readOnly={false}
isEdit={false}
registerPreSubmitValidator={() => {}}
/>
</ConnectorFormTestProvider>
);
await userEvent.clear(res.getByTestId(field));
if (value !== '') {
await userEvent.type(res.getByTestId(field), value, {
delay: 10,
});
}
await userEvent.click(res.getByTestId('form-test-provide-submit'));
expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false });
});
});
});

View file

@ -0,0 +1,45 @@
/*
* 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, API_KEY_ID_LABEL, API_KEY_ID_HELP_TEXT } from './translations';
const configFormSchema: ConfigFieldSchema[] = [{ id: 'url', label: URL_LABEL, isUrlField: true }];
const secretsFormSchema: SecretsFieldSchema[] = [
{ id: 'apiKey', label: API_KEY_LABEL, isPasswordField: true },
{
id: 'apiKeyID',
label: API_KEY_ID_LABEL,
isPasswordField: true,
isRequired: false,
helpText: API_KEY_ID_HELP_TEXT,
},
];
const XSOARConnectorFields: React.FC<ActionConnectorFieldsProps> = ({ readOnly, isEdit }) => {
return (
<>
<SimpleConnectorForm
isEdit={isEdit}
readOnly={readOnly}
configFormSchema={configFormSchema}
secretsFormSchema={secretsFormSchema}
/>
</>
);
};
// eslint-disable-next-line import/no-default-export
export { XSOARConnectorFields as default };

View file

@ -0,0 +1,66 @@
/*
* 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 { XSOARSeverity } from '../../../common/xsoar/constants';
export const severityOptions = [
{
value: XSOARSeverity.UNKNOWN,
text: i18n.translate(
'xpack.stackConnectors.components.xsoar.eventSelectSeverityUnknownOptionLabel',
{
defaultMessage: 'Unknown',
}
),
},
{
value: XSOARSeverity.INFORMATIONAL,
text: i18n.translate(
'xpack.stackConnectors.components.xsoar.eventSelectSeverityInformationalOptionLabel',
{
defaultMessage: 'Informational',
}
),
},
{
value: XSOARSeverity.LOW,
text: i18n.translate(
'xpack.stackConnectors.components.xsoar.eventSelectSeverityLowOptionLabel',
{
defaultMessage: 'Low',
}
),
},
{
value: XSOARSeverity.MEDIUM,
text: i18n.translate(
'xpack.stackConnectors.components.xsoar.eventSelectSeverityMediumOptionLabel',
{
defaultMessage: 'Medium',
}
),
},
{
value: XSOARSeverity.HIGH,
text: i18n.translate(
'xpack.stackConnectors.components.xsoar.eventSelectSeverityHighOptionLabel',
{
defaultMessage: 'High',
}
),
},
{
value: XSOARSeverity.CRITICAL,
text: i18n.translate(
'xpack.stackConnectors.components.xsoar.eventSelectSeverityCriticalOptionLabel',
{
defaultMessage: 'Critical',
}
),
},
];

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 getXSOARConnectorType } from './xsoar';

View file

@ -0,0 +1,45 @@
/*
* 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
version="1.1"
xmlns="http://www.w3.org/2000/svg"
width="180"
height="180"
viewBox="0 0 180 180"
fill="none"
{...props}
>
<path
d="M0 0 C59.4 0 118.8 0 180 0 C180 59.4 180 118.8 180 180 C120.6 180 61.2 180 0 180 C0 120.6 0 61.2 0 0 Z "
fill="#FEFEFE"
transform="translate(0,0)"
/>
<path
d="M0 0 C0 11.88 0 23.76 0 36 C5.445 36.495 5.445 36.495 11 37 C23.89312642 40.73692232 34.87162395 48.36931836 41.76171875 59.96484375 C48.07437887 72.08576258 50.43971101 84.47923625 46.5637207 97.89599609 C42.26551236 111.05035692 34.17899054 121.54046247 21.75 127.875 C14.04399747 131.47617112 9.1053914 132.24121738 0 133 C0 144.88 0 156.76 0 169 C-24.23522933 169 -43.35264863 160.25811545 -60.8125 143.5625 C-76.84026319 126.78388665 -84.872243 104.0210434 -84.51928711 80.99316406 C-83.71001346 60.47708128 -75.31748211 42.39007039 -62 27 C-61.42765625 26.32710937 -60.8553125 25.65421875 -60.265625 24.9609375 C-44.93602384 8.34122139 -22.06892026 0 0 0 Z "
fill="#02CC67"
transform="translate(108,5)"
/>
<path
d="M0 0 C10.52864676 8.69370494 16.43235973 20.33764021 17.97265625 33.8515625 C18.50961099 47.3884742 14.58471229 59.93950836 5.55859375 70.21875 C-4.4347226 80.75174265 -16.58867412 85.97534344 -31.00390625 86.53125 C-45.1565789 86.31948931 -56.9777179 80.70659268 -66.85546875 70.796875 C-78.06743615 57.6112654 -79.64663933 43.88512024 -78.44140625 27.21875 C-77.64997359 23.15304735 -76.436914 19.8367734 -74.44140625 16.21875 C-74.01730469 15.44789062 -73.59320313 14.67703125 -73.15625 13.8828125 C-65.71409762 1.73489804 -54.89874847 -6.13554065 -41.31640625 -10.21875 C-25.93977477 -12.45084167 -12.56035967 -9.16403842 0 0 Z "
fill="#FBFEFC"
transform="translate(138.44140625,51.78125)"
/>
<path
d="M0 0 C13.74396568 0 23.98108874 3.58656823 34.12109375 12.9609375 C44.49350337 23.82353661 48.60984892 36.24760551 48.375 50.984375 C47.48222761 64.08262134 41.08555096 75.39512537 31.6875 84.3125 C22.17264545 92.36353078 12.39116947 94.96740254 0 96 C0 64.32 0 32.64 0 0 Z "
fill="#01CC66"
transform="translate(108,42)"
/>
</svg>
);
// eslint-disable-next-line import/no-default-export
export { Logo as default };

View file

@ -0,0 +1,364 @@
/*
* 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, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ActionConnector, ActionConnectorMode } from '@kbn/triggers-actions-ui-plugin/public/types';
import XSOARParamsFields from './params';
import type { UseSubActionParams } from '@kbn/triggers-actions-ui-plugin/public/application/hooks/use_sub_action';
import { SUB_ACTION } from '../../../common/xsoar/constants';
import { ExecutorParams, XSOARRunActionParams } from '../../../common/xsoar/types';
import * as translations from './translations';
interface Result {
isLoading: boolean;
response: Record<string, unknown>;
error: null | Error;
}
const triggersActionsPath = '@kbn/triggers-actions-ui-plugin/public';
const response = {
playbooks: [
{
id: '8db0105c-f674-4d83-8095-f95a9f61e77a',
version: 4,
cacheVersn: 0,
sequenceNumber: 33831652,
primaryTerm: 11,
modified: '2023-12-12T13:51:15.668021556Z',
sizeInBytes: 0,
packID: '',
packName: '',
itemVersion: '',
fromServerVersion: '',
toServerVersion: '',
propagationLabels: ['all'],
definitionId: '',
vcShouldIgnore: false,
vcShouldKeepItemLegacyProdMachine: false,
commitMessage: '',
shouldCommit: false,
name: 'playbook0',
nameRaw: 'playbook0',
prevName: 'aaa',
startTaskId: '0',
tasks: {
'0': {
id: '0',
taskId: 'e228a044-2ad5-4ab0-873a-d5bb94a5c1b4',
type: 'start',
task: {
id: 'e228a044-2ad5-4ab0-873a-d5bb94a5c1b4',
version: 1,
cacheVersn: 0,
sequenceNumber: 13431901,
primaryTerm: 8,
modified: '2023-05-23T07:16:19.930125981Z',
sizeInBytes: 0,
},
nextTasks: {
'#none#': ['1'],
},
continueOnErrorType: '',
view: {
position: {
x: 450,
y: 50,
},
},
evidenceData: {},
},
'1': {
id: '1',
taskId: 'c28b63d3-c860-4e16-82b4-6db6b58bdee3',
type: 'regular',
task: {
id: 'c28b63d3-c860-4e16-82b4-6db6b58bdee3',
version: 1,
cacheVersn: 0,
sequenceNumber: 33831651,
primaryTerm: 11,
modified: '2023-12-12T13:51:15.604271789Z',
sizeInBytes: 0,
name: 'Untitled Task 1',
description: 'commands.local.cmd.set.incident',
scriptId: 'Builtin|||setIncident',
type: 'regular',
isCommand: true,
brand: 'Builtin',
},
scriptArguments: {
severity: {
simple: '1',
},
},
continueOnErrorType: '',
view: {
position: {
x: 450,
y: 200,
},
},
evidenceData: {},
},
},
taskIds: ['e228a044-2ad5-4ab0-873a-d5bb94a5c1b4', 'c28b63d3-c860-4e16-82b4-6db6b58bdee3'],
scriptIds: [],
commands: ['setIncident'],
brands: ['Builtin'],
missingScriptsIds: ['Builtin|||setIncident'],
view: {
linkLabelsPosition: {},
paper: {
dimensions: {
height: 245,
width: 380,
x: 450,
y: 50,
},
},
},
inputs: null,
outputs: null,
quiet: true,
},
],
tags: [
'Phishing',
'Sandbox',
'Severity',
'Malware',
'Remediation',
'Job',
'Sinkhole',
'TIM',
'PAN-OS',
],
total: 1,
};
const mockUseSubActionPlaybooks = jest.fn().mockImplementation(() => ({
isLoading: false,
response,
error: null,
}));
const mockUseSubAction = jest.fn<Result, [UseSubActionParams<unknown>]>(mockUseSubActionPlaybooks);
const mockToasts = { danger: jest.fn(), warning: jest.fn() };
jest.mock(triggersActionsPath, () => {
const original = jest.requireActual(triggersActionsPath);
return {
...original,
useSubAction: (params: UseSubActionParams<unknown>) => mockUseSubAction(params),
useKibana: () => ({
...original.useKibana(),
notifications: { toasts: mockToasts },
}),
};
});
describe('XSOARParamsFields renders', () => {
const subActionParams: XSOARRunActionParams = {
name: 'new incident',
playbookId: '8db0105c-f674-4d83-8095-f95a9f61e77a',
createInvestigation: false,
severity: 2,
isRuleSeverity: false,
body: '',
};
const actionParams: ExecutorParams = {
subAction: SUB_ACTION.RUN,
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: { name: [] },
index: 0,
messageVariables: [],
};
beforeEach(() => {
jest.clearAllMocks();
});
describe('New connector', () => {
it('should render empty run form', () => {
const props = { ...defaultProps, actionParams: {} };
const { getByTestId } = render(<XSOARParamsFields {...props} />);
expect(getByTestId('nameInput')).toBeInTheDocument();
expect(getByTestId('xsoar-playbookSelector')).toBeInTheDocument();
expect(getByTestId('rule-severity-toggle')).toBeInTheDocument();
expect(getByTestId('bodyJsonEditor')).toBeInTheDocument();
expect(getByTestId('rule-severity-toggle')).not.toBeChecked();
expect(getByTestId('bodyJsonEditor')).toHaveProperty('value', '');
expect(editAction).toHaveBeenCalledWith('subAction', SUB_ACTION.RUN, 0);
expect(editAction).toHaveBeenCalledWith(
'subActionParams',
{ createInvestigation: false, severity: 0 },
0
);
});
it('should render empty test form', () => {
const props = { ...defaultProps, actionParams: {}, executionMode: ActionConnectorMode.Test };
const { getByTestId } = render(<XSOARParamsFields {...props} />);
expect(getByTestId('nameInput')).toBeInTheDocument();
expect(getByTestId('xsoar-playbookSelector')).toBeInTheDocument();
expect(getByTestId('severitySelectInput')).toBeInTheDocument();
expect(getByTestId('bodyJsonEditor')).toBeInTheDocument();
expect(getByTestId('severitySelectInput')).toHaveValue('0');
expect(getByTestId('bodyJsonEditor')).toHaveProperty('value', '');
expect(editAction).toHaveBeenCalledWith('subAction', SUB_ACTION.RUN, 0);
expect(editAction).toHaveBeenCalledWith(
'subActionParams',
{ createInvestigation: false, severity: 0 },
0
);
});
it('should renders playbook selector and start investigation toggle after playbook selection', async () => {
const props = { ...defaultProps, actionParams: {} };
render(<XSOARParamsFields {...props} />);
expect(mockUseSubActionPlaybooks).toHaveBeenCalledWith(
expect.objectContaining({ subAction: 'getPlaybooks' })
);
await waitFor(() => {
expect(screen.getByTestId('comboBoxSearchInput')).not.toBeDisabled();
});
await userEvent.click(screen.getByTestId('comboBoxSearchInput'));
expect(screen.getByText('playbook0')).toBeInTheDocument();
await userEvent.click(screen.getByText('playbook0'), { pointerEventsCheck: 0 });
await waitFor(() => {
expect(editAction).toHaveBeenCalledTimes(3);
expect(editAction).toHaveBeenCalledWith('subAction', SUB_ACTION.RUN, 0);
expect(editAction).toHaveBeenCalledWith(
'subActionParams',
{ createInvestigation: false, severity: 0 },
0
);
expect(editAction).toHaveBeenCalledWith(
'subActionParams',
{
createInvestigation: false,
playbookId: '8db0105c-f674-4d83-8095-f95a9f61e77a',
severity: 0,
},
0
);
});
expect(screen.getByTestId('createInvestigation-toggle')).toBeInTheDocument();
expect(screen.getByTestId('createInvestigation-toggle')).not.toBeChecked();
});
});
describe('Edit connector', () => {
it('all Params fields is rendered', () => {
const { getByTestId } = render(<XSOARParamsFields {...defaultProps} />);
expect(mockUseSubActionPlaybooks).toHaveBeenCalledWith(
expect.objectContaining({ subAction: 'getPlaybooks' })
);
expect(getByTestId('nameInput')).toBeInTheDocument();
expect(getByTestId('xsoar-playbookSelector')).toBeInTheDocument();
expect(getByTestId('createInvestigation-toggle')).toBeInTheDocument();
expect(getByTestId('rule-severity-toggle')).toBeInTheDocument();
expect(getByTestId('severitySelectInput')).toBeInTheDocument();
expect(getByTestId('bodyJsonEditor')).toBeInTheDocument();
expect(getByTestId('nameInput')).toHaveValue('new incident');
expect(getByTestId('comboBoxSearchInput')).toHaveProperty('value', 'playbook0');
expect(getByTestId('createInvestigation-toggle')).not.toBeChecked();
expect(getByTestId('rule-severity-toggle')).not.toBeChecked();
expect(getByTestId('severitySelectInput')).toHaveValue('2');
expect(getByTestId('bodyJsonEditor')).toHaveProperty('value', '');
});
it('hides the severity select input when rule severity is enabled', () => {
const { getByTestId } = render(<XSOARParamsFields {...defaultProps} />);
const ruleSeverityToggleEl = getByTestId('rule-severity-toggle');
fireEvent.click(ruleSeverityToggleEl);
expect(getByTestId('rule-severity-toggle')).toBeEnabled();
expect(editAction).toHaveBeenCalledWith(
'subActionParams',
{ ...subActionParams, severity: 2, isRuleSeverity: true },
0
);
expect(screen.queryByTestId('severitySelectInput')).not.toBeInTheDocument();
});
it('should show warning if playbook not found', () => {
const props = {
...defaultProps,
actionParams: { subActionParams: { ...subActionParams, playbookId: 'wrong-playbookId' } },
};
render(<XSOARParamsFields {...props} />);
expect(mockToasts.warning).toHaveBeenCalledWith({
title: translations.PLAYBOOK_NOT_FOUND_WARNING,
});
});
it('should show error when playbooks subAction has error', () => {
const errorMessage = 'something broke';
mockUseSubActionPlaybooks.mockReturnValueOnce({
isLoading: false,
response,
error: new Error(errorMessage),
});
render(<XSOARParamsFields {...defaultProps} />);
expect(mockToasts.danger).toHaveBeenCalledWith({
title: translations.PLAYBOOKS_ERROR,
body: errorMessage,
});
});
it('handles the case when subAction is undefined', () => {
const props = {
...defaultProps,
actionParams: {
...actionParams,
subAction: undefined,
},
};
render(<XSOARParamsFields {...props} />);
expect(editAction).toHaveBeenCalledWith('subAction', SUB_ACTION.RUN, 0);
});
});
});

View file

@ -0,0 +1,298 @@
/*
* 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, useMemo, useCallback } from 'react';
import {
useSubAction,
useKibana,
ActionParamsProps,
JsonEditorWithMessageVariables,
TextFieldWithMessageVariables,
ActionConnectorMode,
} from '@kbn/triggers-actions-ui-plugin/public';
import {
EuiFormRow,
EuiComboBoxOptionOption,
EuiComboBox,
EuiFlexGroup,
EuiFlexItem,
EuiHighlight,
EuiSwitch,
EuiSelect,
} from '@elastic/eui';
import { SUB_ACTION, XSOARSeverity } from '../../../common/xsoar/constants';
import {
ExecutorParams,
XSOARRunActionParams,
XSOARPlaybooksActionResponse,
XSOARPlaybooksActionParams,
XSOARPlaybooksObject,
} from '../../../common/xsoar/types';
import * as translations from './translations';
import { severityOptions } from './constants';
type PlaybookOption = EuiComboBoxOptionOption<XSOARPlaybooksObject>;
const createOption = (playbook: XSOARPlaybooksObject): PlaybookOption => ({
key: playbook.id,
label: playbook.name,
});
const renderPlaybook = (
{ label }: PlaybookOption,
searchValue: string,
contentClassName: string
) => (
<EuiFlexGroup className={contentClassName} direction="row" alignItems="center">
<EuiFlexItem grow={false}>
<EuiHighlight search={searchValue}>{label}</EuiHighlight>
</EuiFlexItem>
</EuiFlexGroup>
);
const XSOARParamsFields: React.FunctionComponent<ActionParamsProps<ExecutorParams>> = ({
actionConnector,
actionParams,
editAction,
index,
errors,
messageVariables,
executionMode,
}) => {
const { toasts } = useKibana().notifications;
const isTest = executionMode === ActionConnectorMode.Test;
const incident = useMemo(
() =>
(actionParams.subActionParams as XSOARRunActionParams) ??
({
severity: XSOARSeverity.UNKNOWN,
createInvestigation: false,
} as unknown as XSOARRunActionParams),
[actionParams.subActionParams]
);
const [connectorId, setConnectorId] = useState<string | undefined>(actionConnector?.id);
const [selectedPlaybookOption, setSelectedPlaybookOption] = useState<
PlaybookOption | null | undefined
>();
const [isRuleSeverity, setIsRuleSeverity] = useState<boolean>(Boolean(incident.isRuleSeverity));
const [playbooks, setPlaybooks] = useState<XSOARPlaybooksObject[]>();
useEffect(() => {
if (actionConnector != null && connectorId !== actionConnector.id) {
setConnectorId(actionConnector?.id);
setSelectedPlaybookOption(null);
setIsRuleSeverity(isTest ? false : true);
editAction(
'subActionParams',
{
severity: XSOARSeverity.UNKNOWN,
createInvestigation: false,
},
index
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [actionConnector]);
useEffect(() => {
if (!actionParams.subAction) {
editAction('subAction', SUB_ACTION.RUN, index);
}
if (!actionParams.subActionParams) {
editAction('subActionParams', incident, index);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [actionParams]);
const {
response: { playbooks: fetchedPlaybooks } = {},
isLoading: isLoadingPlaybooks,
error: playbooksError,
} = useSubAction<XSOARPlaybooksActionParams, XSOARPlaybooksActionResponse>({
connectorId,
subAction: 'getPlaybooks',
});
useEffect(() => {
if (playbooksError) {
toasts.danger({ title: translations.PLAYBOOKS_ERROR, body: playbooksError.message });
setPlaybooks([]);
} else {
setPlaybooks(fetchedPlaybooks);
}
}, [toasts, playbooksError, fetchedPlaybooks]);
const playbooksOptions = useMemo(() => playbooks?.map(createOption) ?? [], [playbooks]);
useEffect(() => {
if (selectedPlaybookOption === undefined && incident.playbookId && playbooks !== undefined) {
const selectedPlaybook = playbooks.find(({ id }) => id === incident.playbookId);
if (selectedPlaybook) {
setSelectedPlaybookOption(createOption(selectedPlaybook));
} else {
toasts.warning({ title: translations.PLAYBOOK_NOT_FOUND_WARNING });
editAction(
'subActionParams',
{ ...incident, playbookId: undefined, createInvestigation: false },
index
);
}
}
if (
selectedPlaybookOption !== undefined &&
selectedPlaybookOption?.key !== incident.playbookId
) {
editAction(
'subActionParams',
{
...incident,
playbookId: selectedPlaybookOption?.key,
createInvestigation:
selectedPlaybookOption === null ? false : incident.createInvestigation,
},
index
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedPlaybookOption, incident.playbookId, playbooks, toasts, editAction, index]);
const selectedPlaybookOptions = useMemo(
() => (selectedPlaybookOption ? [selectedPlaybookOption] : []),
[selectedPlaybookOption]
);
const onChangePlaybook = useCallback(([selected]: PlaybookOption[]) => {
setSelectedPlaybookOption(selected ?? null);
}, []);
return (
<>
<TextFieldWithMessageVariables
index={index}
editAction={(key, value) => {
editAction('subActionParams', { ...incident, [key]: value }, index);
}}
messageVariables={messageVariables}
paramsProperty={'name'}
inputTargetValue={incident.name}
wrapField={true}
formRowProps={{
label: translations.NAME_LABEL,
fullWidth: true,
helpText: '',
isInvalid:
errors.name !== undefined &&
Number(errors.name.length) > 0 &&
incident.name !== undefined,
error: errors.name as string,
}}
errors={errors.name as string[]}
/>
<EuiFormRow
fullWidth
error={errors.playbook as string[]}
isInvalid={!!errors.playbook?.length && selectedPlaybookOption !== undefined}
label={translations.PLAYBOOK_LABEL}
helpText={translations.PLAYBOOK_HELP}
>
<EuiComboBox
aria-label={translations.PLAYBOOK_ARIA_LABEL}
placeholder={translations.PLAYBOOK_PLACEHOLDER}
singleSelection={{ asPlainText: true }}
options={playbooksOptions}
selectedOptions={selectedPlaybookOptions}
onChange={onChangePlaybook}
isDisabled={isLoadingPlaybooks}
isLoading={isLoadingPlaybooks}
renderOption={renderPlaybook}
fullWidth
data-test-subj="xsoar-playbookSelector"
/>
</EuiFormRow>
{selectedPlaybookOption && (
<EuiFormRow fullWidth>
<EuiSwitch
label={translations.START_INVESTIGATION_LABEL}
checked={incident.createInvestigation}
data-test-subj="createInvestigation-toggle"
onChange={(e) => {
editAction(
'subActionParams',
{
...incident,
createInvestigation: e.target.checked,
},
index
);
}}
/>
</EuiFormRow>
)}
{!isTest && (
<EuiFormRow fullWidth>
<EuiSwitch
label={translations.IS_RULE_SEVERITY_LABEL}
checked={Boolean(isRuleSeverity)}
data-test-subj="rule-severity-toggle"
onChange={(e) => {
setIsRuleSeverity(e.target.checked);
editAction(
'subActionParams',
{
...incident,
isRuleSeverity: e.target.checked,
},
index
);
}}
/>
</EuiFormRow>
)}
{!Boolean(isRuleSeverity) && (
<EuiFormRow fullWidth label={translations.SEVERITY_LABEL}>
<EuiSelect
fullWidth
data-test-subj="severitySelectInput"
disabled={Boolean(isRuleSeverity)}
value={incident.severity ?? severityOptions[0].value}
options={severityOptions}
onChange={(e) => {
editAction(
'subActionParams',
{ ...incident, severity: parseFloat(e.target.value) },
index
);
}}
/>
</EuiFormRow>
)}
<JsonEditorWithMessageVariables
key={connectorId}
messageVariables={messageVariables}
paramsProperty={'body'}
inputTargetValue={incident.body}
label={translations.BODY_LABEL}
ariaLabel={translations.BODY_DESCRIPTION}
onDocumentsChange={(json: string) =>
editAction('subActionParams', { ...incident, body: json }, index)
}
dataTestSubj="xsoar-body"
onBlur={() => {
if (!incident.body) {
editAction('subActionParams', { ...incident, body: null }, index);
}
}}
/>
</>
);
};
// eslint-disable-next-line import/no-default-export
export { XSOARParamsFields as default };

View file

@ -0,0 +1,139 @@
/*
* 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.xsoar.urlFieldLabel', {
defaultMessage: 'URL',
});
export const SELECT_MESSAGE = i18n.translate(
'xpack.stackConnectors.components.xsoar.selectMessageText',
{
defaultMessage: 'Create an incident in XSOAR',
}
);
export const API_KEY_LABEL = i18n.translate(
'xpack.stackConnectors.components.xsoar.apiKeyFieldLabel',
{
defaultMessage: 'API key',
}
);
export const API_KEY_ID_LABEL = i18n.translate(
'xpack.stackConnectors.components.xsoar.apiKeyIDFieldLabel',
{
defaultMessage: 'API key ID',
}
);
export const API_KEY_ID_HELP_TEXT = i18n.translate(
'xpack.stackConnectors.components.xsoar.apiKeyIDFieldHelpText',
{
defaultMessage:
'Enter the API key ID (the unique serial number for your API key) to authenticate with your XSOAR cloud instance.',
}
);
export const NAME_LABEL = i18n.translate(
'xpack.stackConnectors.components.xsoar.params.nameFieldLabel',
{
defaultMessage: 'Name',
}
);
export const NAME_REQUIRED = i18n.translate(
'xpack.stackConnectors.components.xsoar.params.error.requiredNameText',
{
defaultMessage: 'Incident name is required.',
}
);
export const BODY_REQUIRED = i18n.translate(
'xpack.stackConnectors.components.xsoar.params.error.requiredBodyText',
{
defaultMessage: 'Body is required.',
}
);
export const START_INVESTIGATION_LABEL = i18n.translate(
'xpack.stackConnectors.components.xsoar.params.startInvestigationToggleLabel',
{
defaultMessage: 'Start investigation',
}
);
export const SEVERITY_LABEL = i18n.translate(
'xpack.stackConnectors.components.xsoar.params.severitySelectInputLabel',
{
defaultMessage: 'Severity',
}
);
export const IS_RULE_SEVERITY_LABEL = i18n.translate(
'xpack.stackConnectors.components.xsoar.params.isRuleSeverityToggleLabel',
{
defaultMessage: 'Use severity assigned to the rule',
}
);
export const PLAYBOOKS_ERROR = i18n.translate(
'xpack.stackConnectors.components.xsoar.params.componentError.playbooksRequestFailed',
{
defaultMessage: 'Unable to retrieve playbooks from XSOAR.',
}
);
export const PLAYBOOK_NOT_FOUND_WARNING = i18n.translate(
'xpack.stackConnectors.components.xsoar.params.componentWarning.playbookNotFound',
{
defaultMessage: 'Could not find the selected playbook. Choose a different one.',
}
);
export const BODY_LABEL = i18n.translate(
'xpack.stackConnectors.components.xsoar.params.bodyFieldLabel',
{
defaultMessage: 'Body',
}
);
export const BODY_DESCRIPTION = i18n.translate(
'xpack.stackConnectors.components.xsoar.params.bodyCodeEditorAriaLabel',
{
defaultMessage: 'Code editor',
}
);
export const PLAYBOOK_LABEL = i18n.translate(
'xpack.stackConnectors.components.xsoar.params.playbookFieldLabel',
{
defaultMessage: 'XSOAR playbooks',
}
);
export const PLAYBOOK_HELP = i18n.translate(
'xpack.stackConnectors.components.xsoar.params.playbookHelp',
{
defaultMessage: 'The XSOAR playbook to associate with incident',
}
);
export const PLAYBOOK_PLACEHOLDER = i18n.translate(
'xpack.stackConnectors.components.xsoar.params.playbookPlaceholder',
{
defaultMessage: 'Select a playbook',
}
);
export const PLAYBOOK_ARIA_LABEL = i18n.translate(
'xpack.stackConnectors.components.xsoar.params.playbookFieldAriaLabel',
{
defaultMessage: 'Select an XSOAR playbook.',
}
);

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 type { ActionTypeModel as ConnectorTypeModel } from '@kbn/triggers-actions-ui-plugin/public';
import type { Config, Secrets, ExecutorParams } from '../../../common/xsoar/types';
export type XSOARConnector = ConnectorTypeModel<Config, Secrets, ExecutorParams>;

View file

@ -0,0 +1,72 @@
/*
* 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/xsoar/constants';
import { ExperimentalFeaturesService } from '../../common/experimental_features_service';
import * as translations from './translations';
const CONNECTOR_TYPE_ID = '.xsoar';
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('XSOAR RUN action params validation', () => {
test('RUN action params validation succeeds when action params is valid', async () => {
const actionParams = {
subAction: SUB_ACTION.RUN,
subActionParams: {
name: 'new incident',
playbookId: 'playbook0',
createInvestigation: false,
severity: 1,
body: '',
},
};
expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
errors: {
name: [],
},
});
});
test('RUN action params validation fails when required fields is not valid', async () => {
const actionParams = {
subAction: SUB_ACTION.RUN,
subActionParams: {
name: '',
playbookId: 'playbook0',
createInvestigation: false,
severity: 1,
body: '',
},
};
expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
errors: {
name: [translations.NAME_REQUIRED],
},
});
});
});

View file

@ -0,0 +1,45 @@
/*
* 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 { GenericValidationResult } from '@kbn/triggers-actions-ui-plugin/public/types';
import { XSOARConnector } from './types';
import { XSOAR_CONNECTOR_ID, SUB_ACTION, XSOAR_TITLE } from '../../../common/xsoar/constants';
import { ExecutorParams } from '../../../common/xsoar/types';
import * as i18n from './translations';
interface ValidationErrors {
name: string[];
}
export function getConnectorType(): XSOARConnector {
return {
id: XSOAR_CONNECTOR_ID,
hideInUi: true,
iconClass: lazy(() => import('./logo')),
selectMessage: i18n.SELECT_MESSAGE,
actionTypeTitle: XSOAR_TITLE,
validateParams: async (
actionParams: ExecutorParams
): Promise<GenericValidationResult<ValidationErrors>> => {
const translations = await import('./translations');
const errors: ValidationErrors = {
name: [],
};
const { subAction, subActionParams } = actionParams;
if (subAction === SUB_ACTION.RUN) {
if (!subActionParams?.name?.length) {
errors.name.push(translations.NAME_REQUIRED);
}
}
return { errors };
},
actionConnectorFields: lazy(() => import('./connector')),
actionParamsFields: lazy(() => import('./params')),
};
}

View file

@ -32,6 +32,7 @@ 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 { getConnectorType as getXSOARConnectorType } from './xsoar';
import { getOpsgenieConnectorType } from './opsgenie';
import type { ActionParamsType as ServiceNowITSMActionParams } from './servicenow_itsm';
import type { ActionParamsType as ServiceNowSIRActionParams } from './servicenow_sir';
@ -114,6 +115,7 @@ export function registerConnectorTypes({
actions.registerSubActionConnectorType(getD3SecurityConnectorType());
actions.registerSubActionConnectorType(getResilientConnectorType());
actions.registerSubActionConnectorType(getTheHiveConnectorType());
actions.registerSubActionConnectorType(getXSOARConnectorType());
if (experimentalFeatures.sentinelOneConnectorOn) {
actions.registerSubActionConnectorType(getSentinelOneConnectorType());

View file

@ -0,0 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { XSOARConnectorType } from '.';
import { getConnectorType } from '.';
let connectorType: XSOARConnectorType;
describe('XSOAR Connector', () => {
beforeEach(() => {
connectorType = getConnectorType();
});
test('exposes the connector as `XSOAR` with id `.xsoar`', () => {
expect(connectorType.id).toEqual('.xsoar');
expect(connectorType.name).toEqual('XSOAR');
});
});

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 type { SubActionConnectorType } from '@kbn/actions-plugin/server/sub_action_framework/types';
import { ValidatorType } from '@kbn/actions-plugin/server/sub_action_framework/types';
import { SecurityConnectorFeatureId } from '@kbn/actions-plugin/common';
import { urlAllowListValidator } from '@kbn/actions-plugin/server';
import { XSOAR_CONNECTOR_ID, XSOAR_TITLE } from '../../../common/xsoar/constants';
import { ConfigSchema, SecretsSchema } from '../../../common/xsoar/schema';
import type { Config, Secrets } from '../../../common/xsoar/types';
import { XSOARConnector } from './xsoar';
import { renderParameterTemplates } from './render';
export type XSOARConnectorType = SubActionConnectorType<Config, Secrets>;
export function getConnectorType(): XSOARConnectorType {
return {
id: XSOAR_CONNECTOR_ID,
minimumLicenseRequired: 'platinum',
name: XSOAR_TITLE,
getService: (params) => new XSOARConnector(params),
supportedFeatureIds: [SecurityConnectorFeatureId],
schema: {
config: ConfigSchema,
secrets: SecretsSchema,
},
renderParameterTemplates,
validators: [{ type: ValidatorType.CONFIG, validator: urlAllowListValidator('url') }],
};
}

View file

@ -0,0 +1,78 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { loggingSystemMock } from '@kbn/core/server/mocks';
import { renderParameterTemplates } from './render';
import { SUB_ACTION } from '../../../common/xsoar/constants';
import Mustache from 'mustache';
const params = {
subAction: SUB_ACTION.RUN,
subActionParams: {
name: 'new incident - {{alert.uuid}}',
playbookId: 'playbook0',
createInvestigation: true,
severity: 0,
isRuleSeverity: true,
body: '',
},
};
const variables = {
url: 'https://example.com',
context: { rule: { severity: 'medium' } },
alert: { uuid: 'test123' },
};
const logger = loggingSystemMock.createLogger();
describe('XSOAR - renderParameterTemplates', () => {
it('should rendered subActionParams with variables', () => {
const result = renderParameterTemplates(logger, params, variables);
expect(result.subActionParams).toEqual({
name: `new incident - ${variables.alert.uuid}`,
playbookId: 'playbook0',
createInvestigation: true,
severity: 2,
isRuleSeverity: true,
body: '',
});
});
it('should not use rule severity if isRuleSeverity is false', () => {
const paramswithoutRuleSeverity = {
...params,
subActionParams: { ...params.subActionParams, isRuleSeverity: false },
};
const result = renderParameterTemplates(logger, paramswithoutRuleSeverity, variables);
expect(result.subActionParams).toEqual({
name: `new incident - ${variables.alert.uuid}`,
playbookId: 'playbook0',
createInvestigation: true,
severity: 0,
isRuleSeverity: false,
body: '',
});
});
it('should render error body', () => {
const errorMessage = 'test error';
jest.spyOn(Mustache, 'render').mockImplementation(() => {
throw new Error(errorMessage);
});
const result = renderParameterTemplates(logger, params, variables);
expect(result.subActionParams).toEqual({
body: 'error rendering mustache template "": test error',
createInvestigation: true,
name: 'error rendering mustache template "new incident - {{alert.uuid}}": test error',
playbookId: 'error rendering mustache template "playbook0": test error',
severity: 0,
isRuleSeverity: true,
});
});
});

View file

@ -0,0 +1,47 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { ExecutorParams } from '@kbn/actions-plugin/server/sub_action_framework/types';
import {
renderMustacheObject,
renderMustacheString,
} from '@kbn/actions-plugin/server/lib/mustache_renderer';
import type { RenderParameterTemplates } from '@kbn/actions-plugin/server/types';
function mapSeverity(severity: string): number {
switch (severity) {
case 'low':
return 1;
case 'medium':
return 2;
case 'high':
return 3;
case 'critical':
return 4;
default:
return 0;
}
}
export const renderParameterTemplates: RenderParameterTemplates<ExecutorParams> = (
logger,
params,
variables
) => {
return {
...params,
subActionParams: {
...renderMustacheObject(logger, params.subActionParams, variables),
severity:
params.subActionParams.isRuleSeverity === true
? mapSeverity(
renderMustacheString(logger, '{{context.rule.severity}}', variables, 'json')
)
: params.subActionParams.severity,
},
};
};

View file

@ -0,0 +1,527 @@
/*
* 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 { XSOARConnector } from './xsoar';
import { actionsConfigMock } from '@kbn/actions-plugin/server/actions_config.mock';
import { XSOAR_CONNECTOR_ID } from '../../../common/xsoar/constants';
import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
import { actionsMock } from '@kbn/actions-plugin/server/mocks';
import {
XSOARRunActionResponseSchema,
XSOARPlaybooksActionResponseSchema,
} from '../../../common/xsoar/schema';
import type { XSOARRunActionParams } from '../../../common/xsoar/types';
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
const mockTime = new Date('2025-02-20T10:10:30.000');
describe('XSOARConnector', () => {
const logger = loggingSystemMock.createLogger();
const connector = new XSOARConnector({
configurationUtilities: actionsConfigMock.create(),
connector: { id: '1', type: XSOAR_CONNECTOR_ID },
config: { url: 'https://example.com' },
secrets: { apiKey: 'test123', apiKeyID: null },
logger,
services: actionsMock.createServices(),
});
const cloudConnector = new XSOARConnector({
configurationUtilities: actionsConfigMock.create(),
connector: { id: '2', type: XSOAR_CONNECTOR_ID },
config: { url: 'https://test.com' },
secrets: { apiKey: 'test123', apiKeyID: '123' },
logger,
services: actionsMock.createServices(),
});
let mockRequest: jest.Mock;
let mockCloudRequest: jest.Mock;
let mockError: jest.Mock;
let connectorUsageCollector: ConnectorUsageCollector;
beforeAll(() => {
jest.useFakeTimers();
jest.setSystemTime(mockTime);
});
afterAll(() => {
jest.useRealTimers();
});
beforeEach(() => {
mockError = jest.fn().mockImplementation(() => {
throw new Error('API Error');
});
jest.clearAllMocks();
connectorUsageCollector = new ConnectorUsageCollector({
logger,
connectorId: 'test-connector-id',
});
});
describe('getPlaybooks', () => {
const mockResponse = {
data: {
playbooks: [
{
id: '8db0105c-f674-4d83-8095-f95a9f61e77a',
version: 4,
cacheVersn: 0,
sequenceNumber: 33831652,
primaryTerm: 11,
modified: '2023-12-12T13:51:15.668021556Z',
sizeInBytes: 0,
packID: '',
packName: '',
itemVersion: '',
fromServerVersion: '',
toServerVersion: '',
propagationLabels: ['all'],
definitionId: '',
vcShouldIgnore: false,
vcShouldKeepItemLegacyProdMachine: false,
commitMessage: '',
shouldCommit: false,
name: 'aaa',
nameRaw: 'aaa',
prevName: 'aaa',
startTaskId: '0',
tasks: {
'0': {
id: '0',
taskId: 'e228a044-2ad5-4ab0-873a-d5bb94a5c1b4',
type: 'start',
task: {
id: 'e228a044-2ad5-4ab0-873a-d5bb94a5c1b4',
version: 1,
cacheVersn: 0,
sequenceNumber: 13431901,
primaryTerm: 8,
modified: '2023-05-23T07:16:19.930125981Z',
sizeInBytes: 0,
},
nextTasks: {
'#none#': ['1'],
},
continueOnErrorType: '',
view: {
position: {
x: 450,
y: 50,
},
},
evidenceData: {},
},
'1': {
id: '1',
taskId: 'c28b63d3-c860-4e16-82b4-6db6b58bdee3',
type: 'regular',
task: {
id: 'c28b63d3-c860-4e16-82b4-6db6b58bdee3',
version: 1,
cacheVersn: 0,
sequenceNumber: 33831651,
primaryTerm: 11,
modified: '2023-12-12T13:51:15.604271789Z',
sizeInBytes: 0,
name: 'Untitled Task 1',
description: 'commands.local.cmd.set.incident',
scriptId: 'Builtin|||setIncident',
type: 'regular',
isCommand: true,
brand: 'Builtin',
},
scriptArguments: {
severity: {
simple: '1',
},
},
continueOnErrorType: '',
view: {
position: {
x: 450,
y: 200,
},
},
evidenceData: {},
},
},
taskIds: [
'e228a044-2ad5-4ab0-873a-d5bb94a5c1b4',
'c28b63d3-c860-4e16-82b4-6db6b58bdee3',
],
scriptIds: [],
commands: ['setIncident'],
brands: ['Builtin'],
missingScriptsIds: ['Builtin|||setIncident'],
view: {
linkLabelsPosition: {},
paper: {
dimensions: {
height: 245,
width: 380,
x: 450,
y: 50,
},
},
},
inputs: null,
outputs: null,
quiet: true,
},
],
tags: [
'Endpoint',
'ITDR',
'Automated',
'Phishing',
'Sandbox',
'Joe Security',
'Severity',
'Malware',
'Sumo Logic',
'Remediation',
'Job',
'Code42 Incydr',
'Sinkhole',
'XDR',
'TIM',
'PAN-OS',
'Vulnerability',
'Virus',
'Domaintools',
],
total: 1,
},
};
beforeEach(() => {
mockRequest = jest.fn().mockResolvedValue(mockResponse);
mockCloudRequest = jest.fn().mockResolvedValue(mockResponse);
// @ts-ignore
connector.request = mockRequest;
// @ts-ignore
cloudConnector.request = mockCloudRequest;
jest.clearAllMocks();
});
it('XSOAR API call is successful with correct parameters', async () => {
const response = await connector.getPlaybooks(undefined, connectorUsageCollector);
expect(mockRequest).toBeCalledTimes(1);
expect(mockRequest).toHaveBeenCalledWith(
{
method: 'post',
url: 'https://example.com/playbook/search',
data: {},
responseSchema: XSOARPlaybooksActionResponseSchema,
headers: {
Authorization: 'test123',
},
timeout: 15000,
},
connectorUsageCollector
);
expect(response).toEqual(mockResponse.data);
});
it('Auth headers are correctly set for cloud instance', async () => {
const response = await cloudConnector.getPlaybooks(undefined, connectorUsageCollector);
expect(mockCloudRequest).toBeCalledTimes(1);
expect(mockCloudRequest).toHaveBeenCalledWith(
{
method: 'post',
url: 'https://test.com/xsoar/public/v1/playbook/search',
data: {},
responseSchema: XSOARPlaybooksActionResponseSchema,
headers: {
Authorization: 'test123',
'x-xdr-auth-id': '123',
},
timeout: 15000,
},
connectorUsageCollector
);
expect(response).toEqual(mockResponse.data);
});
it('errors during API calls are properly handled', async () => {
// @ts-ignore
connector.request = mockError;
await expect(connector.getPlaybooks(undefined, connectorUsageCollector)).rejects.toThrow(
'API Error'
);
});
});
describe('run', () => {
const mockResponse = {
data: {
id: '178791',
version: 0,
cacheVersn: 0,
modified: '1970-01-01T00:00:00Z',
sizeInBytes: 0,
CustomFields: {
bmcassignee: [{}],
bmccustomer: [{}],
bmcrequester: [{}],
containmentsla: {
accumulatedPause: 0,
breachTriggered: false,
dueDate: '0001-01-01T00:00:00Z',
endDate: '0001-01-01T00:00:00Z',
lastPauseDate: '0001-01-01T00:00:00Z',
runStatus: 'idle',
sla: 30,
slaStatus: -1,
startDate: '0001-01-01T00:00:00Z',
totalDuration: 0,
},
crowdstrikefalconbehaviourpatterndispositiondetails: [{}, {}, {}],
datadogcloudsiem: [{}, {}, {}],
dataminrpulserelatedterms: [{}, {}, {}],
decyfirdatadetails: [{}, {}, {}],
detectionsla: {
accumulatedPause: 0,
breachTriggered: false,
dueDate: '0001-01-01T00:00:00Z',
endDate: '0001-01-01T00:00:00Z',
lastPauseDate: '0001-01-01T00:00:00Z',
runStatus: 'idle',
sla: 20,
slaStatus: -1,
startDate: '0001-01-01T00:00:00Z',
totalDuration: 0,
},
domaintoolsirisdetect: [{}, {}, {}],
endpoint: [{}],
externalid: '178791',
extrahoprevealxdetectiondevices: [{}, {}, {}],
extrahoprevealxmitretechniques: [{}, {}, {}],
filerelationships: [{}, {}, {}],
fortisiemattacktactics: [{}, {}],
fortisiemevents: [{}],
incidentduration: {
accumulatedPause: 0,
breachTriggered: false,
dueDate: '0001-01-01T00:00:00Z',
endDate: '0001-01-01T00:00:00Z',
lastPauseDate: '0001-01-01T00:00:00Z',
runStatus: 'idle',
sla: 0,
slaStatus: -1,
startDate: '0001-01-01T00:00:00Z',
totalDuration: 0,
},
incidentrdpachehuntingstringssimilarity: [{}, {}, {}],
incidentrdpcachehuntingstringsifter: [{}, {}, {}],
inventasource: [{}],
microsoftsentinelowner: [],
qintelqwatchexposures: [{}, {}, {}],
remediationsla: {
accumulatedPause: 0,
breachTriggered: false,
dueDate: '0001-01-01T00:00:00Z',
endDate: '0001-01-01T00:00:00Z',
lastPauseDate: '0001-01-01T00:00:00Z',
runStatus: 'idle',
sla: 7200,
slaStatus: -1,
startDate: '0001-01-01T00:00:00Z',
totalDuration: 0,
},
rsametasevents: [],
rsarawlogslist: [],
securitypolicymatch: [{}],
similarincidentsdbot: [{}],
spycloudcompassdevicedata: [{}, {}, {}],
suspiciousexecutions: [{}, {}, {}],
timetoassignment: {
accumulatedPause: 0,
breachTriggered: false,
dueDate: '0001-01-01T00:00:00Z',
endDate: '0001-01-01T00:00:00Z',
lastPauseDate: '0001-01-01T00:00:00Z',
runStatus: 'idle',
sla: 0,
slaStatus: -1,
startDate: '0001-01-01T00:00:00Z',
totalDuration: 0,
},
triagesla: {
accumulatedPause: 0,
breachTriggered: false,
dueDate: '0001-01-01T00:00:00Z',
endDate: '0001-01-01T00:00:00Z',
lastPauseDate: '0001-01-01T00:00:00Z',
runStatus: 'idle',
sla: 30,
slaStatus: -1,
startDate: '0001-01-01T00:00:00Z',
totalDuration: 0,
},
urlsslverification: [],
xdralertsearchresults: [{}, {}, {}],
xdrinvestigationresults: [
{},
{},
{},
{
columnheader1: '',
},
{},
{
columnheader1: '',
},
{},
{},
],
xpanseserviceclassifications: [{}, {}, {}],
xpanseservicevalidation: [
{
columnheader1: '',
},
{},
{},
],
},
account: '',
autime: 1713700028107000000,
type: 'Unclassified',
rawType: 'Unclassified',
name: 'My test incident',
rawName: 'My test incident',
status: 0,
custom_status: '',
resolution_status: '',
reason: '',
created: '2024-04-21T11:47:08.107Z',
occurred: '2024-04-21T11:47:08.107982676Z',
closed: '0001-01-01T00:00:00Z',
sla: 0,
severity: 2,
investigationId: '',
labels: [
{
value: '',
type: 'Instance',
},
{
value: 'Manual',
type: 'Brand',
},
],
attachment: null,
details: 'My test incident',
openDuration: 0,
lastOpen: '0001-01-01T00:00:00Z',
closingUserId: '',
owner: '',
activated: '0001-01-01T00:00:00Z',
closeReason: '',
rawCloseReason: '',
closeNotes: '',
playbookId: 'playbook0',
dueDate: '2024-05-01T11:47:08.107988742Z',
reminder: '0001-01-01T00:00:00Z',
runStatus: '',
notifyTime: '0001-01-01T00:00:00Z',
phase: '',
rawPhase: '',
isPlayground: false,
rawJSON: '',
parent: '',
parentXDRIncident: '',
retained: false,
category: '',
rawCategory: '',
linkedIncidents: null,
linkedCount: 0,
droppedCount: 0,
sourceInstance: '',
sourceBrand: 'Manual',
canvases: null,
lastJobRunTime: '0001-01-01T00:00:00Z',
feedBased: false,
dbotMirrorId: '',
dbotMirrorInstance: '',
dbotMirrorDirection: '',
dbotDirtyFields: null,
dbotCurrentDirtyFields: null,
dbotMirrorTags: null,
dbotMirrorLastSync: '0001-01-01T00:00:00Z',
isDebug: false,
},
};
beforeEach(() => {
mockRequest = jest.fn().mockResolvedValue(mockResponse);
mockCloudRequest = jest.fn().mockResolvedValue(mockResponse);
// @ts-ignore
connector.request = mockRequest;
// @ts-ignore
cloudConnector.request = mockCloudRequest;
jest.clearAllMocks();
});
const incident: XSOARRunActionParams = {
name: 'My test incident',
playbookId: 'playbook0',
createInvestigation: false,
severity: 2,
isRuleSeverity: false,
body: JSON.stringify({}),
};
const malformedIncident: XSOARRunActionParams = {
name: 'My test incident 2',
playbookId: 'playbook0',
createInvestigation: false,
isRuleSeverity: false,
severity: 2,
body: '{',
};
const { body, isRuleSeverity, ...incidentWithoutBody } = incident;
const expectedIncident = { ...JSON.parse(body || '{}'), ...incidentWithoutBody };
it('XSOAR API call is successful with correct parameters', async () => {
await connector.run(incident, connectorUsageCollector);
expect(mockRequest).toBeCalledTimes(1);
expect(mockRequest).toHaveBeenCalledWith(
{
url: 'https://example.com/incident',
method: 'post',
responseSchema: XSOARRunActionResponseSchema,
data: expectedIncident,
headers: {
Authorization: 'test123',
},
},
connectorUsageCollector
);
});
it('errors during API calls are properly handled', async () => {
// @ts-ignore
connector.request = mockError;
await expect(connector.run(expectedIncident, connectorUsageCollector)).rejects.toThrow(
'API Error'
);
});
it('error when malformed incident is passed', async () => {
await expect(connector.run(malformedIncident, connectorUsageCollector)).rejects.toThrowError(
`Error parsing Body: SyntaxError: Expected property name or '}' in JSON at position 1`
);
});
});
});

View file

@ -0,0 +1,143 @@
/*
* 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 type { ServiceParams } from '@kbn/actions-plugin/server';
import { SubActionConnector } from '@kbn/actions-plugin/server';
import type { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
import type { AxiosError } from 'axios';
import type {
Config,
Secrets,
XSOARRunActionParams,
XSOARPlaybooksActionResponse,
} from '../../../common/xsoar/types';
import {
XSOARRunActionResponseSchema,
XSOARPlaybooksActionResponseSchema,
XSOARPlaybooksActionParamsSchema,
XSOARRunActionParamsSchema,
} from '../../../common/xsoar/schema';
import { SUB_ACTION } from '../../../common/xsoar/constants';
export const CLOUD_API_PATH = '/xsoar/public/v1';
export const INCIDENT_PATH = '/incident';
export const PLAYBOOKS_PATH = '/playbook/search';
export class XSOARConnector extends SubActionConnector<Config, Secrets> {
private urls: {
playbooks: string;
incident: string;
};
private isCloud: boolean;
private ConnectorId: string;
constructor(params: ServiceParams<Config, Secrets>) {
super(params);
this.isCloud = this.secrets.apiKeyID !== null && this.secrets.apiKeyID !== '';
this.urls = {
playbooks: this.isCloud
? `${this.config.url}${CLOUD_API_PATH}${PLAYBOOKS_PATH}`
: `${this.config.url}${PLAYBOOKS_PATH}`,
incident: this.isCloud
? `${this.config.url}${CLOUD_API_PATH}${INCIDENT_PATH}`
: `${this.config.url}${INCIDENT_PATH}`,
};
this.ConnectorId = params.connector.id;
this.registerSubActions();
}
private registerSubActions() {
this.registerSubAction({
name: SUB_ACTION.PLAYBOOKS,
method: 'getPlaybooks',
schema: XSOARPlaybooksActionParamsSchema,
});
this.registerSubAction({
name: SUB_ACTION.RUN,
method: 'run',
schema: XSOARRunActionParamsSchema,
});
}
private getAuthHeaders() {
return this.isCloud
? { Authorization: this.secrets.apiKey, 'x-xdr-auth-id': this.secrets.apiKeyID }
: { Authorization: this.secrets.apiKey };
}
protected getResponseErrorMessage(error: AxiosError): string {
if (error.response?.statusText) {
return `API Error: ${error.response?.statusText}`;
}
return error.toString();
}
private formatIncidentBody(incident: XSOARRunActionParams) {
try {
const { body, isRuleSeverity, ...incidentWithoutBody } = incident;
const bodyJson = JSON.parse(body || '{}');
const mergedIncident = { ...bodyJson, ...incidentWithoutBody };
return mergedIncident;
} catch (err) {
const errMessage = i18n.translate('xpack.stackConnectors.xsoar.BodyParsingErrorMessage', {
defaultMessage: 'error triggering XSOAR workflow, parsing body',
});
this.logger.error(`error on ${this.ConnectorId} XSOAR event: ${errMessage}: ${err.message}`);
throw new Error(
i18n.translate('xpack.stackConnectors.xsoar.incidentBodyParsingError', {
defaultMessage: 'Error parsing Body: {err}',
values: {
err: err.toString(),
},
})
);
}
}
public async run(
incident: XSOARRunActionParams,
connectorUsageCollector: ConnectorUsageCollector
) {
const mergedIncident = this.formatIncidentBody(incident);
await this.request(
{
method: 'post',
url: `${this.urls.incident}`,
data: mergedIncident,
headers: this.getAuthHeaders(),
responseSchema: XSOARRunActionResponseSchema,
},
connectorUsageCollector
);
}
public async getPlaybooks(
params: unknown,
connectorUsageCollector: ConnectorUsageCollector
): Promise<XSOARPlaybooksActionResponse> {
const res = await this.request(
{
method: 'post',
url: `${this.urls.playbooks}`,
data: {},
headers: this.getAuthHeaders(),
responseSchema: XSOARPlaybooksActionResponseSchema,
timeout: 15000,
},
connectorUsageCollector
);
return res.data;
}
}

View file

@ -141,7 +141,7 @@ describe('Stack Connectors Plugin', () => {
name: 'Torq',
})
);
expect(actionsSetup.registerSubActionConnectorType).toHaveBeenCalledTimes(12);
expect(actionsSetup.registerSubActionConnectorType).toHaveBeenCalledTimes(13);
expect(actionsSetup.registerSubActionConnectorType).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
@ -200,13 +200,20 @@ describe('Stack Connectors Plugin', () => {
);
expect(actionsSetup.registerSubActionConnectorType).toHaveBeenNthCalledWith(
9,
expect.objectContaining({
id: '.xsoar',
name: 'XSOAR',
})
);
expect(actionsSetup.registerSubActionConnectorType).toHaveBeenNthCalledWith(
10,
expect.objectContaining({
id: '.sentinelone',
name: 'Sentinel One',
})
);
expect(actionsSetup.registerSubActionConnectorType).toHaveBeenNthCalledWith(
10,
11,
expect.objectContaining({
id: '.crowdstrike',
name: 'CrowdStrike',

View file

@ -122,6 +122,10 @@ Array [
"cost": 1,
"taskType": "actions:.xmatters",
},
Object {
"cost": 1,
"taskType": "actions:.xsoar",
},
Object {
"cost": 10,
"taskType": "alerting:siem.indicatorRule",

View file

@ -68,6 +68,7 @@ const enabledActionTypes = [
'.tines',
'.webhook',
'.xmatters',
'.xsoar',
'.torq',
'test.sub-action-connector',
'test.sub-action-connector-without-sub-actions',

View file

@ -0,0 +1,423 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type http from 'http';
import type {
RequestHandlerContext,
KibanaRequest,
KibanaResponseFactory,
IKibanaResponse,
IRouter,
} from '@kbn/core/server';
import type { ProxyArgs } from './simulator';
import { Simulator } from './simulator';
export const XSOARPlaybook0 = {
id: '8db0105c-f674-4d83-8095-f95a9f61e77a',
version: 4,
cacheVersn: 0,
sequenceNumber: 33831652,
primaryTerm: 11,
modified: '2023-12-12T13:51:15.668021556Z',
sizeInBytes: 0,
packID: '',
packName: '',
itemVersion: '',
fromServerVersion: '',
toServerVersion: '',
propagationLabels: ['all'],
definitionId: '',
vcShouldIgnore: false,
vcShouldKeepItemLegacyProdMachine: false,
commitMessage: '',
shouldCommit: false,
name: 'playbook0',
nameRaw: 'playbook0',
prevName: 'aaa',
startTaskId: '0',
tasks: {
'0': {
id: '0',
taskId: 'e228a044-2ad5-4ab0-873a-d5bb94a5c1b4',
type: 'start',
task: {
id: 'e228a044-2ad5-4ab0-873a-d5bb94a5c1b4',
version: 1,
cacheVersn: 0,
sequenceNumber: 13431901,
primaryTerm: 8,
modified: '2023-05-23T07:16:19.930125981Z',
sizeInBytes: 0,
},
nextTasks: {
'#none#': ['1'],
},
continueOnErrorType: '',
view: {
position: {
x: 450,
y: 50,
},
},
evidenceData: {},
},
'1': {
id: '1',
taskId: 'c28b63d3-c860-4e16-82b4-6db6b58bdee3',
type: 'regular',
task: {
id: 'c28b63d3-c860-4e16-82b4-6db6b58bdee3',
version: 1,
cacheVersn: 0,
sequenceNumber: 33831651,
primaryTerm: 11,
modified: '2023-12-12T13:51:15.604271789Z',
sizeInBytes: 0,
name: 'Untitled Task 1',
description: 'commands.local.cmd.set.incident',
scriptId: 'Builtin|||setIncident',
type: 'regular',
isCommand: true,
brand: 'Builtin',
},
scriptArguments: {
severity: {
simple: '1',
},
},
continueOnErrorType: '',
view: {
position: {
x: 450,
y: 200,
},
},
evidenceData: {},
},
},
taskIds: ['e228a044-2ad5-4ab0-873a-d5bb94a5c1b4', 'c28b63d3-c860-4e16-82b4-6db6b58bdee3'],
scriptIds: [],
commands: ['setIncident'],
brands: ['Builtin'],
missingScriptsIds: ['Builtin|||setIncident'],
view: {
linkLabelsPosition: {},
paper: {
dimensions: {
height: 245,
width: 380,
x: 450,
y: 50,
},
},
},
inputs: null,
outputs: null,
quiet: true,
};
export const XSOARPlaybooksResponse = {
playbooks: [XSOARPlaybook0],
tags: [
'Phishing',
'Sandbox',
'Severity',
'Malware',
'Remediation',
'Job',
'Sinkhole',
'TIM',
'PAN-OS',
],
total: 1,
};
export const XSOARRunSuccessResponse = {
id: '178791',
version: 0,
cacheVersn: 0,
modified: '1970-01-01T00:00:00Z',
sizeInBytes: 0,
CustomFields: {
bmcassignee: [{}],
bmccustomer: [{}],
bmcrequester: [{}],
containmentsla: {
accumulatedPause: 0,
breachTriggered: false,
dueDate: '0001-01-01T00:00:00Z',
endDate: '0001-01-01T00:00:00Z',
lastPauseDate: '0001-01-01T00:00:00Z',
runStatus: 'idle',
sla: 30,
slaStatus: -1,
startDate: '0001-01-01T00:00:00Z',
totalDuration: 0,
},
crowdstrikefalconbehaviourpatterndispositiondetails: [{}, {}, {}],
datadogcloudsiem: [{}, {}, {}],
dataminrpulserelatedterms: [{}, {}, {}],
decyfirdatadetails: [{}, {}, {}],
detectionsla: {
accumulatedPause: 0,
breachTriggered: false,
dueDate: '0001-01-01T00:00:00Z',
endDate: '0001-01-01T00:00:00Z',
lastPauseDate: '0001-01-01T00:00:00Z',
runStatus: 'idle',
sla: 20,
slaStatus: -1,
startDate: '0001-01-01T00:00:00Z',
totalDuration: 0,
},
domaintoolsirisdetect: [{}, {}, {}],
endpoint: [{}],
externalid: '178791',
extrahoprevealxdetectiondevices: [{}, {}, {}],
extrahoprevealxmitretechniques: [{}, {}, {}],
filerelationships: [{}, {}, {}],
fortisiemattacktactics: [{}, {}],
fortisiemevents: [{}],
incidentduration: {
accumulatedPause: 0,
breachTriggered: false,
dueDate: '0001-01-01T00:00:00Z',
endDate: '0001-01-01T00:00:00Z',
lastPauseDate: '0001-01-01T00:00:00Z',
runStatus: 'idle',
sla: 0,
slaStatus: -1,
startDate: '0001-01-01T00:00:00Z',
totalDuration: 0,
},
incidentrdpachehuntingstringssimilarity: [{}, {}, {}],
incidentrdpcachehuntingstringsifter: [{}, {}, {}],
inventasource: [{}],
microsoftsentinelowner: [],
qintelqwatchexposures: [{}, {}, {}],
remediationsla: {
accumulatedPause: 0,
breachTriggered: false,
dueDate: '0001-01-01T00:00:00Z',
endDate: '0001-01-01T00:00:00Z',
lastPauseDate: '0001-01-01T00:00:00Z',
runStatus: 'idle',
sla: 7200,
slaStatus: -1,
startDate: '0001-01-01T00:00:00Z',
totalDuration: 0,
},
rsametasevents: [],
rsarawlogslist: [],
securitypolicymatch: [{}],
similarincidentsdbot: [{}],
spycloudcompassdevicedata: [{}, {}, {}],
suspiciousexecutions: [{}, {}, {}],
timetoassignment: {
accumulatedPause: 0,
breachTriggered: false,
dueDate: '0001-01-01T00:00:00Z',
endDate: '0001-01-01T00:00:00Z',
lastPauseDate: '0001-01-01T00:00:00Z',
runStatus: 'idle',
sla: 0,
slaStatus: -1,
startDate: '0001-01-01T00:00:00Z',
totalDuration: 0,
},
triagesla: {
accumulatedPause: 0,
breachTriggered: false,
dueDate: '0001-01-01T00:00:00Z',
endDate: '0001-01-01T00:00:00Z',
lastPauseDate: '0001-01-01T00:00:00Z',
runStatus: 'idle',
sla: 30,
slaStatus: -1,
startDate: '0001-01-01T00:00:00Z',
totalDuration: 0,
},
urlsslverification: [],
xdralertsearchresults: [{}, {}, {}],
xdrinvestigationresults: [
{},
{},
{},
{
columnheader1: '',
},
{},
{
columnheader1: '',
},
{},
{},
],
xpanseserviceclassifications: [{}, {}, {}],
xpanseservicevalidation: [
{
columnheader1: '',
},
{},
{},
],
},
account: '',
autime: 1713700028107000000,
type: 'Unclassified',
rawType: 'Unclassified',
name: 'My test incident',
rawName: 'My test incident',
status: 0,
custom_status: '',
resolution_status: '',
reason: '',
created: '2024-04-21T11:47:08.107Z',
occurred: '2024-04-21T11:47:08.107982676Z',
closed: '0001-01-01T00:00:00Z',
sla: 0,
severity: 2,
investigationId: '',
labels: [
{
value: '',
type: 'Instance',
},
{
value: 'Manual',
type: 'Brand',
},
],
attachment: null,
details: 'My test incident',
openDuration: 0,
lastOpen: '0001-01-01T00:00:00Z',
closingUserId: '',
owner: '',
activated: '0001-01-01T00:00:00Z',
closeReason: '',
rawCloseReason: '',
closeNotes: '',
playbookId: '8db0105c-f674-4d83-8095-f95a9f61e77a',
dueDate: '2024-05-01T11:47:08.107988742Z',
reminder: '0001-01-01T00:00:00Z',
runStatus: '',
notifyTime: '0001-01-01T00:00:00Z',
phase: '',
rawPhase: '',
isPlayground: false,
rawJSON: '',
parent: '',
parentXDRIncident: '',
retained: false,
category: '',
rawCategory: '',
linkedIncidents: null,
linkedCount: 0,
droppedCount: 0,
sourceInstance: '',
sourceBrand: 'Manual',
canvases: null,
lastJobRunTime: '0001-01-01T00:00:00Z',
feedBased: false,
dbotMirrorId: '',
dbotMirrorInstance: '',
dbotMirrorDirection: '',
dbotDirtyFields: null,
dbotCurrentDirtyFields: null,
dbotMirrorTags: null,
dbotMirrorLastSync: '0001-01-01T00:00:00Z',
isDebug: false,
};
export const XSOARFailedResponse = {
id: 'incCreateErr',
status: 400,
title: 'Failed creating incident',
detail: 'Failed creating incident',
error: '',
encrypted: false,
multires: null,
};
export class XSOARSimulator 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 XSOARSimulator.sendErrorResponse(response);
}
return XSOARSimulator.sendResponse(request, response);
}
private static sendResponse(request: http.IncomingMessage, response: http.ServerResponse) {
response.setHeader('Content-Type', 'application/json');
let body;
if (request.url?.match('/incident')) {
response.statusCode = 201;
body = XSOARRunSuccessResponse;
} else if (request.url?.match('/playbook/search')) {
response.statusCode = 200;
body = XSOARPlaybooksResponse;
}
response.end(JSON.stringify(body, null, 4));
}
private static sendErrorResponse(response: http.ServerResponse) {
response.statusCode = 400;
response.setHeader('Content-Type', 'application/json');
response.end(JSON.stringify(XSOARFailedResponse, null, 4));
}
}
export function initPlugin(router: IRouter, path: string) {
router.post(
{
path: `${path}/playbook/search`,
options: {
authRequired: false,
},
validate: {},
security: { authz: { enabled: false, reason: 'This route is opted out from authorization' } },
},
async function (
context: RequestHandlerContext,
req: KibanaRequest<any, any, any, any>,
res: KibanaResponseFactory
): Promise<IKibanaResponse<any>> {
return res.ok({ body: XSOARPlaybooksResponse });
}
);
router.post(
{
path: `${path}/incident`,
options: {
authRequired: false,
},
validate: {},
security: { authz: { enabled: false, reason: 'This route is opted out from authorization' } },
},
async function (
context: RequestHandlerContext,
req: KibanaRequest<any, any, any, any>,
res: KibanaResponseFactory
): Promise<IKibanaResponse<any>> {
return res.ok({ body: XSOARRunSuccessResponse });
}
);
}

View file

@ -0,0 +1,384 @@
/*
* 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 {
XSOARSimulator,
XSOARPlaybooksResponse,
} from '@kbn/actions-simulators-plugin/server/xsoar_simulation';
import { TaskErrorSource } from '@kbn/task-manager-plugin/common';
import type { FtrProviderContext } from '../../../../../common/ftr_provider_context';
const connectorTypeId = '.xsoar';
const name = 'XSOAR action';
const secrets = {
apiKey: 'apiKey',
};
// eslint-disable-next-line import/no-default-export
export default function xsoarTest({ 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 },
secrets,
})
.expect(200);
return body.id;
};
describe('XSOAR', () => {
describe('action creation', () => {
const simulator = new XSOARSimulator({
returnError: false,
proxy: {
config: configService.get('kbnTestServer.serverArgs'),
},
});
const config = { url: '' };
before(async () => {
config.url = await simulator.start();
});
after(() => {
simulator.close();
});
it('should return 200 when creating the connector', 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 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://xsoar.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://xsoar.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 XSOARSimulator({
proxy: {
config: configService.get('kbnTestServer.serverArgs'),
},
});
let xsoarActionId: string;
before(async () => {
const url = await simulator.start();
xsoarActionId = await createConnector(url);
});
after(() => {
simulator.close();
});
it('should fail when the params is empty', async () => {
const { body } = await supertest
.post(`/api/actions/connector/${xsoarActionId}/_execute`)
.set('kbn-xsrf', 'foo')
.send({
params: {},
});
expect(200);
expect(Object.keys(body).sort()).to.eql([
'connector_id',
'errorSource',
'message',
'retry',
'status',
]);
expect(body.connector_id).to.eql(xsoarActionId);
expect(body.status).to.eql('error');
});
it('should fail when the subAction is invalid', async () => {
const { body } = await supertest
.post(`/api/actions/connector/${xsoarActionId}/_execute`)
.set('kbn-xsrf', 'foo')
.send({
params: { subAction: 'invalidAction' },
})
.expect(200);
expect(body).to.eql({
connector_id: xsoarActionId,
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: ${xsoarActionId}. Connector name: XSOAR. Connector type: ${connectorTypeId}`,
});
});
it("should fail to run when the name parameter isn't included", async () => {
const { body } = await supertest
.post(`/api/actions/connector/${xsoarActionId}/_execute`)
.set('kbn-xsrf', 'foo')
.send({
params: {
subAction: 'run',
subActionParams: {
severity: 1,
createInvestigation: false,
body: '',
isRuleSeverity: false,
},
},
})
.expect(200);
expect(body).to.eql({
connector_id: xsoarActionId,
status: 'error',
retry: true,
message: 'an error occurred while running the action',
errorSource: TaskErrorSource.USER,
service_message:
'Request validation failed (Error: [name]: expected value of type [string] but got [undefined])',
});
});
});
describe('execution', () => {
describe('successful response simulator', () => {
const simulator = new XSOARSimulator({
proxy: {
config: configService.get('kbnTestServer.serverArgs'),
},
});
let url: string;
let xsoarActionId: string;
before(async () => {
url = await simulator.start();
xsoarActionId = await createConnector(url);
});
after(() => {
simulator.close();
});
it('should get playbooks', async () => {
const { body } = await supertest
.post(`/api/actions/connector/${xsoarActionId}/_execute`)
.set('kbn-xsrf', 'foo')
.send({
params: { subAction: 'getPlaybooks', subActionParams: {} },
})
.expect(200);
expect(simulator.requestUrl).to.eql(`${url}/playbook/search`);
expect(body).to.eql({
status: 'ok',
connector_id: xsoarActionId,
data: XSOARPlaybooksResponse,
});
});
it('should create incident', async () => {
const { body } = await supertest
.post(`/api/actions/connector/${xsoarActionId}/_execute`)
.set('kbn-xsrf', 'foo')
.send({
params: {
subAction: 'run',
subActionParams: {
name: 'My test incident',
severity: 2,
playbookId: '8db0105c-f674-4d83-8095-f95a9f61e77a',
createInvestigation: false,
body: null,
isRuleSeverity: false,
},
},
})
.expect(200);
expect(simulator.requestData).to.eql({
name: 'My test incident',
severity: 2,
playbookId: '8db0105c-f674-4d83-8095-f95a9f61e77a',
createInvestigation: false,
});
expect(simulator.requestUrl).to.eql(`${url}/incident`);
expect(body).to.eql({
status: 'ok',
connector_id: xsoarActionId,
data: {},
});
});
});
describe('error response simulator', () => {
const simulator = new XSOARSimulator({
returnError: true,
proxy: {
config: configService.get('kbnTestServer.serverArgs'),
},
});
let xsoarActionId: string;
before(async () => {
const url = await simulator.start();
xsoarActionId = await createConnector(url);
});
after(() => {
simulator.close();
});
it('should return a failure when attempting to get playbooks', async () => {
const { body } = await supertest
.post(`/api/actions/connector/${xsoarActionId}/_execute`)
.set('kbn-xsrf', 'foo')
.send({
params: {
subAction: 'getPlaybooks',
subActionParams: {},
},
})
.expect(200);
expect(body).to.eql({
status: 'error',
message: 'an error occurred while running the action',
retry: true,
connector_id: xsoarActionId,
errorSource: TaskErrorSource.FRAMEWORK,
service_message: 'Status code: 400. Message: API Error: Bad Request',
});
});
it('should return a failure when attempting to run', async () => {
const { body } = await supertest
.post(`/api/actions/connector/${xsoarActionId}/_execute`)
.set('kbn-xsrf', 'foo')
.send({
params: {
subAction: 'run',
subActionParams: {
name: 'My test incident',
playbookId: '8db0105c-f674-4d83-8095-f95a9f61e77a',
severity: 1,
isRuleSeverity: false,
createInvestigation: false,
},
},
})
.expect(200);
expect(simulator.requestData).to.eql({
name: 'My test incident',
playbookId: '8db0105c-f674-4d83-8095-f95a9f61e77a',
severity: 1,
createInvestigation: false,
});
expect(body).to.eql({
status: 'error',
message: 'an error occurred while running the action',
retry: true,
connector_id: xsoarActionId,
errorSource: TaskErrorSource.FRAMEWORK,
service_message: 'Status code: 400. Message: API Error: Bad Request',
});
});
});
});
});
});
}

View file

@ -44,6 +44,7 @@ export default function connectorsTests({ loadTestFile, getService }: FtrProvide
loadTestFile(require.resolve('./connector_types/thehive'));
loadTestFile(require.resolve('./connector_types/bedrock'));
loadTestFile(require.resolve('./connector_types/gemini'));
loadTestFile(require.resolve('./connector_types/xsoar'));
loadTestFile(require.resolve('./create'));
loadTestFile(require.resolve('./delete'));
loadTestFile(require.resolve('./execute'));

View file

@ -58,6 +58,7 @@ export default function createRegisteredConnectorTypeTests({ getService }: FtrPr
'.cases',
'.crowdstrike',
'.microsoft_defender_endpoint',
'.xsoar',
].sort()
);
});

View file

@ -88,6 +88,7 @@ export default function ({ getService }: FtrProviderContext) {
'actions:.torq',
'actions:.webhook',
'actions:.xmatters',
'actions:.xsoar',
'actions:connector_usage_reporting',
'actions_telemetry',
'ad_hoc_run-backfill',