mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 10:40:07 -04:00
XSOAR Connector (#212049)
## Summary XSOAR action connector, enabling users to send alerts generated by the rule detection engine to Palo Alto XSOAR for automation and remediation. ### **create connector**  ### **test connector** 1. **test page**  2. **select playbook**  ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [x] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [x] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [x] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Sergi Massaneda <sergi.massaneda@elastic.co> Co-authored-by: Nastasha Solomon <79124755+nastasha-solomon@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
31fe87ae06
commit
3fcdc062fa
38 changed files with 3368 additions and 3 deletions
|
@ -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"
|
||||
|
|
|
@ -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).
|
||||
|
|
80
docs/reference/connectors-kibana/xsoar-action-type.md
Normal file
80
docs/reference/connectors-kibana/xsoar-action-type.md
Normal file
|
@ -0,0 +1,80 @@
|
|||
---
|
||||
navigation_title: "{{xsoar}}"
|
||||
mapped_pages:
|
||||
- https://www.elastic.co/guide/en/kibana/current/xsoar-action-type.html
|
||||
---
|
||||
|
||||
# {{xsoar}} connector and action [xsoar-action-type]
|
||||
|
||||
|
||||
{{xsoar}} connector uses the [{{xsoar}} REST API](https://cortex-panw.stoplight.io/docs/cortex-xsoar-8/m0qlgh9inh4vk-create-or-update-an-incident) to create Cortex {{xsoar}} incidents.
|
||||
|
||||
|
||||
## Create connectors in {{kib}} [define-xsoar-ui]
|
||||
|
||||
You can create connectors in **{{stack-manage-app}} > {{connectors-ui}}** or as needed when you’re creating a rule. For example:
|
||||
|
||||
% TO DO: Use `:class: screenshot`
|
||||

|
||||
|
||||
|
||||
### 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}} 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.
|
BIN
docs/reference/images/xsoar-connector.png
Normal file
BIN
docs/reference/images/xsoar-connector.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 76 KiB |
BIN
docs/reference/images/xsoar-params-test.png
Normal file
BIN
docs/reference/images/xsoar-params-test.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 46 KiB |
|
@ -58,6 +58,7 @@ toc:
|
|||
- file: connectors-kibana/webhook-action-type.md
|
||||
- file: connectors-kibana/cases-webhook-action-type.md
|
||||
- file: connectors-kibana/xmatters-action-type.md
|
||||
- file: connectors-kibana/xsoar-action-type.md
|
||||
- file: connectors-kibana/pre-configured-connectors.md
|
||||
- file: kibana-plugins.md
|
||||
- file: commands.md
|
||||
|
@ -65,4 +66,4 @@ toc:
|
|||
- file: commands/kibana-encryption-keys.md
|
||||
- file: commands/kibana-verification-code.md
|
||||
- file: osquery-exported-fields.md
|
||||
- file: osquery-manager-prebuilt-packs.md
|
||||
- file: osquery-manager-prebuilt-packs.md
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -30,6 +30,7 @@ export const connectorTypes: string[] = [
|
|||
'.d3security',
|
||||
'.resilient',
|
||||
'.thehive',
|
||||
'.xsoar',
|
||||
'.sentinelone',
|
||||
'.crowdstrike',
|
||||
'.inference',
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -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,
|
||||
}),
|
||||
]);
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { TypeOf } from '@kbn/config-schema';
|
||||
import type {
|
||||
ConfigSchema,
|
||||
SecretsSchema,
|
||||
XSOARRunActionParamsSchema,
|
||||
XSOARRunActionResponseSchema,
|
||||
XSOARPlaybooksObjectSchema,
|
||||
XSOARPlaybooksActionResponseSchema,
|
||||
ExecutorParamsSchema,
|
||||
} from './schema';
|
||||
|
||||
export type Config = TypeOf<typeof ConfigSchema>;
|
||||
export type Secrets = TypeOf<typeof SecretsSchema>;
|
||||
export type XSOARRunActionParams = TypeOf<typeof XSOARRunActionParamsSchema>;
|
||||
export type XSOARRunActionResponse = TypeOf<typeof XSOARRunActionResponseSchema>;
|
||||
export type XSOARPlaybooksActionParams = void;
|
||||
export type XSOARPlaybooksObject = TypeOf<typeof XSOARPlaybooksObjectSchema>;
|
||||
export type XSOARPlaybooksActionResponse = TypeOf<typeof XSOARPlaybooksActionResponseSchema>;
|
||||
export type ExecutorParams = TypeOf<typeof ExecutorParamsSchema>;
|
|
@ -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());
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import XSOARConnectorFields from './connector';
|
||||
import { ConnectorFormTestProvider } from '../lib/test_utils';
|
||||
import { act, render, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
jest.mock('@kbn/triggers-actions-ui-plugin/public/common/lib/kibana');
|
||||
|
||||
describe('XSOARActionConnectorFields renders', () => {
|
||||
const actionConnector = {
|
||||
actionTypeId: '.xsoar',
|
||||
name: 'XSOAR',
|
||||
config: {
|
||||
url: 'https://test.com',
|
||||
},
|
||||
secrets: {
|
||||
apiKey: 'apiKey',
|
||||
},
|
||||
isDeprecated: false,
|
||||
};
|
||||
|
||||
it('XSOAR connector fields are rendered', () => {
|
||||
const { getByTestId } = render(
|
||||
<ConnectorFormTestProvider connector={actionConnector}>
|
||||
<XSOARConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
expect(getByTestId('config.url-input')).toBeInTheDocument();
|
||||
expect(getByTestId('secrets.apiKey-input')).toBeInTheDocument();
|
||||
expect(getByTestId('secrets.apiKeyID-input')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('Validation', () => {
|
||||
const onSubmit = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const tests: Array<[string, string]> = [
|
||||
['config.url-input', 'not-valid'],
|
||||
['secrets.apiKey-input', ''],
|
||||
];
|
||||
|
||||
it('connector validation succeeds when connector config is valid', async () => {
|
||||
const { getByTestId } = render(
|
||||
<ConnectorFormTestProvider connector={actionConnector} onSubmit={onSubmit}>
|
||||
<XSOARConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await userEvent.click(getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
|
||||
waitFor(() => {
|
||||
expect(onSubmit).toBeCalledWith({
|
||||
data: {
|
||||
actionTypeId: '.xsoar',
|
||||
name: 'XSOAR',
|
||||
config: {
|
||||
url: 'https://test.com',
|
||||
},
|
||||
secrets: {
|
||||
apiKey: 'apiKey',
|
||||
},
|
||||
isDeprecated: false,
|
||||
},
|
||||
isValid: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it.each(tests)('validates correctly %p', async (field, value) => {
|
||||
const res = render(
|
||||
<ConnectorFormTestProvider connector={actionConnector} onSubmit={onSubmit}>
|
||||
<XSOARConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await userEvent.clear(res.getByTestId(field));
|
||||
if (value !== '') {
|
||||
await userEvent.type(res.getByTestId(field), value, {
|
||||
delay: 10,
|
||||
});
|
||||
}
|
||||
|
||||
await userEvent.click(res.getByTestId('form-test-provide-submit'));
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false });
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { ActionConnectorFieldsProps } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import {
|
||||
ConfigFieldSchema,
|
||||
SimpleConnectorForm,
|
||||
SecretsFieldSchema,
|
||||
} from '@kbn/triggers-actions-ui-plugin/public';
|
||||
|
||||
import { URL_LABEL, API_KEY_LABEL, API_KEY_ID_LABEL, API_KEY_ID_HELP_TEXT } from './translations';
|
||||
|
||||
const configFormSchema: ConfigFieldSchema[] = [{ id: 'url', label: URL_LABEL, isUrlField: true }];
|
||||
|
||||
const secretsFormSchema: SecretsFieldSchema[] = [
|
||||
{ id: 'apiKey', label: API_KEY_LABEL, isPasswordField: true },
|
||||
{
|
||||
id: 'apiKeyID',
|
||||
label: API_KEY_ID_LABEL,
|
||||
isPasswordField: true,
|
||||
isRequired: false,
|
||||
helpText: API_KEY_ID_HELP_TEXT,
|
||||
},
|
||||
];
|
||||
|
||||
const XSOARConnectorFields: React.FC<ActionConnectorFieldsProps> = ({ readOnly, isEdit }) => {
|
||||
return (
|
||||
<>
|
||||
<SimpleConnectorForm
|
||||
isEdit={isEdit}
|
||||
readOnly={readOnly}
|
||||
configFormSchema={configFormSchema}
|
||||
secretsFormSchema={secretsFormSchema}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { XSOARConnectorFields as default };
|
|
@ -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',
|
||||
}
|
||||
),
|
||||
},
|
||||
];
|
|
@ -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';
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { LogoProps } from '../types';
|
||||
|
||||
const Logo = (props: LogoProps) => (
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="180"
|
||||
height="180"
|
||||
viewBox="0 0 180 180"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M0 0 C59.4 0 118.8 0 180 0 C180 59.4 180 118.8 180 180 C120.6 180 61.2 180 0 180 C0 120.6 0 61.2 0 0 Z "
|
||||
fill="#FEFEFE"
|
||||
transform="translate(0,0)"
|
||||
/>
|
||||
<path
|
||||
d="M0 0 C0 11.88 0 23.76 0 36 C5.445 36.495 5.445 36.495 11 37 C23.89312642 40.73692232 34.87162395 48.36931836 41.76171875 59.96484375 C48.07437887 72.08576258 50.43971101 84.47923625 46.5637207 97.89599609 C42.26551236 111.05035692 34.17899054 121.54046247 21.75 127.875 C14.04399747 131.47617112 9.1053914 132.24121738 0 133 C0 144.88 0 156.76 0 169 C-24.23522933 169 -43.35264863 160.25811545 -60.8125 143.5625 C-76.84026319 126.78388665 -84.872243 104.0210434 -84.51928711 80.99316406 C-83.71001346 60.47708128 -75.31748211 42.39007039 -62 27 C-61.42765625 26.32710937 -60.8553125 25.65421875 -60.265625 24.9609375 C-44.93602384 8.34122139 -22.06892026 0 0 0 Z "
|
||||
fill="#02CC67"
|
||||
transform="translate(108,5)"
|
||||
/>
|
||||
<path
|
||||
d="M0 0 C10.52864676 8.69370494 16.43235973 20.33764021 17.97265625 33.8515625 C18.50961099 47.3884742 14.58471229 59.93950836 5.55859375 70.21875 C-4.4347226 80.75174265 -16.58867412 85.97534344 -31.00390625 86.53125 C-45.1565789 86.31948931 -56.9777179 80.70659268 -66.85546875 70.796875 C-78.06743615 57.6112654 -79.64663933 43.88512024 -78.44140625 27.21875 C-77.64997359 23.15304735 -76.436914 19.8367734 -74.44140625 16.21875 C-74.01730469 15.44789062 -73.59320313 14.67703125 -73.15625 13.8828125 C-65.71409762 1.73489804 -54.89874847 -6.13554065 -41.31640625 -10.21875 C-25.93977477 -12.45084167 -12.56035967 -9.16403842 0 0 Z "
|
||||
fill="#FBFEFC"
|
||||
transform="translate(138.44140625,51.78125)"
|
||||
/>
|
||||
<path
|
||||
d="M0 0 C13.74396568 0 23.98108874 3.58656823 34.12109375 12.9609375 C44.49350337 23.82353661 48.60984892 36.24760551 48.375 50.984375 C47.48222761 64.08262134 41.08555096 75.39512537 31.6875 84.3125 C22.17264545 92.36353078 12.39116947 94.96740254 0 96 C0 64.32 0 32.64 0 0 Z "
|
||||
fill="#01CC66"
|
||||
transform="translate(108,42)"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { Logo as default };
|
|
@ -0,0 +1,364 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { ActionConnector, ActionConnectorMode } from '@kbn/triggers-actions-ui-plugin/public/types';
|
||||
import XSOARParamsFields from './params';
|
||||
import type { UseSubActionParams } from '@kbn/triggers-actions-ui-plugin/public/application/hooks/use_sub_action';
|
||||
import { SUB_ACTION } from '../../../common/xsoar/constants';
|
||||
import { ExecutorParams, XSOARRunActionParams } from '../../../common/xsoar/types';
|
||||
import * as translations from './translations';
|
||||
|
||||
interface Result {
|
||||
isLoading: boolean;
|
||||
response: Record<string, unknown>;
|
||||
error: null | Error;
|
||||
}
|
||||
|
||||
const triggersActionsPath = '@kbn/triggers-actions-ui-plugin/public';
|
||||
|
||||
const response = {
|
||||
playbooks: [
|
||||
{
|
||||
id: '8db0105c-f674-4d83-8095-f95a9f61e77a',
|
||||
version: 4,
|
||||
cacheVersn: 0,
|
||||
sequenceNumber: 33831652,
|
||||
primaryTerm: 11,
|
||||
modified: '2023-12-12T13:51:15.668021556Z',
|
||||
sizeInBytes: 0,
|
||||
packID: '',
|
||||
packName: '',
|
||||
itemVersion: '',
|
||||
fromServerVersion: '',
|
||||
toServerVersion: '',
|
||||
propagationLabels: ['all'],
|
||||
definitionId: '',
|
||||
vcShouldIgnore: false,
|
||||
vcShouldKeepItemLegacyProdMachine: false,
|
||||
commitMessage: '',
|
||||
shouldCommit: false,
|
||||
name: 'playbook0',
|
||||
nameRaw: 'playbook0',
|
||||
prevName: 'aaa',
|
||||
startTaskId: '0',
|
||||
tasks: {
|
||||
'0': {
|
||||
id: '0',
|
||||
taskId: 'e228a044-2ad5-4ab0-873a-d5bb94a5c1b4',
|
||||
type: 'start',
|
||||
task: {
|
||||
id: 'e228a044-2ad5-4ab0-873a-d5bb94a5c1b4',
|
||||
version: 1,
|
||||
cacheVersn: 0,
|
||||
sequenceNumber: 13431901,
|
||||
primaryTerm: 8,
|
||||
modified: '2023-05-23T07:16:19.930125981Z',
|
||||
sizeInBytes: 0,
|
||||
},
|
||||
nextTasks: {
|
||||
'#none#': ['1'],
|
||||
},
|
||||
continueOnErrorType: '',
|
||||
view: {
|
||||
position: {
|
||||
x: 450,
|
||||
y: 50,
|
||||
},
|
||||
},
|
||||
evidenceData: {},
|
||||
},
|
||||
'1': {
|
||||
id: '1',
|
||||
taskId: 'c28b63d3-c860-4e16-82b4-6db6b58bdee3',
|
||||
type: 'regular',
|
||||
task: {
|
||||
id: 'c28b63d3-c860-4e16-82b4-6db6b58bdee3',
|
||||
version: 1,
|
||||
cacheVersn: 0,
|
||||
sequenceNumber: 33831651,
|
||||
primaryTerm: 11,
|
||||
modified: '2023-12-12T13:51:15.604271789Z',
|
||||
sizeInBytes: 0,
|
||||
name: 'Untitled Task 1',
|
||||
description: 'commands.local.cmd.set.incident',
|
||||
scriptId: 'Builtin|||setIncident',
|
||||
type: 'regular',
|
||||
isCommand: true,
|
||||
brand: 'Builtin',
|
||||
},
|
||||
scriptArguments: {
|
||||
severity: {
|
||||
simple: '1',
|
||||
},
|
||||
},
|
||||
continueOnErrorType: '',
|
||||
view: {
|
||||
position: {
|
||||
x: 450,
|
||||
y: 200,
|
||||
},
|
||||
},
|
||||
evidenceData: {},
|
||||
},
|
||||
},
|
||||
taskIds: ['e228a044-2ad5-4ab0-873a-d5bb94a5c1b4', 'c28b63d3-c860-4e16-82b4-6db6b58bdee3'],
|
||||
scriptIds: [],
|
||||
commands: ['setIncident'],
|
||||
brands: ['Builtin'],
|
||||
missingScriptsIds: ['Builtin|||setIncident'],
|
||||
view: {
|
||||
linkLabelsPosition: {},
|
||||
paper: {
|
||||
dimensions: {
|
||||
height: 245,
|
||||
width: 380,
|
||||
x: 450,
|
||||
y: 50,
|
||||
},
|
||||
},
|
||||
},
|
||||
inputs: null,
|
||||
outputs: null,
|
||||
quiet: true,
|
||||
},
|
||||
],
|
||||
tags: [
|
||||
'Phishing',
|
||||
'Sandbox',
|
||||
'Severity',
|
||||
'Malware',
|
||||
'Remediation',
|
||||
'Job',
|
||||
'Sinkhole',
|
||||
'TIM',
|
||||
'PAN-OS',
|
||||
],
|
||||
total: 1,
|
||||
};
|
||||
|
||||
const mockUseSubActionPlaybooks = jest.fn().mockImplementation(() => ({
|
||||
isLoading: false,
|
||||
response,
|
||||
error: null,
|
||||
}));
|
||||
const mockUseSubAction = jest.fn<Result, [UseSubActionParams<unknown>]>(mockUseSubActionPlaybooks);
|
||||
|
||||
const mockToasts = { danger: jest.fn(), warning: jest.fn() };
|
||||
jest.mock(triggersActionsPath, () => {
|
||||
const original = jest.requireActual(triggersActionsPath);
|
||||
return {
|
||||
...original,
|
||||
useSubAction: (params: UseSubActionParams<unknown>) => mockUseSubAction(params),
|
||||
useKibana: () => ({
|
||||
...original.useKibana(),
|
||||
notifications: { toasts: mockToasts },
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe('XSOARParamsFields renders', () => {
|
||||
const subActionParams: XSOARRunActionParams = {
|
||||
name: 'new incident',
|
||||
playbookId: '8db0105c-f674-4d83-8095-f95a9f61e77a',
|
||||
createInvestigation: false,
|
||||
severity: 2,
|
||||
isRuleSeverity: false,
|
||||
body: '',
|
||||
};
|
||||
|
||||
const actionParams: ExecutorParams = {
|
||||
subAction: SUB_ACTION.RUN,
|
||||
subActionParams,
|
||||
};
|
||||
const connector: ActionConnector = {
|
||||
secrets: {},
|
||||
config: {},
|
||||
id: 'test',
|
||||
actionTypeId: '.test',
|
||||
name: 'Test',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
isSystemAction: false as const,
|
||||
};
|
||||
|
||||
const editAction = jest.fn();
|
||||
const defaultProps = {
|
||||
actionConnector: connector,
|
||||
actionParams,
|
||||
editAction,
|
||||
errors: { name: [] },
|
||||
index: 0,
|
||||
messageVariables: [],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('New connector', () => {
|
||||
it('should render empty run form', () => {
|
||||
const props = { ...defaultProps, actionParams: {} };
|
||||
const { getByTestId } = render(<XSOARParamsFields {...props} />);
|
||||
|
||||
expect(getByTestId('nameInput')).toBeInTheDocument();
|
||||
expect(getByTestId('xsoar-playbookSelector')).toBeInTheDocument();
|
||||
expect(getByTestId('rule-severity-toggle')).toBeInTheDocument();
|
||||
expect(getByTestId('bodyJsonEditor')).toBeInTheDocument();
|
||||
|
||||
expect(getByTestId('rule-severity-toggle')).not.toBeChecked();
|
||||
expect(getByTestId('bodyJsonEditor')).toHaveProperty('value', '');
|
||||
|
||||
expect(editAction).toHaveBeenCalledWith('subAction', SUB_ACTION.RUN, 0);
|
||||
expect(editAction).toHaveBeenCalledWith(
|
||||
'subActionParams',
|
||||
{ createInvestigation: false, severity: 0 },
|
||||
0
|
||||
);
|
||||
});
|
||||
|
||||
it('should render empty test form', () => {
|
||||
const props = { ...defaultProps, actionParams: {}, executionMode: ActionConnectorMode.Test };
|
||||
const { getByTestId } = render(<XSOARParamsFields {...props} />);
|
||||
|
||||
expect(getByTestId('nameInput')).toBeInTheDocument();
|
||||
expect(getByTestId('xsoar-playbookSelector')).toBeInTheDocument();
|
||||
expect(getByTestId('severitySelectInput')).toBeInTheDocument();
|
||||
expect(getByTestId('bodyJsonEditor')).toBeInTheDocument();
|
||||
|
||||
expect(getByTestId('severitySelectInput')).toHaveValue('0');
|
||||
expect(getByTestId('bodyJsonEditor')).toHaveProperty('value', '');
|
||||
|
||||
expect(editAction).toHaveBeenCalledWith('subAction', SUB_ACTION.RUN, 0);
|
||||
expect(editAction).toHaveBeenCalledWith(
|
||||
'subActionParams',
|
||||
{ createInvestigation: false, severity: 0 },
|
||||
0
|
||||
);
|
||||
});
|
||||
|
||||
it('should renders playbook selector and start investigation toggle after playbook selection', async () => {
|
||||
const props = { ...defaultProps, actionParams: {} };
|
||||
render(<XSOARParamsFields {...props} />);
|
||||
|
||||
expect(mockUseSubActionPlaybooks).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ subAction: 'getPlaybooks' })
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('comboBoxSearchInput')).not.toBeDisabled();
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByTestId('comboBoxSearchInput'));
|
||||
expect(screen.getByText('playbook0')).toBeInTheDocument();
|
||||
await userEvent.click(screen.getByText('playbook0'), { pointerEventsCheck: 0 });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(editAction).toHaveBeenCalledTimes(3);
|
||||
expect(editAction).toHaveBeenCalledWith('subAction', SUB_ACTION.RUN, 0);
|
||||
expect(editAction).toHaveBeenCalledWith(
|
||||
'subActionParams',
|
||||
{ createInvestigation: false, severity: 0 },
|
||||
0
|
||||
);
|
||||
expect(editAction).toHaveBeenCalledWith(
|
||||
'subActionParams',
|
||||
{
|
||||
createInvestigation: false,
|
||||
playbookId: '8db0105c-f674-4d83-8095-f95a9f61e77a',
|
||||
severity: 0,
|
||||
},
|
||||
0
|
||||
);
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('createInvestigation-toggle')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('createInvestigation-toggle')).not.toBeChecked();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edit connector', () => {
|
||||
it('all Params fields is rendered', () => {
|
||||
const { getByTestId } = render(<XSOARParamsFields {...defaultProps} />);
|
||||
|
||||
expect(mockUseSubActionPlaybooks).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ subAction: 'getPlaybooks' })
|
||||
);
|
||||
|
||||
expect(getByTestId('nameInput')).toBeInTheDocument();
|
||||
expect(getByTestId('xsoar-playbookSelector')).toBeInTheDocument();
|
||||
expect(getByTestId('createInvestigation-toggle')).toBeInTheDocument();
|
||||
expect(getByTestId('rule-severity-toggle')).toBeInTheDocument();
|
||||
expect(getByTestId('severitySelectInput')).toBeInTheDocument();
|
||||
expect(getByTestId('bodyJsonEditor')).toBeInTheDocument();
|
||||
|
||||
expect(getByTestId('nameInput')).toHaveValue('new incident');
|
||||
expect(getByTestId('comboBoxSearchInput')).toHaveProperty('value', 'playbook0');
|
||||
expect(getByTestId('createInvestigation-toggle')).not.toBeChecked();
|
||||
expect(getByTestId('rule-severity-toggle')).not.toBeChecked();
|
||||
expect(getByTestId('severitySelectInput')).toHaveValue('2');
|
||||
expect(getByTestId('bodyJsonEditor')).toHaveProperty('value', '');
|
||||
});
|
||||
|
||||
it('hides the severity select input when rule severity is enabled', () => {
|
||||
const { getByTestId } = render(<XSOARParamsFields {...defaultProps} />);
|
||||
const ruleSeverityToggleEl = getByTestId('rule-severity-toggle');
|
||||
|
||||
fireEvent.click(ruleSeverityToggleEl);
|
||||
expect(getByTestId('rule-severity-toggle')).toBeEnabled();
|
||||
expect(editAction).toHaveBeenCalledWith(
|
||||
'subActionParams',
|
||||
{ ...subActionParams, severity: 2, isRuleSeverity: true },
|
||||
0
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('severitySelectInput')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show warning if playbook not found', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
actionParams: { subActionParams: { ...subActionParams, playbookId: 'wrong-playbookId' } },
|
||||
};
|
||||
render(<XSOARParamsFields {...props} />);
|
||||
|
||||
expect(mockToasts.warning).toHaveBeenCalledWith({
|
||||
title: translations.PLAYBOOK_NOT_FOUND_WARNING,
|
||||
});
|
||||
});
|
||||
|
||||
it('should show error when playbooks subAction has error', () => {
|
||||
const errorMessage = 'something broke';
|
||||
mockUseSubActionPlaybooks.mockReturnValueOnce({
|
||||
isLoading: false,
|
||||
response,
|
||||
error: new Error(errorMessage),
|
||||
});
|
||||
|
||||
render(<XSOARParamsFields {...defaultProps} />);
|
||||
|
||||
expect(mockToasts.danger).toHaveBeenCalledWith({
|
||||
title: translations.PLAYBOOKS_ERROR,
|
||||
body: errorMessage,
|
||||
});
|
||||
});
|
||||
|
||||
it('handles the case when subAction is undefined', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
actionParams: {
|
||||
...actionParams,
|
||||
subAction: undefined,
|
||||
},
|
||||
};
|
||||
render(<XSOARParamsFields {...props} />);
|
||||
expect(editAction).toHaveBeenCalledWith('subAction', SUB_ACTION.RUN, 0);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,298 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import {
|
||||
useSubAction,
|
||||
useKibana,
|
||||
ActionParamsProps,
|
||||
JsonEditorWithMessageVariables,
|
||||
TextFieldWithMessageVariables,
|
||||
ActionConnectorMode,
|
||||
} from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import {
|
||||
EuiFormRow,
|
||||
EuiComboBoxOptionOption,
|
||||
EuiComboBox,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiHighlight,
|
||||
EuiSwitch,
|
||||
EuiSelect,
|
||||
} from '@elastic/eui';
|
||||
import { SUB_ACTION, XSOARSeverity } from '../../../common/xsoar/constants';
|
||||
import {
|
||||
ExecutorParams,
|
||||
XSOARRunActionParams,
|
||||
XSOARPlaybooksActionResponse,
|
||||
XSOARPlaybooksActionParams,
|
||||
XSOARPlaybooksObject,
|
||||
} from '../../../common/xsoar/types';
|
||||
import * as translations from './translations';
|
||||
import { severityOptions } from './constants';
|
||||
|
||||
type PlaybookOption = EuiComboBoxOptionOption<XSOARPlaybooksObject>;
|
||||
|
||||
const createOption = (playbook: XSOARPlaybooksObject): PlaybookOption => ({
|
||||
key: playbook.id,
|
||||
label: playbook.name,
|
||||
});
|
||||
|
||||
const renderPlaybook = (
|
||||
{ label }: PlaybookOption,
|
||||
searchValue: string,
|
||||
contentClassName: string
|
||||
) => (
|
||||
<EuiFlexGroup className={contentClassName} direction="row" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiHighlight search={searchValue}>{label}</EuiHighlight>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
||||
const XSOARParamsFields: React.FunctionComponent<ActionParamsProps<ExecutorParams>> = ({
|
||||
actionConnector,
|
||||
actionParams,
|
||||
editAction,
|
||||
index,
|
||||
errors,
|
||||
messageVariables,
|
||||
executionMode,
|
||||
}) => {
|
||||
const { toasts } = useKibana().notifications;
|
||||
const isTest = executionMode === ActionConnectorMode.Test;
|
||||
const incident = useMemo(
|
||||
() =>
|
||||
(actionParams.subActionParams as XSOARRunActionParams) ??
|
||||
({
|
||||
severity: XSOARSeverity.UNKNOWN,
|
||||
createInvestigation: false,
|
||||
} as unknown as XSOARRunActionParams),
|
||||
|
||||
[actionParams.subActionParams]
|
||||
);
|
||||
|
||||
const [connectorId, setConnectorId] = useState<string | undefined>(actionConnector?.id);
|
||||
const [selectedPlaybookOption, setSelectedPlaybookOption] = useState<
|
||||
PlaybookOption | null | undefined
|
||||
>();
|
||||
const [isRuleSeverity, setIsRuleSeverity] = useState<boolean>(Boolean(incident.isRuleSeverity));
|
||||
const [playbooks, setPlaybooks] = useState<XSOARPlaybooksObject[]>();
|
||||
|
||||
useEffect(() => {
|
||||
if (actionConnector != null && connectorId !== actionConnector.id) {
|
||||
setConnectorId(actionConnector?.id);
|
||||
setSelectedPlaybookOption(null);
|
||||
setIsRuleSeverity(isTest ? false : true);
|
||||
editAction(
|
||||
'subActionParams',
|
||||
{
|
||||
severity: XSOARSeverity.UNKNOWN,
|
||||
createInvestigation: false,
|
||||
},
|
||||
index
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [actionConnector]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!actionParams.subAction) {
|
||||
editAction('subAction', SUB_ACTION.RUN, index);
|
||||
}
|
||||
if (!actionParams.subActionParams) {
|
||||
editAction('subActionParams', incident, index);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [actionParams]);
|
||||
|
||||
const {
|
||||
response: { playbooks: fetchedPlaybooks } = {},
|
||||
isLoading: isLoadingPlaybooks,
|
||||
error: playbooksError,
|
||||
} = useSubAction<XSOARPlaybooksActionParams, XSOARPlaybooksActionResponse>({
|
||||
connectorId,
|
||||
subAction: 'getPlaybooks',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (playbooksError) {
|
||||
toasts.danger({ title: translations.PLAYBOOKS_ERROR, body: playbooksError.message });
|
||||
setPlaybooks([]);
|
||||
} else {
|
||||
setPlaybooks(fetchedPlaybooks);
|
||||
}
|
||||
}, [toasts, playbooksError, fetchedPlaybooks]);
|
||||
|
||||
const playbooksOptions = useMemo(() => playbooks?.map(createOption) ?? [], [playbooks]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedPlaybookOption === undefined && incident.playbookId && playbooks !== undefined) {
|
||||
const selectedPlaybook = playbooks.find(({ id }) => id === incident.playbookId);
|
||||
if (selectedPlaybook) {
|
||||
setSelectedPlaybookOption(createOption(selectedPlaybook));
|
||||
} else {
|
||||
toasts.warning({ title: translations.PLAYBOOK_NOT_FOUND_WARNING });
|
||||
editAction(
|
||||
'subActionParams',
|
||||
{ ...incident, playbookId: undefined, createInvestigation: false },
|
||||
index
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
selectedPlaybookOption !== undefined &&
|
||||
selectedPlaybookOption?.key !== incident.playbookId
|
||||
) {
|
||||
editAction(
|
||||
'subActionParams',
|
||||
{
|
||||
...incident,
|
||||
playbookId: selectedPlaybookOption?.key,
|
||||
createInvestigation:
|
||||
selectedPlaybookOption === null ? false : incident.createInvestigation,
|
||||
},
|
||||
index
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedPlaybookOption, incident.playbookId, playbooks, toasts, editAction, index]);
|
||||
|
||||
const selectedPlaybookOptions = useMemo(
|
||||
() => (selectedPlaybookOption ? [selectedPlaybookOption] : []),
|
||||
[selectedPlaybookOption]
|
||||
);
|
||||
|
||||
const onChangePlaybook = useCallback(([selected]: PlaybookOption[]) => {
|
||||
setSelectedPlaybookOption(selected ?? null);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TextFieldWithMessageVariables
|
||||
index={index}
|
||||
editAction={(key, value) => {
|
||||
editAction('subActionParams', { ...incident, [key]: value }, index);
|
||||
}}
|
||||
messageVariables={messageVariables}
|
||||
paramsProperty={'name'}
|
||||
inputTargetValue={incident.name}
|
||||
wrapField={true}
|
||||
formRowProps={{
|
||||
label: translations.NAME_LABEL,
|
||||
fullWidth: true,
|
||||
helpText: '',
|
||||
isInvalid:
|
||||
errors.name !== undefined &&
|
||||
Number(errors.name.length) > 0 &&
|
||||
incident.name !== undefined,
|
||||
error: errors.name as string,
|
||||
}}
|
||||
errors={errors.name as string[]}
|
||||
/>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
error={errors.playbook as string[]}
|
||||
isInvalid={!!errors.playbook?.length && selectedPlaybookOption !== undefined}
|
||||
label={translations.PLAYBOOK_LABEL}
|
||||
helpText={translations.PLAYBOOK_HELP}
|
||||
>
|
||||
<EuiComboBox
|
||||
aria-label={translations.PLAYBOOK_ARIA_LABEL}
|
||||
placeholder={translations.PLAYBOOK_PLACEHOLDER}
|
||||
singleSelection={{ asPlainText: true }}
|
||||
options={playbooksOptions}
|
||||
selectedOptions={selectedPlaybookOptions}
|
||||
onChange={onChangePlaybook}
|
||||
isDisabled={isLoadingPlaybooks}
|
||||
isLoading={isLoadingPlaybooks}
|
||||
renderOption={renderPlaybook}
|
||||
fullWidth
|
||||
data-test-subj="xsoar-playbookSelector"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
{selectedPlaybookOption && (
|
||||
<EuiFormRow fullWidth>
|
||||
<EuiSwitch
|
||||
label={translations.START_INVESTIGATION_LABEL}
|
||||
checked={incident.createInvestigation}
|
||||
data-test-subj="createInvestigation-toggle"
|
||||
onChange={(e) => {
|
||||
editAction(
|
||||
'subActionParams',
|
||||
{
|
||||
...incident,
|
||||
createInvestigation: e.target.checked,
|
||||
},
|
||||
index
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
{!isTest && (
|
||||
<EuiFormRow fullWidth>
|
||||
<EuiSwitch
|
||||
label={translations.IS_RULE_SEVERITY_LABEL}
|
||||
checked={Boolean(isRuleSeverity)}
|
||||
data-test-subj="rule-severity-toggle"
|
||||
onChange={(e) => {
|
||||
setIsRuleSeverity(e.target.checked);
|
||||
editAction(
|
||||
'subActionParams',
|
||||
{
|
||||
...incident,
|
||||
isRuleSeverity: e.target.checked,
|
||||
},
|
||||
index
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
{!Boolean(isRuleSeverity) && (
|
||||
<EuiFormRow fullWidth label={translations.SEVERITY_LABEL}>
|
||||
<EuiSelect
|
||||
fullWidth
|
||||
data-test-subj="severitySelectInput"
|
||||
disabled={Boolean(isRuleSeverity)}
|
||||
value={incident.severity ?? severityOptions[0].value}
|
||||
options={severityOptions}
|
||||
onChange={(e) => {
|
||||
editAction(
|
||||
'subActionParams',
|
||||
{ ...incident, severity: parseFloat(e.target.value) },
|
||||
index
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
<JsonEditorWithMessageVariables
|
||||
key={connectorId}
|
||||
messageVariables={messageVariables}
|
||||
paramsProperty={'body'}
|
||||
inputTargetValue={incident.body}
|
||||
label={translations.BODY_LABEL}
|
||||
ariaLabel={translations.BODY_DESCRIPTION}
|
||||
onDocumentsChange={(json: string) =>
|
||||
editAction('subActionParams', { ...incident, body: json }, index)
|
||||
}
|
||||
dataTestSubj="xsoar-body"
|
||||
onBlur={() => {
|
||||
if (!incident.body) {
|
||||
editAction('subActionParams', { ...incident, body: null }, index);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { XSOARParamsFields as default };
|
|
@ -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.',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { ActionTypeModel as ConnectorTypeModel } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import type { Config, Secrets, ExecutorParams } from '../../../common/xsoar/types';
|
||||
|
||||
export type XSOARConnector = ConnectorTypeModel<Config, Secrets, ExecutorParams>;
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { TypeRegistry } from '@kbn/triggers-actions-ui-plugin/public/application/type_registry';
|
||||
import { registerConnectorTypes } from '..';
|
||||
import { ActionTypeModel as ConnectorTypeModel } from '@kbn/triggers-actions-ui-plugin/public/types';
|
||||
import { experimentalFeaturesMock, registrationServicesMock } from '../../mocks';
|
||||
import { SUB_ACTION } from '../../../common/xsoar/constants';
|
||||
import { ExperimentalFeaturesService } from '../../common/experimental_features_service';
|
||||
import * as translations from './translations';
|
||||
|
||||
const CONNECTOR_TYPE_ID = '.xsoar';
|
||||
let connectorTypeModel: ConnectorTypeModel;
|
||||
beforeAll(() => {
|
||||
const connectorTypeRegistry = new TypeRegistry<ConnectorTypeModel>();
|
||||
ExperimentalFeaturesService.init({ experimentalFeatures: experimentalFeaturesMock });
|
||||
registerConnectorTypes({ connectorTypeRegistry, services: registrationServicesMock });
|
||||
const getResult = connectorTypeRegistry.get(CONNECTOR_TYPE_ID);
|
||||
if (getResult !== null) {
|
||||
connectorTypeModel = getResult;
|
||||
}
|
||||
});
|
||||
|
||||
describe('actionTypeRegistry.get() works', () => {
|
||||
test('action type static data is as expected', () => {
|
||||
expect(connectorTypeModel.id).toEqual(CONNECTOR_TYPE_ID);
|
||||
});
|
||||
});
|
||||
|
||||
describe('XSOAR RUN action params validation', () => {
|
||||
test('RUN action params validation succeeds when action params is valid', async () => {
|
||||
const actionParams = {
|
||||
subAction: SUB_ACTION.RUN,
|
||||
subActionParams: {
|
||||
name: 'new incident',
|
||||
playbookId: 'playbook0',
|
||||
createInvestigation: false,
|
||||
severity: 1,
|
||||
body: '',
|
||||
},
|
||||
};
|
||||
|
||||
expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: {
|
||||
name: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('RUN action params validation fails when required fields is not valid', async () => {
|
||||
const actionParams = {
|
||||
subAction: SUB_ACTION.RUN,
|
||||
subActionParams: {
|
||||
name: '',
|
||||
playbookId: 'playbook0',
|
||||
createInvestigation: false,
|
||||
severity: 1,
|
||||
body: '',
|
||||
},
|
||||
};
|
||||
|
||||
expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: {
|
||||
name: [translations.NAME_REQUIRED],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { lazy } from 'react';
|
||||
import { GenericValidationResult } from '@kbn/triggers-actions-ui-plugin/public/types';
|
||||
import { XSOARConnector } from './types';
|
||||
import { XSOAR_CONNECTOR_ID, SUB_ACTION, XSOAR_TITLE } from '../../../common/xsoar/constants';
|
||||
import { ExecutorParams } from '../../../common/xsoar/types';
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface ValidationErrors {
|
||||
name: string[];
|
||||
}
|
||||
|
||||
export function getConnectorType(): XSOARConnector {
|
||||
return {
|
||||
id: XSOAR_CONNECTOR_ID,
|
||||
hideInUi: true,
|
||||
iconClass: lazy(() => import('./logo')),
|
||||
selectMessage: i18n.SELECT_MESSAGE,
|
||||
actionTypeTitle: XSOAR_TITLE,
|
||||
validateParams: async (
|
||||
actionParams: ExecutorParams
|
||||
): Promise<GenericValidationResult<ValidationErrors>> => {
|
||||
const translations = await import('./translations');
|
||||
const errors: ValidationErrors = {
|
||||
name: [],
|
||||
};
|
||||
const { subAction, subActionParams } = actionParams;
|
||||
|
||||
if (subAction === SUB_ACTION.RUN) {
|
||||
if (!subActionParams?.name?.length) {
|
||||
errors.name.push(translations.NAME_REQUIRED);
|
||||
}
|
||||
}
|
||||
return { errors };
|
||||
},
|
||||
actionConnectorFields: lazy(() => import('./connector')),
|
||||
actionParamsFields: lazy(() => import('./params')),
|
||||
};
|
||||
}
|
|
@ -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());
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { SubActionConnectorType } from '@kbn/actions-plugin/server/sub_action_framework/types';
|
||||
import { ValidatorType } from '@kbn/actions-plugin/server/sub_action_framework/types';
|
||||
import { SecurityConnectorFeatureId } from '@kbn/actions-plugin/common';
|
||||
import { urlAllowListValidator } from '@kbn/actions-plugin/server';
|
||||
import { XSOAR_CONNECTOR_ID, XSOAR_TITLE } from '../../../common/xsoar/constants';
|
||||
import { ConfigSchema, SecretsSchema } from '../../../common/xsoar/schema';
|
||||
import type { Config, Secrets } from '../../../common/xsoar/types';
|
||||
import { XSOARConnector } from './xsoar';
|
||||
import { renderParameterTemplates } from './render';
|
||||
|
||||
export type XSOARConnectorType = SubActionConnectorType<Config, Secrets>;
|
||||
|
||||
export function getConnectorType(): XSOARConnectorType {
|
||||
return {
|
||||
id: XSOAR_CONNECTOR_ID,
|
||||
minimumLicenseRequired: 'platinum',
|
||||
name: XSOAR_TITLE,
|
||||
getService: (params) => new XSOARConnector(params),
|
||||
supportedFeatureIds: [SecurityConnectorFeatureId],
|
||||
schema: {
|
||||
config: ConfigSchema,
|
||||
secrets: SecretsSchema,
|
||||
},
|
||||
renderParameterTemplates,
|
||||
validators: [{ type: ValidatorType.CONFIG, validator: urlAllowListValidator('url') }],
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { ExecutorParams } from '@kbn/actions-plugin/server/sub_action_framework/types';
|
||||
import {
|
||||
renderMustacheObject,
|
||||
renderMustacheString,
|
||||
} from '@kbn/actions-plugin/server/lib/mustache_renderer';
|
||||
import type { RenderParameterTemplates } from '@kbn/actions-plugin/server/types';
|
||||
|
||||
function mapSeverity(severity: string): number {
|
||||
switch (severity) {
|
||||
case 'low':
|
||||
return 1;
|
||||
case 'medium':
|
||||
return 2;
|
||||
case 'high':
|
||||
return 3;
|
||||
case 'critical':
|
||||
return 4;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
export const renderParameterTemplates: RenderParameterTemplates<ExecutorParams> = (
|
||||
logger,
|
||||
params,
|
||||
variables
|
||||
) => {
|
||||
return {
|
||||
...params,
|
||||
subActionParams: {
|
||||
...renderMustacheObject(logger, params.subActionParams, variables),
|
||||
severity:
|
||||
params.subActionParams.isRuleSeverity === true
|
||||
? mapSeverity(
|
||||
renderMustacheString(logger, '{{context.rule.severity}}', variables, 'json')
|
||||
)
|
||||
: params.subActionParams.severity,
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { ServiceParams } from '@kbn/actions-plugin/server';
|
||||
import { SubActionConnector } from '@kbn/actions-plugin/server';
|
||||
import type { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
|
||||
import type { AxiosError } from 'axios';
|
||||
|
||||
import type {
|
||||
Config,
|
||||
Secrets,
|
||||
XSOARRunActionParams,
|
||||
XSOARPlaybooksActionResponse,
|
||||
} from '../../../common/xsoar/types';
|
||||
import {
|
||||
XSOARRunActionResponseSchema,
|
||||
XSOARPlaybooksActionResponseSchema,
|
||||
XSOARPlaybooksActionParamsSchema,
|
||||
XSOARRunActionParamsSchema,
|
||||
} from '../../../common/xsoar/schema';
|
||||
import { SUB_ACTION } from '../../../common/xsoar/constants';
|
||||
|
||||
export const CLOUD_API_PATH = '/xsoar/public/v1';
|
||||
export const INCIDENT_PATH = '/incident';
|
||||
export const PLAYBOOKS_PATH = '/playbook/search';
|
||||
|
||||
export class XSOARConnector extends SubActionConnector<Config, Secrets> {
|
||||
private urls: {
|
||||
playbooks: string;
|
||||
incident: string;
|
||||
};
|
||||
private isCloud: boolean;
|
||||
private ConnectorId: string;
|
||||
|
||||
constructor(params: ServiceParams<Config, Secrets>) {
|
||||
super(params);
|
||||
|
||||
this.isCloud = this.secrets.apiKeyID !== null && this.secrets.apiKeyID !== '';
|
||||
this.urls = {
|
||||
playbooks: this.isCloud
|
||||
? `${this.config.url}${CLOUD_API_PATH}${PLAYBOOKS_PATH}`
|
||||
: `${this.config.url}${PLAYBOOKS_PATH}`,
|
||||
incident: this.isCloud
|
||||
? `${this.config.url}${CLOUD_API_PATH}${INCIDENT_PATH}`
|
||||
: `${this.config.url}${INCIDENT_PATH}`,
|
||||
};
|
||||
this.ConnectorId = params.connector.id;
|
||||
this.registerSubActions();
|
||||
}
|
||||
|
||||
private registerSubActions() {
|
||||
this.registerSubAction({
|
||||
name: SUB_ACTION.PLAYBOOKS,
|
||||
method: 'getPlaybooks',
|
||||
schema: XSOARPlaybooksActionParamsSchema,
|
||||
});
|
||||
|
||||
this.registerSubAction({
|
||||
name: SUB_ACTION.RUN,
|
||||
method: 'run',
|
||||
schema: XSOARRunActionParamsSchema,
|
||||
});
|
||||
}
|
||||
|
||||
private getAuthHeaders() {
|
||||
return this.isCloud
|
||||
? { Authorization: this.secrets.apiKey, 'x-xdr-auth-id': this.secrets.apiKeyID }
|
||||
: { Authorization: this.secrets.apiKey };
|
||||
}
|
||||
|
||||
protected getResponseErrorMessage(error: AxiosError): string {
|
||||
if (error.response?.statusText) {
|
||||
return `API Error: ${error.response?.statusText}`;
|
||||
}
|
||||
return error.toString();
|
||||
}
|
||||
|
||||
private formatIncidentBody(incident: XSOARRunActionParams) {
|
||||
try {
|
||||
const { body, isRuleSeverity, ...incidentWithoutBody } = incident;
|
||||
const bodyJson = JSON.parse(body || '{}');
|
||||
const mergedIncident = { ...bodyJson, ...incidentWithoutBody };
|
||||
|
||||
return mergedIncident;
|
||||
} catch (err) {
|
||||
const errMessage = i18n.translate('xpack.stackConnectors.xsoar.BodyParsingErrorMessage', {
|
||||
defaultMessage: 'error triggering XSOAR workflow, parsing body',
|
||||
});
|
||||
|
||||
this.logger.error(`error on ${this.ConnectorId} XSOAR event: ${errMessage}: ${err.message}`);
|
||||
|
||||
throw new Error(
|
||||
i18n.translate('xpack.stackConnectors.xsoar.incidentBodyParsingError', {
|
||||
defaultMessage: 'Error parsing Body: {err}',
|
||||
values: {
|
||||
err: err.toString(),
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async run(
|
||||
incident: XSOARRunActionParams,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
) {
|
||||
const mergedIncident = this.formatIncidentBody(incident);
|
||||
await this.request(
|
||||
{
|
||||
method: 'post',
|
||||
url: `${this.urls.incident}`,
|
||||
data: mergedIncident,
|
||||
headers: this.getAuthHeaders(),
|
||||
responseSchema: XSOARRunActionResponseSchema,
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
}
|
||||
|
||||
public async getPlaybooks(
|
||||
params: unknown,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<XSOARPlaybooksActionResponse> {
|
||||
const res = await this.request(
|
||||
{
|
||||
method: 'post',
|
||||
url: `${this.urls.playbooks}`,
|
||||
data: {},
|
||||
headers: this.getAuthHeaders(),
|
||||
responseSchema: XSOARPlaybooksActionResponseSchema,
|
||||
timeout: 15000,
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
return res.data;
|
||||
}
|
||||
}
|
|
@ -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',
|
||||
|
|
|
@ -122,6 +122,10 @@ Array [
|
|||
"cost": 1,
|
||||
"taskType": "actions:.xmatters",
|
||||
},
|
||||
Object {
|
||||
"cost": 1,
|
||||
"taskType": "actions:.xsoar",
|
||||
},
|
||||
Object {
|
||||
"cost": 10,
|
||||
"taskType": "alerting:siem.indicatorRule",
|
||||
|
|
|
@ -68,6 +68,7 @@ const enabledActionTypes = [
|
|||
'.tines',
|
||||
'.webhook',
|
||||
'.xmatters',
|
||||
'.xsoar',
|
||||
'.torq',
|
||||
'test.sub-action-connector',
|
||||
'test.sub-action-connector-without-sub-actions',
|
||||
|
|
|
@ -0,0 +1,423 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type http from 'http';
|
||||
import type {
|
||||
RequestHandlerContext,
|
||||
KibanaRequest,
|
||||
KibanaResponseFactory,
|
||||
IKibanaResponse,
|
||||
IRouter,
|
||||
} from '@kbn/core/server';
|
||||
import type { ProxyArgs } from './simulator';
|
||||
import { Simulator } from './simulator';
|
||||
|
||||
export const XSOARPlaybook0 = {
|
||||
id: '8db0105c-f674-4d83-8095-f95a9f61e77a',
|
||||
version: 4,
|
||||
cacheVersn: 0,
|
||||
sequenceNumber: 33831652,
|
||||
primaryTerm: 11,
|
||||
modified: '2023-12-12T13:51:15.668021556Z',
|
||||
sizeInBytes: 0,
|
||||
packID: '',
|
||||
packName: '',
|
||||
itemVersion: '',
|
||||
fromServerVersion: '',
|
||||
toServerVersion: '',
|
||||
propagationLabels: ['all'],
|
||||
definitionId: '',
|
||||
vcShouldIgnore: false,
|
||||
vcShouldKeepItemLegacyProdMachine: false,
|
||||
commitMessage: '',
|
||||
shouldCommit: false,
|
||||
name: 'playbook0',
|
||||
nameRaw: 'playbook0',
|
||||
prevName: 'aaa',
|
||||
startTaskId: '0',
|
||||
tasks: {
|
||||
'0': {
|
||||
id: '0',
|
||||
taskId: 'e228a044-2ad5-4ab0-873a-d5bb94a5c1b4',
|
||||
type: 'start',
|
||||
task: {
|
||||
id: 'e228a044-2ad5-4ab0-873a-d5bb94a5c1b4',
|
||||
version: 1,
|
||||
cacheVersn: 0,
|
||||
sequenceNumber: 13431901,
|
||||
primaryTerm: 8,
|
||||
modified: '2023-05-23T07:16:19.930125981Z',
|
||||
sizeInBytes: 0,
|
||||
},
|
||||
nextTasks: {
|
||||
'#none#': ['1'],
|
||||
},
|
||||
continueOnErrorType: '',
|
||||
view: {
|
||||
position: {
|
||||
x: 450,
|
||||
y: 50,
|
||||
},
|
||||
},
|
||||
evidenceData: {},
|
||||
},
|
||||
'1': {
|
||||
id: '1',
|
||||
taskId: 'c28b63d3-c860-4e16-82b4-6db6b58bdee3',
|
||||
type: 'regular',
|
||||
task: {
|
||||
id: 'c28b63d3-c860-4e16-82b4-6db6b58bdee3',
|
||||
version: 1,
|
||||
cacheVersn: 0,
|
||||
sequenceNumber: 33831651,
|
||||
primaryTerm: 11,
|
||||
modified: '2023-12-12T13:51:15.604271789Z',
|
||||
sizeInBytes: 0,
|
||||
name: 'Untitled Task 1',
|
||||
description: 'commands.local.cmd.set.incident',
|
||||
scriptId: 'Builtin|||setIncident',
|
||||
type: 'regular',
|
||||
isCommand: true,
|
||||
brand: 'Builtin',
|
||||
},
|
||||
scriptArguments: {
|
||||
severity: {
|
||||
simple: '1',
|
||||
},
|
||||
},
|
||||
continueOnErrorType: '',
|
||||
view: {
|
||||
position: {
|
||||
x: 450,
|
||||
y: 200,
|
||||
},
|
||||
},
|
||||
evidenceData: {},
|
||||
},
|
||||
},
|
||||
taskIds: ['e228a044-2ad5-4ab0-873a-d5bb94a5c1b4', 'c28b63d3-c860-4e16-82b4-6db6b58bdee3'],
|
||||
scriptIds: [],
|
||||
commands: ['setIncident'],
|
||||
brands: ['Builtin'],
|
||||
missingScriptsIds: ['Builtin|||setIncident'],
|
||||
view: {
|
||||
linkLabelsPosition: {},
|
||||
paper: {
|
||||
dimensions: {
|
||||
height: 245,
|
||||
width: 380,
|
||||
x: 450,
|
||||
y: 50,
|
||||
},
|
||||
},
|
||||
},
|
||||
inputs: null,
|
||||
outputs: null,
|
||||
quiet: true,
|
||||
};
|
||||
|
||||
export const XSOARPlaybooksResponse = {
|
||||
playbooks: [XSOARPlaybook0],
|
||||
tags: [
|
||||
'Phishing',
|
||||
'Sandbox',
|
||||
'Severity',
|
||||
'Malware',
|
||||
'Remediation',
|
||||
'Job',
|
||||
'Sinkhole',
|
||||
'TIM',
|
||||
'PAN-OS',
|
||||
],
|
||||
total: 1,
|
||||
};
|
||||
|
||||
export const XSOARRunSuccessResponse = {
|
||||
id: '178791',
|
||||
version: 0,
|
||||
cacheVersn: 0,
|
||||
modified: '1970-01-01T00:00:00Z',
|
||||
sizeInBytes: 0,
|
||||
CustomFields: {
|
||||
bmcassignee: [{}],
|
||||
bmccustomer: [{}],
|
||||
bmcrequester: [{}],
|
||||
containmentsla: {
|
||||
accumulatedPause: 0,
|
||||
breachTriggered: false,
|
||||
dueDate: '0001-01-01T00:00:00Z',
|
||||
endDate: '0001-01-01T00:00:00Z',
|
||||
lastPauseDate: '0001-01-01T00:00:00Z',
|
||||
runStatus: 'idle',
|
||||
sla: 30,
|
||||
slaStatus: -1,
|
||||
startDate: '0001-01-01T00:00:00Z',
|
||||
totalDuration: 0,
|
||||
},
|
||||
crowdstrikefalconbehaviourpatterndispositiondetails: [{}, {}, {}],
|
||||
datadogcloudsiem: [{}, {}, {}],
|
||||
dataminrpulserelatedterms: [{}, {}, {}],
|
||||
decyfirdatadetails: [{}, {}, {}],
|
||||
detectionsla: {
|
||||
accumulatedPause: 0,
|
||||
breachTriggered: false,
|
||||
dueDate: '0001-01-01T00:00:00Z',
|
||||
endDate: '0001-01-01T00:00:00Z',
|
||||
lastPauseDate: '0001-01-01T00:00:00Z',
|
||||
runStatus: 'idle',
|
||||
sla: 20,
|
||||
slaStatus: -1,
|
||||
startDate: '0001-01-01T00:00:00Z',
|
||||
totalDuration: 0,
|
||||
},
|
||||
domaintoolsirisdetect: [{}, {}, {}],
|
||||
endpoint: [{}],
|
||||
externalid: '178791',
|
||||
extrahoprevealxdetectiondevices: [{}, {}, {}],
|
||||
extrahoprevealxmitretechniques: [{}, {}, {}],
|
||||
filerelationships: [{}, {}, {}],
|
||||
fortisiemattacktactics: [{}, {}],
|
||||
fortisiemevents: [{}],
|
||||
incidentduration: {
|
||||
accumulatedPause: 0,
|
||||
breachTriggered: false,
|
||||
dueDate: '0001-01-01T00:00:00Z',
|
||||
endDate: '0001-01-01T00:00:00Z',
|
||||
lastPauseDate: '0001-01-01T00:00:00Z',
|
||||
runStatus: 'idle',
|
||||
sla: 0,
|
||||
slaStatus: -1,
|
||||
startDate: '0001-01-01T00:00:00Z',
|
||||
totalDuration: 0,
|
||||
},
|
||||
incidentrdpachehuntingstringssimilarity: [{}, {}, {}],
|
||||
incidentrdpcachehuntingstringsifter: [{}, {}, {}],
|
||||
inventasource: [{}],
|
||||
microsoftsentinelowner: [],
|
||||
qintelqwatchexposures: [{}, {}, {}],
|
||||
remediationsla: {
|
||||
accumulatedPause: 0,
|
||||
breachTriggered: false,
|
||||
dueDate: '0001-01-01T00:00:00Z',
|
||||
endDate: '0001-01-01T00:00:00Z',
|
||||
lastPauseDate: '0001-01-01T00:00:00Z',
|
||||
runStatus: 'idle',
|
||||
sla: 7200,
|
||||
slaStatus: -1,
|
||||
startDate: '0001-01-01T00:00:00Z',
|
||||
totalDuration: 0,
|
||||
},
|
||||
rsametasevents: [],
|
||||
rsarawlogslist: [],
|
||||
securitypolicymatch: [{}],
|
||||
similarincidentsdbot: [{}],
|
||||
spycloudcompassdevicedata: [{}, {}, {}],
|
||||
suspiciousexecutions: [{}, {}, {}],
|
||||
timetoassignment: {
|
||||
accumulatedPause: 0,
|
||||
breachTriggered: false,
|
||||
dueDate: '0001-01-01T00:00:00Z',
|
||||
endDate: '0001-01-01T00:00:00Z',
|
||||
lastPauseDate: '0001-01-01T00:00:00Z',
|
||||
runStatus: 'idle',
|
||||
sla: 0,
|
||||
slaStatus: -1,
|
||||
startDate: '0001-01-01T00:00:00Z',
|
||||
totalDuration: 0,
|
||||
},
|
||||
triagesla: {
|
||||
accumulatedPause: 0,
|
||||
breachTriggered: false,
|
||||
dueDate: '0001-01-01T00:00:00Z',
|
||||
endDate: '0001-01-01T00:00:00Z',
|
||||
lastPauseDate: '0001-01-01T00:00:00Z',
|
||||
runStatus: 'idle',
|
||||
sla: 30,
|
||||
slaStatus: -1,
|
||||
startDate: '0001-01-01T00:00:00Z',
|
||||
totalDuration: 0,
|
||||
},
|
||||
urlsslverification: [],
|
||||
xdralertsearchresults: [{}, {}, {}],
|
||||
xdrinvestigationresults: [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{
|
||||
columnheader1: '',
|
||||
},
|
||||
{},
|
||||
{
|
||||
columnheader1: '',
|
||||
},
|
||||
{},
|
||||
{},
|
||||
],
|
||||
xpanseserviceclassifications: [{}, {}, {}],
|
||||
xpanseservicevalidation: [
|
||||
{
|
||||
columnheader1: '',
|
||||
},
|
||||
{},
|
||||
{},
|
||||
],
|
||||
},
|
||||
account: '',
|
||||
autime: 1713700028107000000,
|
||||
type: 'Unclassified',
|
||||
rawType: 'Unclassified',
|
||||
name: 'My test incident',
|
||||
rawName: 'My test incident',
|
||||
status: 0,
|
||||
custom_status: '',
|
||||
resolution_status: '',
|
||||
reason: '',
|
||||
created: '2024-04-21T11:47:08.107Z',
|
||||
occurred: '2024-04-21T11:47:08.107982676Z',
|
||||
closed: '0001-01-01T00:00:00Z',
|
||||
sla: 0,
|
||||
severity: 2,
|
||||
investigationId: '',
|
||||
labels: [
|
||||
{
|
||||
value: '',
|
||||
type: 'Instance',
|
||||
},
|
||||
{
|
||||
value: 'Manual',
|
||||
type: 'Brand',
|
||||
},
|
||||
],
|
||||
attachment: null,
|
||||
details: 'My test incident',
|
||||
openDuration: 0,
|
||||
lastOpen: '0001-01-01T00:00:00Z',
|
||||
closingUserId: '',
|
||||
owner: '',
|
||||
activated: '0001-01-01T00:00:00Z',
|
||||
closeReason: '',
|
||||
rawCloseReason: '',
|
||||
closeNotes: '',
|
||||
playbookId: '8db0105c-f674-4d83-8095-f95a9f61e77a',
|
||||
dueDate: '2024-05-01T11:47:08.107988742Z',
|
||||
reminder: '0001-01-01T00:00:00Z',
|
||||
runStatus: '',
|
||||
notifyTime: '0001-01-01T00:00:00Z',
|
||||
phase: '',
|
||||
rawPhase: '',
|
||||
isPlayground: false,
|
||||
rawJSON: '',
|
||||
parent: '',
|
||||
parentXDRIncident: '',
|
||||
retained: false,
|
||||
category: '',
|
||||
rawCategory: '',
|
||||
linkedIncidents: null,
|
||||
linkedCount: 0,
|
||||
droppedCount: 0,
|
||||
sourceInstance: '',
|
||||
sourceBrand: 'Manual',
|
||||
canvases: null,
|
||||
lastJobRunTime: '0001-01-01T00:00:00Z',
|
||||
feedBased: false,
|
||||
dbotMirrorId: '',
|
||||
dbotMirrorInstance: '',
|
||||
dbotMirrorDirection: '',
|
||||
dbotDirtyFields: null,
|
||||
dbotCurrentDirtyFields: null,
|
||||
dbotMirrorTags: null,
|
||||
dbotMirrorLastSync: '0001-01-01T00:00:00Z',
|
||||
isDebug: false,
|
||||
};
|
||||
|
||||
export const XSOARFailedResponse = {
|
||||
id: 'incCreateErr',
|
||||
status: 400,
|
||||
title: 'Failed creating incident',
|
||||
detail: 'Failed creating incident',
|
||||
error: '',
|
||||
encrypted: false,
|
||||
multires: null,
|
||||
};
|
||||
|
||||
export class XSOARSimulator extends Simulator {
|
||||
private readonly returnError: boolean;
|
||||
|
||||
constructor({ returnError = false, proxy }: { returnError?: boolean; proxy?: ProxyArgs }) {
|
||||
super(proxy);
|
||||
|
||||
this.returnError = returnError;
|
||||
}
|
||||
|
||||
public async handler(
|
||||
request: http.IncomingMessage,
|
||||
response: http.ServerResponse,
|
||||
data: Record<string, unknown>
|
||||
) {
|
||||
if (this.returnError) {
|
||||
return XSOARSimulator.sendErrorResponse(response);
|
||||
}
|
||||
return XSOARSimulator.sendResponse(request, response);
|
||||
}
|
||||
|
||||
private static sendResponse(request: http.IncomingMessage, response: http.ServerResponse) {
|
||||
response.setHeader('Content-Type', 'application/json');
|
||||
let body;
|
||||
if (request.url?.match('/incident')) {
|
||||
response.statusCode = 201;
|
||||
body = XSOARRunSuccessResponse;
|
||||
} else if (request.url?.match('/playbook/search')) {
|
||||
response.statusCode = 200;
|
||||
body = XSOARPlaybooksResponse;
|
||||
}
|
||||
response.end(JSON.stringify(body, null, 4));
|
||||
}
|
||||
|
||||
private static sendErrorResponse(response: http.ServerResponse) {
|
||||
response.statusCode = 400;
|
||||
response.setHeader('Content-Type', 'application/json');
|
||||
response.end(JSON.stringify(XSOARFailedResponse, null, 4));
|
||||
}
|
||||
}
|
||||
|
||||
export function initPlugin(router: IRouter, path: string) {
|
||||
router.post(
|
||||
{
|
||||
path: `${path}/playbook/search`,
|
||||
options: {
|
||||
authRequired: false,
|
||||
},
|
||||
validate: {},
|
||||
security: { authz: { enabled: false, reason: 'This route is opted out from authorization' } },
|
||||
},
|
||||
async function (
|
||||
context: RequestHandlerContext,
|
||||
req: KibanaRequest<any, any, any, any>,
|
||||
res: KibanaResponseFactory
|
||||
): Promise<IKibanaResponse<any>> {
|
||||
return res.ok({ body: XSOARPlaybooksResponse });
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
{
|
||||
path: `${path}/incident`,
|
||||
options: {
|
||||
authRequired: false,
|
||||
},
|
||||
validate: {},
|
||||
security: { authz: { enabled: false, reason: 'This route is opted out from authorization' } },
|
||||
},
|
||||
async function (
|
||||
context: RequestHandlerContext,
|
||||
req: KibanaRequest<any, any, any, any>,
|
||||
res: KibanaResponseFactory
|
||||
): Promise<IKibanaResponse<any>> {
|
||||
return res.ok({ body: XSOARRunSuccessResponse });
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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'));
|
||||
|
|
|
@ -58,6 +58,7 @@ export default function createRegisteredConnectorTypeTests({ getService }: FtrPr
|
|||
'.cases',
|
||||
'.crowdstrike',
|
||||
'.microsoft_defender_endpoint',
|
||||
'.xsoar',
|
||||
].sort()
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue