diff --git a/docs/docset.yml b/docs/docset.yml index fc5aaacec943..4e0ce27fa936 100644 --- a/docs/docset.yml +++ b/docs/docset.yml @@ -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" diff --git a/docs/reference/connectors-kibana.md b/docs/reference/connectors-kibana.md index 3bd1f2784f2c..412114428212 100644 --- a/docs/reference/connectors-kibana.md +++ b/docs/reference/connectors-kibana.md @@ -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). diff --git a/docs/reference/connectors-kibana/xsoar-action-type.md b/docs/reference/connectors-kibana/xsoar-action-type.md new file mode 100644 index 000000000000..7cd890618e42 --- /dev/null +++ b/docs/reference/connectors-kibana/xsoar-action-type.md @@ -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 you’re 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 you’re 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. diff --git a/docs/reference/images/xsoar-connector.png b/docs/reference/images/xsoar-connector.png new file mode 100644 index 000000000000..d72b22b8a4b7 Binary files /dev/null and b/docs/reference/images/xsoar-connector.png differ diff --git a/docs/reference/images/xsoar-params-test.png b/docs/reference/images/xsoar-params-test.png new file mode 100644 index 000000000000..c52129ee0912 Binary files /dev/null and b/docs/reference/images/xsoar-params-test.png differ diff --git a/docs/reference/toc.yml b/docs/reference/toc.yml index 54499176121f..f58238c091c0 100644 --- a/docs/reference/toc.yml +++ b/docs/reference/toc.yml @@ -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 \ No newline at end of file + - file: osquery-manager-prebuilt-packs.md diff --git a/x-pack/platform/plugins/shared/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap b/x-pack/platform/plugins/shared/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap index 2d3a80816366..97c069717a1e 100644 --- a/x-pack/platform/plugins/shared/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap +++ b/x-pack/platform/plugins/shared/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap @@ -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", +} +`; diff --git a/x-pack/platform/plugins/shared/actions/server/integration_tests/mocks/connector_types.ts b/x-pack/platform/plugins/shared/actions/server/integration_tests/mocks/connector_types.ts index fa0bd3f460b9..0d5c1a64590e 100644 --- a/x-pack/platform/plugins/shared/actions/server/integration_tests/mocks/connector_types.ts +++ b/x-pack/platform/plugins/shared/actions/server/integration_tests/mocks/connector_types.ts @@ -30,6 +30,7 @@ export const connectorTypes: string[] = [ '.d3security', '.resilient', '.thehive', + '.xsoar', '.sentinelone', '.crowdstrike', '.inference', diff --git a/x-pack/platform/plugins/shared/stack_connectors/common/xsoar/constants.ts b/x-pack/platform/plugins/shared/stack_connectors/common/xsoar/constants.ts new file mode 100644 index 000000000000..87ca55b11f6b --- /dev/null +++ b/x-pack/platform/plugins/shared/stack_connectors/common/xsoar/constants.ts @@ -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, +} diff --git a/x-pack/platform/plugins/shared/stack_connectors/common/xsoar/schema.ts b/x-pack/platform/plugins/shared/stack_connectors/common/xsoar/schema.ts new file mode 100644 index 000000000000..154b707c0d29 --- /dev/null +++ b/x-pack/platform/plugins/shared/stack_connectors/common/xsoar/schema.ts @@ -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, + }), +]); diff --git a/x-pack/platform/plugins/shared/stack_connectors/common/xsoar/types.ts b/x-pack/platform/plugins/shared/stack_connectors/common/xsoar/types.ts new file mode 100644 index 000000000000..58f2239dd7a5 --- /dev/null +++ b/x-pack/platform/plugins/shared/stack_connectors/common/xsoar/types.ts @@ -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; +export type Secrets = TypeOf; +export type XSOARRunActionParams = TypeOf; +export type XSOARRunActionResponse = TypeOf; +export type XSOARPlaybooksActionParams = void; +export type XSOARPlaybooksObject = TypeOf; +export type XSOARPlaybooksActionResponse = TypeOf; +export type ExecutorParams = TypeOf; diff --git a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/index.ts b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/index.ts index 1d5dec2223a3..fdf3ed516567 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/index.ts +++ b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/index.ts @@ -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()); diff --git a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/xsoar/connector.test.tsx b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/xsoar/connector.test.tsx new file mode 100644 index 000000000000..c7239b6b1189 --- /dev/null +++ b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/xsoar/connector.test.tsx @@ -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( + + {}} + /> + + ); + + 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( + + {}} + /> + + ); + + 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( + + {}} + /> + + ); + + 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 }); + }); + }); +}); diff --git a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/xsoar/connector.tsx b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/xsoar/connector.tsx new file mode 100644 index 000000000000..14d943c17647 --- /dev/null +++ b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/xsoar/connector.tsx @@ -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 = ({ readOnly, isEdit }) => { + return ( + <> + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { XSOARConnectorFields as default }; diff --git a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/xsoar/constants.ts b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/xsoar/constants.ts new file mode 100644 index 000000000000..954c9d3801b1 --- /dev/null +++ b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/xsoar/constants.ts @@ -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', + } + ), + }, +]; diff --git a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/xsoar/index.ts b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/xsoar/index.ts new file mode 100644 index 000000000000..102596c8f29d --- /dev/null +++ b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/xsoar/index.ts @@ -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'; diff --git a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/xsoar/logo.tsx b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/xsoar/logo.tsx new file mode 100644 index 000000000000..f267a65fc49a --- /dev/null +++ b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/xsoar/logo.tsx @@ -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) => ( + + + + + + +); + +// eslint-disable-next-line import/no-default-export +export { Logo as default }; diff --git a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/xsoar/params.test.tsx b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/xsoar/params.test.tsx new file mode 100644 index 000000000000..838b019e69f7 --- /dev/null +++ b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/xsoar/params.test.tsx @@ -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; + 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]>(mockUseSubActionPlaybooks); + +const mockToasts = { danger: jest.fn(), warning: jest.fn() }; +jest.mock(triggersActionsPath, () => { + const original = jest.requireActual(triggersActionsPath); + return { + ...original, + useSubAction: (params: UseSubActionParams) => 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(); + + 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(); + + 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(); + + 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(); + + 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(); + 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(); + + 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(); + + 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(); + expect(editAction).toHaveBeenCalledWith('subAction', SUB_ACTION.RUN, 0); + }); + }); +}); diff --git a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/xsoar/params.tsx b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/xsoar/params.tsx new file mode 100644 index 000000000000..e044ea5c4675 --- /dev/null +++ b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/xsoar/params.tsx @@ -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; + +const createOption = (playbook: XSOARPlaybooksObject): PlaybookOption => ({ + key: playbook.id, + label: playbook.name, +}); + +const renderPlaybook = ( + { label }: PlaybookOption, + searchValue: string, + contentClassName: string +) => ( + + + {label} + + +); + +const XSOARParamsFields: React.FunctionComponent> = ({ + 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(actionConnector?.id); + const [selectedPlaybookOption, setSelectedPlaybookOption] = useState< + PlaybookOption | null | undefined + >(); + const [isRuleSeverity, setIsRuleSeverity] = useState(Boolean(incident.isRuleSeverity)); + const [playbooks, setPlaybooks] = useState(); + + 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({ + 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 ( + <> + { + 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[]} + /> + + + + {selectedPlaybookOption && ( + + { + editAction( + 'subActionParams', + { + ...incident, + createInvestigation: e.target.checked, + }, + index + ); + }} + /> + + )} + {!isTest && ( + + { + setIsRuleSeverity(e.target.checked); + editAction( + 'subActionParams', + { + ...incident, + isRuleSeverity: e.target.checked, + }, + index + ); + }} + /> + + )} + {!Boolean(isRuleSeverity) && ( + + { + editAction( + 'subActionParams', + { ...incident, severity: parseFloat(e.target.value) }, + index + ); + }} + /> + + )} + + 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 }; diff --git a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/xsoar/translations.ts b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/xsoar/translations.ts new file mode 100644 index 000000000000..d7c096c13fef --- /dev/null +++ b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/xsoar/translations.ts @@ -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.', + } +); diff --git a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/xsoar/types.ts b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/xsoar/types.ts new file mode 100644 index 000000000000..a5db454b163c --- /dev/null +++ b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/xsoar/types.ts @@ -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; diff --git a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/xsoar/xsoar.test.tsx b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/xsoar/xsoar.test.tsx new file mode 100644 index 000000000000..eee0077270ce --- /dev/null +++ b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/xsoar/xsoar.test.tsx @@ -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(); + 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], + }, + }); + }); +}); diff --git a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/xsoar/xsoar.tsx b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/xsoar/xsoar.tsx new file mode 100644 index 000000000000..a7f9d88d85de --- /dev/null +++ b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/xsoar/xsoar.tsx @@ -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> => { + 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')), + }; +} diff --git a/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/index.ts b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/index.ts index f75e485f47f0..6cba51cfd6d4 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/index.ts +++ b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/index.ts @@ -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()); diff --git a/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/xsoar/index.test.ts b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/xsoar/index.test.ts new file mode 100644 index 000000000000..2e5f21cc39ab --- /dev/null +++ b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/xsoar/index.test.ts @@ -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'); + }); +}); diff --git a/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/xsoar/index.ts b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/xsoar/index.ts new file mode 100644 index 000000000000..a6efcd704fb2 --- /dev/null +++ b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/xsoar/index.ts @@ -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; + +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') }], + }; +} diff --git a/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/xsoar/render.test.ts b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/xsoar/render.test.ts new file mode 100644 index 000000000000..9330f6f7ab00 --- /dev/null +++ b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/xsoar/render.test.ts @@ -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, + }); + }); +}); diff --git a/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/xsoar/render.ts b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/xsoar/render.ts new file mode 100644 index 000000000000..3fd15de45429 --- /dev/null +++ b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/xsoar/render.ts @@ -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 = ( + 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, + }, + }; +}; diff --git a/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/xsoar/xsoar.test.ts b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/xsoar/xsoar.test.ts new file mode 100644 index 000000000000..bff78b07acc2 --- /dev/null +++ b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/xsoar/xsoar.test.ts @@ -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` + ); + }); + }); +}); diff --git a/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/xsoar/xsoar.ts b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/xsoar/xsoar.ts new file mode 100644 index 000000000000..6972c6e36cd3 --- /dev/null +++ b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/xsoar/xsoar.ts @@ -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 { + private urls: { + playbooks: string; + incident: string; + }; + private isCloud: boolean; + private ConnectorId: string; + + constructor(params: ServiceParams) { + 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 { + const res = await this.request( + { + method: 'post', + url: `${this.urls.playbooks}`, + data: {}, + headers: this.getAuthHeaders(), + responseSchema: XSOARPlaybooksActionResponseSchema, + timeout: 15000, + }, + connectorUsageCollector + ); + + return res.data; + } +} diff --git a/x-pack/platform/plugins/shared/stack_connectors/server/plugin.test.ts b/x-pack/platform/plugins/shared/stack_connectors/server/plugin.test.ts index 9aa56f1cf019..5d850ef70c55 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/server/plugin.test.ts +++ b/x-pack/platform/plugins/shared/stack_connectors/server/plugin.test.ts @@ -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', diff --git a/x-pack/platform/plugins/shared/task_manager/server/integration_tests/__snapshots__/task_cost_check.test.ts.snap b/x-pack/platform/plugins/shared/task_manager/server/integration_tests/__snapshots__/task_cost_check.test.ts.snap index 3ee56a75faf3..1b8c968d3d1a 100644 --- a/x-pack/platform/plugins/shared/task_manager/server/integration_tests/__snapshots__/task_cost_check.test.ts.snap +++ b/x-pack/platform/plugins/shared/task_manager/server/integration_tests/__snapshots__/task_cost_check.test.ts.snap @@ -122,6 +122,10 @@ Array [ "cost": 1, "taskType": "actions:.xmatters", }, + Object { + "cost": 1, + "taskType": "actions:.xsoar", + }, Object { "cost": 10, "taskType": "alerting:siem.indicatorRule", diff --git a/x-pack/platform/test/alerting_api_integration/common/config.ts b/x-pack/platform/test/alerting_api_integration/common/config.ts index 8d60b7885500..45439923be7e 100644 --- a/x-pack/platform/test/alerting_api_integration/common/config.ts +++ b/x-pack/platform/test/alerting_api_integration/common/config.ts @@ -68,6 +68,7 @@ const enabledActionTypes = [ '.tines', '.webhook', '.xmatters', + '.xsoar', '.torq', 'test.sub-action-connector', 'test.sub-action-connector-without-sub-actions', diff --git a/x-pack/platform/test/alerting_api_integration/common/plugins/actions_simulators/server/xsoar_simulation.ts b/x-pack/platform/test/alerting_api_integration/common/plugins/actions_simulators/server/xsoar_simulation.ts new file mode 100644 index 000000000000..fa1490252f40 --- /dev/null +++ b/x-pack/platform/test/alerting_api_integration/common/plugins/actions_simulators/server/xsoar_simulation.ts @@ -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 + ) { + 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, + res: KibanaResponseFactory + ): Promise> { + 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, + res: KibanaResponseFactory + ): Promise> { + return res.ok({ body: XSOARRunSuccessResponse }); + } + ); +} diff --git a/x-pack/platform/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/xsoar.ts b/x-pack/platform/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/xsoar.ts new file mode 100644 index 000000000000..b81256d4ae38 --- /dev/null +++ b/x-pack/platform/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/xsoar.ts @@ -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', + }); + }); + }); + }); + }); + }); +} diff --git a/x-pack/platform/test/alerting_api_integration/security_and_spaces/group2/tests/actions/index.ts b/x-pack/platform/test/alerting_api_integration/security_and_spaces/group2/tests/actions/index.ts index a6f435fbee99..fa41956eee48 100644 --- a/x-pack/platform/test/alerting_api_integration/security_and_spaces/group2/tests/actions/index.ts +++ b/x-pack/platform/test/alerting_api_integration/security_and_spaces/group2/tests/actions/index.ts @@ -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')); diff --git a/x-pack/platform/test/alerting_api_integration/spaces_only/tests/actions/check_registered_connector_types.ts b/x-pack/platform/test/alerting_api_integration/spaces_only/tests/actions/check_registered_connector_types.ts index 2300c3996071..e136151df7df 100644 --- a/x-pack/platform/test/alerting_api_integration/spaces_only/tests/actions/check_registered_connector_types.ts +++ b/x-pack/platform/test/alerting_api_integration/spaces_only/tests/actions/check_registered_connector_types.ts @@ -58,6 +58,7 @@ export default function createRegisteredConnectorTypeTests({ getService }: FtrPr '.cases', '.crowdstrike', '.microsoft_defender_endpoint', + '.xsoar', ].sort() ); }); diff --git a/x-pack/platform/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts b/x-pack/platform/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts index 7eeded4099ad..4f7a6f1b7473 100644 --- a/x-pack/platform/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts +++ b/x-pack/platform/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts @@ -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',