mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Cases] Update IBM resilient connector to use sub action framework (#180561)
## Summary Fixes https://github.com/elastic/response-ops-team/issues/186 This PR updates IBM resilient connector to use CaseConnector of sub action framework. ### Steps to verify Expectation: IBM connector should work as before in all below scenarios: - Create an IBM resilient connector - Test the connector - Create an alert and use this connector as action - Use this connector in Cases ### Flaky test runner https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/5667 ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
4dcc3c985f
commit
b3f7c5cf0d
23 changed files with 1782 additions and 2705 deletions
|
@ -6068,6 +6068,336 @@ Object {
|
|||
`;
|
||||
|
||||
exports[`Connector type config checks detect connector type changes for: .resilient 1`] = `
|
||||
Object {
|
||||
"flags": Object {
|
||||
"default": Object {
|
||||
"special": "deep",
|
||||
},
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"keys": Object {
|
||||
"comments": Object {
|
||||
"flags": Object {
|
||||
"default": null,
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"matches": Array [
|
||||
Object {
|
||||
"schema": Object {
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
},
|
||||
"items": Array [
|
||||
Object {
|
||||
"flags": Object {
|
||||
"default": Object {
|
||||
"special": "deep",
|
||||
},
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"keys": Object {
|
||||
"comment": Object {
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
},
|
||||
"rules": Array [
|
||||
Object {
|
||||
"args": Object {
|
||||
"method": [Function],
|
||||
},
|
||||
"name": "custom",
|
||||
},
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
"commentId": Object {
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
},
|
||||
"rules": Array [
|
||||
Object {
|
||||
"args": Object {
|
||||
"method": [Function],
|
||||
},
|
||||
"name": "custom",
|
||||
},
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"preferences": Object {
|
||||
"stripUnknown": Object {
|
||||
"objects": false,
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
],
|
||||
"type": "array",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"schema": Object {
|
||||
"allow": Array [
|
||||
null,
|
||||
],
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
"only": true,
|
||||
},
|
||||
"type": "any",
|
||||
},
|
||||
},
|
||||
],
|
||||
"type": "alternatives",
|
||||
},
|
||||
"incident": Object {
|
||||
"flags": Object {
|
||||
"default": Object {
|
||||
"special": "deep",
|
||||
},
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"keys": Object {
|
||||
"description": Object {
|
||||
"flags": Object {
|
||||
"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",
|
||||
},
|
||||
"externalId": Object {
|
||||
"flags": Object {
|
||||
"default": null,
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"matches": Array [
|
||||
Object {
|
||||
"schema": Object {
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
},
|
||||
"rules": Array [
|
||||
Object {
|
||||
"args": Object {
|
||||
"method": [Function],
|
||||
},
|
||||
"name": "custom",
|
||||
},
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"schema": Object {
|
||||
"allow": Array [
|
||||
null,
|
||||
],
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
"only": true,
|
||||
},
|
||||
"type": "any",
|
||||
},
|
||||
},
|
||||
],
|
||||
"type": "alternatives",
|
||||
},
|
||||
"incidentTypes": Object {
|
||||
"flags": Object {
|
||||
"default": null,
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"matches": Array [
|
||||
Object {
|
||||
"schema": Object {
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
},
|
||||
"items": Array [
|
||||
Object {
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"type": "number",
|
||||
},
|
||||
],
|
||||
"type": "array",
|
||||
},
|
||||
},
|
||||
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",
|
||||
},
|
||||
"severityCode": Object {
|
||||
"flags": Object {
|
||||
"default": null,
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"matches": Array [
|
||||
Object {
|
||||
"schema": Object {
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
},
|
||||
"type": "number",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"schema": Object {
|
||||
"allow": Array [
|
||||
null,
|
||||
],
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
"only": true,
|
||||
},
|
||||
"type": "any",
|
||||
},
|
||||
},
|
||||
],
|
||||
"type": "alternatives",
|
||||
},
|
||||
},
|
||||
"preferences": Object {
|
||||
"stripUnknown": Object {
|
||||
"objects": false,
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
"preferences": Object {
|
||||
"stripUnknown": Object {
|
||||
"objects": false,
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Connector type config checks detect connector type changes for: .resilient 2`] = `
|
||||
Object {
|
||||
"flags": Object {
|
||||
"default": Object {
|
||||
"special": "deep",
|
||||
},
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"keys": Object {},
|
||||
"preferences": Object {
|
||||
"stripUnknown": Object {
|
||||
"objects": false,
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Connector type config checks detect connector type changes for: .resilient 3`] = `
|
||||
Object {
|
||||
"flags": Object {
|
||||
"default": Object {
|
||||
"special": "deep",
|
||||
},
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"keys": Object {},
|
||||
"preferences": Object {
|
||||
"stripUnknown": Object {
|
||||
"objects": false,
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Connector type config checks detect connector type changes for: .resilient 4`] = `
|
||||
Object {
|
||||
"flags": Object {
|
||||
"default": Object {
|
||||
"special": "deep",
|
||||
},
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"keys": Object {},
|
||||
"preferences": Object {
|
||||
"stripUnknown": Object {
|
||||
"objects": false,
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Connector type config checks detect connector type changes for: .resilient 5`] = `
|
||||
Object {
|
||||
"flags": Object {
|
||||
"default": Object {
|
||||
|
@ -6115,7 +6445,7 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`Connector type config checks detect connector type changes for: .resilient 2`] = `
|
||||
exports[`Connector type config checks detect connector type changes for: .resilient 6`] = `
|
||||
Object {
|
||||
"flags": Object {
|
||||
"default": Object {
|
||||
|
@ -6163,553 +6493,54 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`Connector type config checks detect connector type changes for: .resilient 3`] = `
|
||||
exports[`Connector type config checks detect connector type changes for: .resilient 7`] = `
|
||||
Object {
|
||||
"flags": Object {
|
||||
"default": Object {
|
||||
"special": "deep",
|
||||
},
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"matches": Array [
|
||||
Object {
|
||||
"schema": Object {
|
||||
"flags": Object {
|
||||
"default": Object {
|
||||
"special": "deep",
|
||||
},
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"keys": Object {
|
||||
"subAction": Object {
|
||||
"allow": Array [
|
||||
"getFields",
|
||||
],
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
"only": true,
|
||||
},
|
||||
"type": "any",
|
||||
},
|
||||
"subActionParams": Object {
|
||||
"flags": Object {
|
||||
"default": Object {
|
||||
"special": "deep",
|
||||
},
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"keys": Object {},
|
||||
"preferences": Object {
|
||||
"stripUnknown": Object {
|
||||
"objects": false,
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
"preferences": Object {
|
||||
"stripUnknown": Object {
|
||||
"objects": false,
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
"keys": Object {
|
||||
"subAction": Object {
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
},
|
||||
"rules": Array [
|
||||
Object {
|
||||
"args": Object {
|
||||
"method": [Function],
|
||||
},
|
||||
"name": "custom",
|
||||
},
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
Object {
|
||||
"schema": Object {
|
||||
"flags": Object {
|
||||
"default": Object {
|
||||
"special": "deep",
|
||||
},
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
"subActionParams": Object {
|
||||
"flags": Object {
|
||||
"default": Object {
|
||||
"special": "deep",
|
||||
},
|
||||
"keys": Object {
|
||||
"subAction": Object {
|
||||
"allow": Array [
|
||||
"getIncident",
|
||||
],
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
"only": true,
|
||||
},
|
||||
"type": "any",
|
||||
},
|
||||
"subActionParams": Object {
|
||||
"flags": Object {
|
||||
"default": Object {
|
||||
"special": "deep",
|
||||
},
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"keys": Object {
|
||||
"externalId": Object {
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
},
|
||||
"rules": Array [
|
||||
Object {
|
||||
"args": Object {
|
||||
"method": [Function],
|
||||
},
|
||||
"name": "custom",
|
||||
},
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"preferences": Object {
|
||||
"stripUnknown": Object {
|
||||
"objects": false,
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
"preferences": Object {
|
||||
"stripUnknown": Object {
|
||||
"objects": false,
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
"unknown": true,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"schema": Object {
|
||||
"flags": Object {
|
||||
"default": Object {
|
||||
"special": "deep",
|
||||
},
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
"keys": Object {},
|
||||
"preferences": Object {
|
||||
"stripUnknown": Object {
|
||||
"objects": false,
|
||||
},
|
||||
"keys": Object {
|
||||
"subAction": Object {
|
||||
"allow": Array [
|
||||
"handshake",
|
||||
],
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
"only": true,
|
||||
},
|
||||
"type": "any",
|
||||
},
|
||||
"subActionParams": Object {
|
||||
"flags": Object {
|
||||
"default": Object {
|
||||
"special": "deep",
|
||||
},
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"keys": Object {},
|
||||
"preferences": Object {
|
||||
"stripUnknown": Object {
|
||||
"objects": false,
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
"preferences": Object {
|
||||
"stripUnknown": Object {
|
||||
"objects": false,
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
Object {
|
||||
"schema": Object {
|
||||
"flags": Object {
|
||||
"default": Object {
|
||||
"special": "deep",
|
||||
},
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"keys": Object {
|
||||
"subAction": Object {
|
||||
"allow": Array [
|
||||
"pushToService",
|
||||
],
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
"only": true,
|
||||
},
|
||||
"type": "any",
|
||||
},
|
||||
"subActionParams": Object {
|
||||
"flags": Object {
|
||||
"default": Object {
|
||||
"special": "deep",
|
||||
},
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"keys": Object {
|
||||
"comments": Object {
|
||||
"flags": Object {
|
||||
"default": null,
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"matches": Array [
|
||||
Object {
|
||||
"schema": Object {
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
},
|
||||
"items": Array [
|
||||
Object {
|
||||
"flags": Object {
|
||||
"default": Object {
|
||||
"special": "deep",
|
||||
},
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"keys": Object {
|
||||
"comment": Object {
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
},
|
||||
"rules": Array [
|
||||
Object {
|
||||
"args": Object {
|
||||
"method": [Function],
|
||||
},
|
||||
"name": "custom",
|
||||
},
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
"commentId": Object {
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
},
|
||||
"rules": Array [
|
||||
Object {
|
||||
"args": Object {
|
||||
"method": [Function],
|
||||
},
|
||||
"name": "custom",
|
||||
},
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"preferences": Object {
|
||||
"stripUnknown": Object {
|
||||
"objects": false,
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
],
|
||||
"type": "array",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"schema": Object {
|
||||
"allow": Array [
|
||||
null,
|
||||
],
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
"only": true,
|
||||
},
|
||||
"type": "any",
|
||||
},
|
||||
},
|
||||
],
|
||||
"type": "alternatives",
|
||||
},
|
||||
"incident": Object {
|
||||
"flags": Object {
|
||||
"default": Object {
|
||||
"special": "deep",
|
||||
},
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"keys": Object {
|
||||
"description": Object {
|
||||
"flags": Object {
|
||||
"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",
|
||||
},
|
||||
"externalId": Object {
|
||||
"flags": Object {
|
||||
"default": null,
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"matches": Array [
|
||||
Object {
|
||||
"schema": Object {
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
},
|
||||
"rules": Array [
|
||||
Object {
|
||||
"args": Object {
|
||||
"method": [Function],
|
||||
},
|
||||
"name": "custom",
|
||||
},
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"schema": Object {
|
||||
"allow": Array [
|
||||
null,
|
||||
],
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
"only": true,
|
||||
},
|
||||
"type": "any",
|
||||
},
|
||||
},
|
||||
],
|
||||
"type": "alternatives",
|
||||
},
|
||||
"incidentTypes": Object {
|
||||
"flags": Object {
|
||||
"default": null,
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"matches": Array [
|
||||
Object {
|
||||
"schema": Object {
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
},
|
||||
"items": Array [
|
||||
Object {
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"type": "number",
|
||||
},
|
||||
],
|
||||
"type": "array",
|
||||
},
|
||||
},
|
||||
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",
|
||||
},
|
||||
"severityCode": Object {
|
||||
"flags": Object {
|
||||
"default": null,
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"matches": Array [
|
||||
Object {
|
||||
"schema": Object {
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
},
|
||||
"type": "number",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"schema": Object {
|
||||
"allow": Array [
|
||||
null,
|
||||
],
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
"only": true,
|
||||
},
|
||||
"type": "any",
|
||||
},
|
||||
},
|
||||
],
|
||||
"type": "alternatives",
|
||||
},
|
||||
},
|
||||
"preferences": Object {
|
||||
"stripUnknown": Object {
|
||||
"objects": false,
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
"preferences": Object {
|
||||
"stripUnknown": Object {
|
||||
"objects": false,
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
"preferences": Object {
|
||||
"stripUnknown": Object {
|
||||
"objects": false,
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
"preferences": Object {
|
||||
"stripUnknown": Object {
|
||||
"objects": false,
|
||||
},
|
||||
Object {
|
||||
"schema": Object {
|
||||
"flags": Object {
|
||||
"default": Object {
|
||||
"special": "deep",
|
||||
},
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"keys": Object {
|
||||
"subAction": Object {
|
||||
"allow": Array [
|
||||
"incidentTypes",
|
||||
],
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
"only": true,
|
||||
},
|
||||
"type": "any",
|
||||
},
|
||||
"subActionParams": Object {
|
||||
"flags": Object {
|
||||
"default": Object {
|
||||
"special": "deep",
|
||||
},
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"keys": Object {},
|
||||
"preferences": Object {
|
||||
"stripUnknown": Object {
|
||||
"objects": false,
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
"preferences": Object {
|
||||
"stripUnknown": Object {
|
||||
"objects": false,
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"schema": Object {
|
||||
"flags": Object {
|
||||
"default": Object {
|
||||
"special": "deep",
|
||||
},
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"keys": Object {
|
||||
"subAction": Object {
|
||||
"allow": Array [
|
||||
"severity",
|
||||
],
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
"only": true,
|
||||
},
|
||||
"type": "any",
|
||||
},
|
||||
"subActionParams": Object {
|
||||
"flags": Object {
|
||||
"default": Object {
|
||||
"special": "deep",
|
||||
},
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"keys": Object {},
|
||||
"preferences": Object {
|
||||
"stripUnknown": Object {
|
||||
"objects": false,
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
"preferences": Object {
|
||||
"stripUnknown": Object {
|
||||
"objects": false,
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
],
|
||||
"type": "alternatives",
|
||||
},
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
|
@ -20,7 +20,6 @@ export const connectorTypes: string[] = [
|
|||
'.servicenow-sir',
|
||||
'.servicenow-itom',
|
||||
'.jira',
|
||||
'.resilient',
|
||||
'.teams',
|
||||
'.torq',
|
||||
'.opsgenie',
|
||||
|
@ -28,6 +27,7 @@ export const connectorTypes: string[] = [
|
|||
'.gen-ai',
|
||||
'.bedrock',
|
||||
'.d3security',
|
||||
'.resilient',
|
||||
'.sentinelone',
|
||||
'.cases',
|
||||
'.observability-ai-assistant',
|
||||
|
|
|
@ -9,11 +9,11 @@ import { PluginSetupContract as ActionsPluginSetupContract } from '@kbn/actions-
|
|||
|
||||
import { getConnectorType as getCasesWebhookConnectorType } from './cases_webhook';
|
||||
import { getConnectorType as getJiraConnectorType } from './jira';
|
||||
import { getConnectorType as getResilientConnectorType } from './resilient';
|
||||
import { getServiceNowITSMConnectorType } from './servicenow_itsm';
|
||||
import { getServiceNowSIRConnectorType } from './servicenow_sir';
|
||||
import { getServiceNowITOMConnectorType } from './servicenow_itom';
|
||||
import { getTinesConnectorType } from './tines';
|
||||
import { getResilientConnectorType } from './resilient';
|
||||
import { getActionType as getTorqConnectorType } from './torq';
|
||||
import { getConnectorType as getEmailConnectorType } from './email';
|
||||
import { getConnectorType as getIndexConnectorType } from './es_index';
|
||||
|
@ -39,8 +39,6 @@ export { ConnectorTypeId as CasesWebhookConnectorTypeId } from './cases_webhook'
|
|||
export type { ActionParamsType as CasesWebhookActionParams } from './cases_webhook';
|
||||
export { ConnectorTypeId as JiraConnectorTypeId } from './jira';
|
||||
export type { ActionParamsType as JiraActionParams } from './jira';
|
||||
export { ConnectorTypeId as ResilientConnectorTypeId } from './resilient';
|
||||
export type { ActionParamsType as ResilientActionParams } from './resilient';
|
||||
export { ServiceNowITSMConnectorTypeId } from './servicenow_itsm';
|
||||
export { ServiceNowSIRConnectorTypeId } from './servicenow_sir';
|
||||
export { ConnectorTypeId as EmailConnectorTypeId } from './email';
|
||||
|
@ -100,7 +98,6 @@ export function registerConnectorTypes({
|
|||
actions.registerType(getServiceNowSIRConnectorType());
|
||||
actions.registerType(getServiceNowITOMConnectorType());
|
||||
actions.registerType(getJiraConnectorType());
|
||||
actions.registerType(getResilientConnectorType());
|
||||
actions.registerType(getTeamsConnectorType());
|
||||
actions.registerType(getTorqConnectorType());
|
||||
|
||||
|
@ -109,6 +106,7 @@ export function registerConnectorTypes({
|
|||
actions.registerSubActionConnectorType(getOpenAIConnectorType());
|
||||
actions.registerSubActionConnectorType(getBedrockConnectorType());
|
||||
actions.registerSubActionConnectorType(getD3SecurityConnectorType());
|
||||
actions.registerSubActionConnectorType(getResilientConnectorType());
|
||||
|
||||
if (experimentalFeatures.sentinelOneConnectorOn) {
|
||||
actions.registerSubActionConnectorType(getSentinelOneConnectorType());
|
||||
|
|
|
@ -1,214 +0,0 @@
|
|||
/*
|
||||
* 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 { Logger } from '@kbn/core/server';
|
||||
import { api } from './api';
|
||||
import { externalServiceMock, apiParams } from './mocks';
|
||||
import { ExternalService } from './types';
|
||||
|
||||
let mockedLogger: jest.Mocked<Logger>;
|
||||
|
||||
describe('api', () => {
|
||||
let externalService: jest.Mocked<ExternalService>;
|
||||
|
||||
beforeEach(() => {
|
||||
externalService = externalServiceMock.create();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('pushToService', () => {
|
||||
describe('create incident', () => {
|
||||
test('it creates an incident', async () => {
|
||||
const params = { ...apiParams, externalId: null };
|
||||
const res = await api.pushToService({
|
||||
externalService,
|
||||
params,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
id: '1',
|
||||
title: '1',
|
||||
pushedDate: '2020-06-03T15:09:13.606Z',
|
||||
url: 'https://resilient.elastic.co/#incidents/1',
|
||||
comments: [
|
||||
{
|
||||
commentId: 'case-comment-1',
|
||||
pushedDate: '2020-06-03T15:09:13.606Z',
|
||||
},
|
||||
{
|
||||
commentId: 'case-comment-2',
|
||||
pushedDate: '2020-06-03T15:09:13.606Z',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('it creates an incident without comments', async () => {
|
||||
const params = { ...apiParams, externalId: null, comments: [] };
|
||||
const res = await api.pushToService({
|
||||
externalService,
|
||||
params,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
id: '1',
|
||||
title: '1',
|
||||
pushedDate: '2020-06-03T15:09:13.606Z',
|
||||
url: 'https://resilient.elastic.co/#incidents/1',
|
||||
});
|
||||
});
|
||||
|
||||
test('it calls createIncident correctly', async () => {
|
||||
const params = { ...apiParams, incident: { ...apiParams.incident, externalId: null } };
|
||||
await api.pushToService({ externalService, params, logger: mockedLogger });
|
||||
|
||||
expect(externalService.createIncident).toHaveBeenCalledWith({
|
||||
incident: {
|
||||
incidentTypes: [1001],
|
||||
severityCode: 6,
|
||||
description: 'Incident description',
|
||||
name: 'Incident title',
|
||||
},
|
||||
});
|
||||
expect(externalService.updateIncident).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('it calls createComment correctly', async () => {
|
||||
const params = { ...apiParams, externalId: null };
|
||||
await api.pushToService({ externalService, params, logger: mockedLogger });
|
||||
expect(externalService.createComment).toHaveBeenCalledTimes(2);
|
||||
expect(externalService.createComment).toHaveBeenNthCalledWith(1, {
|
||||
incidentId: '1',
|
||||
comment: {
|
||||
commentId: 'case-comment-1',
|
||||
comment: 'A comment',
|
||||
},
|
||||
});
|
||||
|
||||
expect(externalService.createComment).toHaveBeenNthCalledWith(2, {
|
||||
incidentId: '1',
|
||||
comment: {
|
||||
commentId: 'case-comment-2',
|
||||
comment: 'Another comment',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('update incident', () => {
|
||||
test('it updates an incident', async () => {
|
||||
const res = await api.pushToService({
|
||||
externalService,
|
||||
params: apiParams,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
id: '1',
|
||||
title: '1',
|
||||
pushedDate: '2020-06-03T15:09:13.606Z',
|
||||
url: 'https://resilient.elastic.co/#incidents/1',
|
||||
comments: [
|
||||
{
|
||||
commentId: 'case-comment-1',
|
||||
pushedDate: '2020-06-03T15:09:13.606Z',
|
||||
},
|
||||
{
|
||||
commentId: 'case-comment-2',
|
||||
pushedDate: '2020-06-03T15:09:13.606Z',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('it updates an incident without comments', async () => {
|
||||
const params = { ...apiParams, comments: [] };
|
||||
const res = await api.pushToService({
|
||||
externalService,
|
||||
params,
|
||||
logger: mockedLogger,
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
id: '1',
|
||||
title: '1',
|
||||
pushedDate: '2020-06-03T15:09:13.606Z',
|
||||
url: 'https://resilient.elastic.co/#incidents/1',
|
||||
});
|
||||
});
|
||||
|
||||
test('it calls updateIncident correctly', async () => {
|
||||
const params = { ...apiParams };
|
||||
await api.pushToService({ externalService, params, logger: mockedLogger });
|
||||
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
incidentTypes: [1001],
|
||||
severityCode: 6,
|
||||
description: 'Incident description',
|
||||
name: 'Incident title',
|
||||
},
|
||||
});
|
||||
expect(externalService.createIncident).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('it calls createComment correctly', async () => {
|
||||
const params = { ...apiParams };
|
||||
await api.pushToService({ externalService, params, logger: mockedLogger });
|
||||
expect(externalService.createComment).toHaveBeenCalledTimes(2);
|
||||
expect(externalService.createComment).toHaveBeenNthCalledWith(1, {
|
||||
incidentId: '1',
|
||||
comment: {
|
||||
commentId: 'case-comment-1',
|
||||
comment: 'A comment',
|
||||
},
|
||||
});
|
||||
|
||||
expect(externalService.createComment).toHaveBeenNthCalledWith(2, {
|
||||
incidentId: '1',
|
||||
comment: {
|
||||
commentId: 'case-comment-2',
|
||||
comment: 'Another comment',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('incidentTypes', () => {
|
||||
test('it returns the incident types correctly', async () => {
|
||||
const res = await api.incidentTypes({
|
||||
externalService,
|
||||
params: {},
|
||||
});
|
||||
expect(res).toEqual([
|
||||
{ id: 17, name: 'Communication error (fax; email)' },
|
||||
{ id: 1001, name: 'Custom type' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('severity', () => {
|
||||
test('it returns the severity correctly', async () => {
|
||||
const res = await api.severity({
|
||||
externalService,
|
||||
params: { id: '10006' },
|
||||
});
|
||||
expect(res).toEqual([
|
||||
{ id: 4, name: 'Low' },
|
||||
{ id: 5, name: 'Medium' },
|
||||
{ id: 6, name: 'High' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,85 +0,0 @@
|
|||
/*
|
||||
* 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 {
|
||||
PushToServiceApiHandlerArgs,
|
||||
HandshakeApiHandlerArgs,
|
||||
GetIncidentApiHandlerArgs,
|
||||
ExternalServiceApi,
|
||||
Incident,
|
||||
GetIncidentTypesHandlerArgs,
|
||||
GetSeverityHandlerArgs,
|
||||
PushToServiceResponse,
|
||||
GetCommonFieldsHandlerArgs,
|
||||
} from './types';
|
||||
|
||||
const handshakeHandler = async ({ externalService, params }: HandshakeApiHandlerArgs) => {};
|
||||
|
||||
const getIncidentHandler = async ({ externalService, params }: GetIncidentApiHandlerArgs) => {};
|
||||
|
||||
const getFieldsHandler = async ({ externalService }: GetCommonFieldsHandlerArgs) => {
|
||||
const res = await externalService.getFields();
|
||||
return res;
|
||||
};
|
||||
const getIncidentTypesHandler = async ({ externalService }: GetIncidentTypesHandlerArgs) => {
|
||||
const res = await externalService.getIncidentTypes();
|
||||
return res;
|
||||
};
|
||||
|
||||
const getSeverityHandler = async ({ externalService }: GetSeverityHandlerArgs) => {
|
||||
const res = await externalService.getSeverity();
|
||||
return res;
|
||||
};
|
||||
|
||||
const pushToServiceHandler = async ({
|
||||
externalService,
|
||||
params,
|
||||
}: PushToServiceApiHandlerArgs): Promise<PushToServiceResponse> => {
|
||||
const { comments } = params;
|
||||
let res: PushToServiceResponse;
|
||||
const { externalId, ...rest } = params.incident;
|
||||
const incident: Incident = rest;
|
||||
|
||||
if (externalId != null) {
|
||||
res = await externalService.updateIncident({
|
||||
incidentId: externalId,
|
||||
incident,
|
||||
});
|
||||
} else {
|
||||
res = await externalService.createIncident({
|
||||
incident,
|
||||
});
|
||||
}
|
||||
|
||||
if (comments && Array.isArray(comments) && comments.length > 0) {
|
||||
res.comments = [];
|
||||
for (const currentComment of comments) {
|
||||
const comment = await externalService.createComment({
|
||||
incidentId: res.id,
|
||||
comment: currentComment,
|
||||
});
|
||||
res.comments = [
|
||||
...(res.comments ?? []),
|
||||
{
|
||||
commentId: comment.commentId,
|
||||
pushedDate: comment.pushedDate,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const api: ExternalServiceApi = {
|
||||
getFields: getFieldsHandler,
|
||||
getIncident: getIncidentHandler,
|
||||
handshake: handshakeHandler,
|
||||
incidentTypes: getIncidentTypesHandler,
|
||||
pushToService: pushToServiceHandler,
|
||||
severity: getSeverityHandler,
|
||||
};
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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 const RESILIENT_CONNECTOR_ID = '.resilient';
|
||||
|
||||
export enum SUB_ACTION {
|
||||
FIELDS = 'getFields',
|
||||
SEVERITY = 'severity',
|
||||
INCIDENT_TYPES = 'incidentTypes',
|
||||
}
|
|
@ -5,144 +5,43 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { TypeOf } from '@kbn/config-schema';
|
||||
|
||||
import type {
|
||||
ActionType as ConnectorType,
|
||||
ActionTypeExecutorOptions as ConnectorTypeExecutorOptions,
|
||||
ActionTypeExecutorResult as ConnectorTypeExecutorResult,
|
||||
} from '@kbn/actions-plugin/server/types';
|
||||
import {
|
||||
SubActionConnectorType,
|
||||
ValidatorType,
|
||||
} from '@kbn/actions-plugin/server/sub_action_framework/types';
|
||||
import {
|
||||
AlertingConnectorFeatureId,
|
||||
CasesConnectorFeatureId,
|
||||
SecurityConnectorFeatureId,
|
||||
} from '@kbn/actions-plugin/common/types';
|
||||
import { validate } from './validators';
|
||||
} from '@kbn/actions-plugin/common';
|
||||
import { urlAllowListValidator } from '@kbn/actions-plugin/server';
|
||||
|
||||
import { ResilientConfig, ResilientSecrets } from './types';
|
||||
import { RESILIENT_CONNECTOR_ID } from './constants';
|
||||
import * as i18n from './translations';
|
||||
import {
|
||||
ExternalIncidentServiceConfigurationSchema,
|
||||
ExternalIncidentServiceSecretConfigurationSchema,
|
||||
ExecutorParamsSchema,
|
||||
PushToServiceIncidentSchema,
|
||||
} from './schema';
|
||||
import { createExternalService } from './service';
|
||||
import { api } from './api';
|
||||
import {
|
||||
ExecutorParams,
|
||||
ExecutorSubActionPushParams,
|
||||
ResilientPublicConfigurationType,
|
||||
ResilientSecretConfigurationType,
|
||||
ResilientExecutorResultData,
|
||||
ExecutorSubActionGetIncidentTypesParams,
|
||||
ExecutorSubActionGetSeverityParams,
|
||||
ExecutorSubActionCommonFieldsParams,
|
||||
} from './types';
|
||||
import * as i18n from './translations';
|
||||
import { ResilientConnector } from './resilient';
|
||||
|
||||
export type ActionParamsType = TypeOf<typeof ExecutorParamsSchema>;
|
||||
|
||||
const supportedSubActions: string[] = ['getFields', 'pushToService', 'incidentTypes', 'severity'];
|
||||
|
||||
export const ConnectorTypeId = '.resilient';
|
||||
// connector type definition
|
||||
export function getConnectorType(): ConnectorType<
|
||||
ResilientPublicConfigurationType,
|
||||
ResilientSecretConfigurationType,
|
||||
ExecutorParams,
|
||||
ResilientExecutorResultData | {}
|
||||
> {
|
||||
return {
|
||||
id: ConnectorTypeId,
|
||||
minimumLicenseRequired: 'platinum',
|
||||
name: i18n.NAME,
|
||||
supportedFeatureIds: [
|
||||
AlertingConnectorFeatureId,
|
||||
CasesConnectorFeatureId,
|
||||
SecurityConnectorFeatureId,
|
||||
],
|
||||
validate: {
|
||||
config: {
|
||||
schema: ExternalIncidentServiceConfigurationSchema,
|
||||
customValidator: validate.config,
|
||||
},
|
||||
secrets: {
|
||||
schema: ExternalIncidentServiceSecretConfigurationSchema,
|
||||
customValidator: validate.secrets,
|
||||
},
|
||||
params: {
|
||||
schema: ExecutorParamsSchema,
|
||||
},
|
||||
},
|
||||
executor,
|
||||
};
|
||||
}
|
||||
|
||||
// action executor
|
||||
async function executor(
|
||||
execOptions: ConnectorTypeExecutorOptions<
|
||||
ResilientPublicConfigurationType,
|
||||
ResilientSecretConfigurationType,
|
||||
ExecutorParams
|
||||
>
|
||||
): Promise<ConnectorTypeExecutorResult<ResilientExecutorResultData | {}>> {
|
||||
const { actionId, config, params, secrets, configurationUtilities, logger } = execOptions;
|
||||
const { subAction, subActionParams } = params as ExecutorParams;
|
||||
let data: ResilientExecutorResultData | null = null;
|
||||
|
||||
const externalService = createExternalService(
|
||||
{
|
||||
config,
|
||||
secrets,
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
);
|
||||
|
||||
if (!api[subAction]) {
|
||||
const errorMessage = `[Action][ExternalService] Unsupported subAction type ${subAction}.`;
|
||||
logger.error(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
if (!supportedSubActions.includes(subAction)) {
|
||||
const errorMessage = `[Action][ExternalService] subAction ${subAction} not implemented.`;
|
||||
logger.error(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
if (subAction === 'pushToService') {
|
||||
const pushToServiceParams = subActionParams as ExecutorSubActionPushParams;
|
||||
|
||||
data = await api.pushToService({
|
||||
externalService,
|
||||
params: pushToServiceParams,
|
||||
logger,
|
||||
});
|
||||
|
||||
logger.debug(`response push to service for incident id: ${data.id}`);
|
||||
}
|
||||
|
||||
if (subAction === 'getFields') {
|
||||
const getFieldsParams = subActionParams as ExecutorSubActionCommonFieldsParams;
|
||||
data = await api.getFields({
|
||||
externalService,
|
||||
params: getFieldsParams,
|
||||
});
|
||||
}
|
||||
|
||||
if (subAction === 'incidentTypes') {
|
||||
const incidentTypesParams = subActionParams as ExecutorSubActionGetIncidentTypesParams;
|
||||
data = await api.incidentTypes({
|
||||
externalService,
|
||||
params: incidentTypesParams,
|
||||
});
|
||||
}
|
||||
|
||||
if (subAction === 'severity') {
|
||||
const severityParams = subActionParams as ExecutorSubActionGetSeverityParams;
|
||||
data = await api.severity({
|
||||
externalService,
|
||||
params: severityParams,
|
||||
});
|
||||
}
|
||||
|
||||
return { status: 'ok', data: data ?? {}, actionId };
|
||||
}
|
||||
export const getResilientConnectorType = (): SubActionConnectorType<
|
||||
ResilientConfig,
|
||||
ResilientSecrets
|
||||
> => ({
|
||||
id: RESILIENT_CONNECTOR_ID,
|
||||
minimumLicenseRequired: 'platinum',
|
||||
name: i18n.NAME,
|
||||
getService: (params) => new ResilientConnector(params, PushToServiceIncidentSchema),
|
||||
schema: {
|
||||
config: ExternalIncidentServiceConfigurationSchema,
|
||||
secrets: ExternalIncidentServiceSecretConfigurationSchema,
|
||||
},
|
||||
supportedFeatureIds: [
|
||||
AlertingConnectorFeatureId,
|
||||
CasesConnectorFeatureId,
|
||||
SecurityConnectorFeatureId,
|
||||
],
|
||||
validators: [{ type: ValidatorType.CONFIG, validator: urlAllowListValidator('apiUrl') }],
|
||||
});
|
||||
|
|
|
@ -5,394 +5,55 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ExternalService, PushToServiceApiParams, ExecutorSubActionPushParams } from './types';
|
||||
|
||||
export const resilientFields = [
|
||||
{
|
||||
id: 17,
|
||||
name: 'name',
|
||||
text: 'Name',
|
||||
prefix: null,
|
||||
type_id: 0,
|
||||
tooltip: 'A unique name to identify this particular incident.',
|
||||
text: 'name',
|
||||
input_type: 'text',
|
||||
required: 'always',
|
||||
hide_notification: false,
|
||||
chosen: false,
|
||||
default_chosen_by_server: false,
|
||||
blank_option: false,
|
||||
internal: true,
|
||||
uuid: 'ad6ed4f2-8d87-4ba2-81fa-03568a9326cc',
|
||||
operations: [
|
||||
'equals',
|
||||
'not_equals',
|
||||
'contains',
|
||||
'not_contains',
|
||||
'changed',
|
||||
'changed_to',
|
||||
'not_changed_to',
|
||||
'has_a_value',
|
||||
'not_has_a_value',
|
||||
],
|
||||
operation_perms: {
|
||||
changed_to: {
|
||||
show_in_manual_actions: false,
|
||||
show_in_auto_actions: true,
|
||||
show_in_notifications: true,
|
||||
},
|
||||
has_a_value: {
|
||||
show_in_manual_actions: true,
|
||||
show_in_auto_actions: true,
|
||||
show_in_notifications: true,
|
||||
},
|
||||
not_changed_to: {
|
||||
show_in_manual_actions: false,
|
||||
show_in_auto_actions: true,
|
||||
show_in_notifications: true,
|
||||
},
|
||||
equals: {
|
||||
show_in_manual_actions: true,
|
||||
show_in_auto_actions: true,
|
||||
show_in_notifications: true,
|
||||
},
|
||||
changed: {
|
||||
show_in_manual_actions: false,
|
||||
show_in_auto_actions: true,
|
||||
show_in_notifications: true,
|
||||
},
|
||||
contains: {
|
||||
show_in_manual_actions: true,
|
||||
show_in_auto_actions: true,
|
||||
show_in_notifications: true,
|
||||
},
|
||||
not_contains: {
|
||||
show_in_manual_actions: true,
|
||||
show_in_auto_actions: true,
|
||||
show_in_notifications: true,
|
||||
},
|
||||
not_equals: {
|
||||
show_in_manual_actions: true,
|
||||
show_in_auto_actions: true,
|
||||
show_in_notifications: true,
|
||||
},
|
||||
not_has_a_value: {
|
||||
show_in_manual_actions: true,
|
||||
show_in_auto_actions: true,
|
||||
show_in_notifications: true,
|
||||
},
|
||||
},
|
||||
values: [],
|
||||
perms: {
|
||||
delete: false,
|
||||
modify_name: false,
|
||||
modify_values: false,
|
||||
modify_blank: false,
|
||||
modify_required: false,
|
||||
modify_operations: false,
|
||||
modify_chosen: false,
|
||||
modify_default: false,
|
||||
show_in_manual_actions: true,
|
||||
show_in_auto_actions: true,
|
||||
show_in_notifications: true,
|
||||
show_in_scripts: true,
|
||||
modify_type: ['text'],
|
||||
sort: true,
|
||||
},
|
||||
read_only: false,
|
||||
changeable: true,
|
||||
rich_text: false,
|
||||
templates: [],
|
||||
deprecated: false,
|
||||
tags: [],
|
||||
calculated: false,
|
||||
is_tracked: false,
|
||||
allow_default_value: false,
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
name: 'description',
|
||||
text: 'Description',
|
||||
prefix: null,
|
||||
type_id: 0,
|
||||
tooltip: 'A free form text description of the incident.',
|
||||
input_type: 'textarea',
|
||||
hide_notification: false,
|
||||
chosen: false,
|
||||
default_chosen_by_server: false,
|
||||
blank_option: false,
|
||||
internal: true,
|
||||
uuid: '420d70b1-98f9-4681-a20b-84f36a9e5e48',
|
||||
operations: [
|
||||
'equals',
|
||||
'not_equals',
|
||||
'contains',
|
||||
'not_contains',
|
||||
'changed',
|
||||
'changed_to',
|
||||
'not_changed_to',
|
||||
'has_a_value',
|
||||
'not_has_a_value',
|
||||
],
|
||||
operation_perms: {
|
||||
changed_to: {
|
||||
show_in_manual_actions: false,
|
||||
show_in_auto_actions: true,
|
||||
show_in_notifications: true,
|
||||
},
|
||||
has_a_value: {
|
||||
show_in_manual_actions: true,
|
||||
show_in_auto_actions: true,
|
||||
show_in_notifications: true,
|
||||
},
|
||||
not_changed_to: {
|
||||
show_in_manual_actions: false,
|
||||
show_in_auto_actions: true,
|
||||
show_in_notifications: true,
|
||||
},
|
||||
equals: {
|
||||
show_in_manual_actions: true,
|
||||
show_in_auto_actions: true,
|
||||
show_in_notifications: true,
|
||||
},
|
||||
changed: {
|
||||
show_in_manual_actions: false,
|
||||
show_in_auto_actions: true,
|
||||
show_in_notifications: true,
|
||||
},
|
||||
contains: {
|
||||
show_in_manual_actions: true,
|
||||
show_in_auto_actions: true,
|
||||
show_in_notifications: true,
|
||||
},
|
||||
not_contains: {
|
||||
show_in_manual_actions: true,
|
||||
show_in_auto_actions: true,
|
||||
show_in_notifications: true,
|
||||
},
|
||||
not_equals: {
|
||||
show_in_manual_actions: true,
|
||||
show_in_auto_actions: true,
|
||||
show_in_notifications: true,
|
||||
},
|
||||
not_has_a_value: {
|
||||
show_in_manual_actions: true,
|
||||
show_in_auto_actions: true,
|
||||
show_in_notifications: true,
|
||||
},
|
||||
},
|
||||
values: [],
|
||||
perms: {
|
||||
delete: false,
|
||||
modify_name: false,
|
||||
modify_values: false,
|
||||
modify_blank: false,
|
||||
modify_required: false,
|
||||
modify_operations: false,
|
||||
modify_chosen: false,
|
||||
modify_default: false,
|
||||
show_in_manual_actions: true,
|
||||
show_in_auto_actions: true,
|
||||
show_in_notifications: true,
|
||||
show_in_scripts: true,
|
||||
modify_type: ['textarea'],
|
||||
sort: true,
|
||||
},
|
||||
read_only: false,
|
||||
changeable: true,
|
||||
rich_text: true,
|
||||
templates: [],
|
||||
deprecated: false,
|
||||
tags: [],
|
||||
calculated: false,
|
||||
is_tracked: false,
|
||||
allow_default_value: false,
|
||||
},
|
||||
{
|
||||
id: 65,
|
||||
name: 'create_date',
|
||||
text: 'Date Created',
|
||||
prefix: null,
|
||||
type_id: 0,
|
||||
tooltip: 'The date the incident was created. This field is read-only.',
|
||||
input_type: 'datetimepicker',
|
||||
hide_notification: false,
|
||||
chosen: false,
|
||||
default_chosen_by_server: false,
|
||||
blank_option: false,
|
||||
internal: true,
|
||||
uuid: 'b4faf728-881a-4e8b-bf0b-d39b720392a1',
|
||||
operations: ['due_within', 'overdue_by', 'has_a_value', 'not_has_a_value'],
|
||||
operation_perms: {
|
||||
has_a_value: {
|
||||
show_in_manual_actions: true,
|
||||
show_in_auto_actions: true,
|
||||
show_in_notifications: true,
|
||||
},
|
||||
not_has_a_value: {
|
||||
show_in_manual_actions: true,
|
||||
show_in_auto_actions: true,
|
||||
show_in_notifications: true,
|
||||
},
|
||||
due_within: {
|
||||
show_in_manual_actions: true,
|
||||
show_in_auto_actions: true,
|
||||
show_in_notifications: true,
|
||||
},
|
||||
overdue_by: {
|
||||
show_in_manual_actions: true,
|
||||
show_in_auto_actions: true,
|
||||
show_in_notifications: true,
|
||||
},
|
||||
},
|
||||
values: [],
|
||||
perms: {
|
||||
delete: false,
|
||||
modify_name: false,
|
||||
modify_values: false,
|
||||
modify_blank: false,
|
||||
modify_required: false,
|
||||
modify_operations: false,
|
||||
modify_chosen: false,
|
||||
modify_default: false,
|
||||
show_in_manual_actions: true,
|
||||
show_in_auto_actions: true,
|
||||
show_in_notifications: true,
|
||||
show_in_scripts: true,
|
||||
modify_type: ['datetimepicker'],
|
||||
sort: true,
|
||||
},
|
||||
read_only: true,
|
||||
changeable: false,
|
||||
rich_text: false,
|
||||
templates: [],
|
||||
deprecated: false,
|
||||
tags: [],
|
||||
calculated: false,
|
||||
is_tracked: false,
|
||||
allow_default_value: false,
|
||||
},
|
||||
];
|
||||
|
||||
const createMock = (): jest.Mocked<ExternalService> => {
|
||||
const service = {
|
||||
getFields: jest.fn().mockImplementation(() => Promise.resolve(resilientFields)),
|
||||
getIncident: jest.fn().mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
id: '1',
|
||||
name: 'title from ibm resilient',
|
||||
description: 'description from ibm resilient',
|
||||
discovered_date: 1589391874472,
|
||||
create_date: 1591192608323,
|
||||
inc_last_modified_date: 1591192650372,
|
||||
})
|
||||
),
|
||||
createIncident: jest.fn().mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
id: '1',
|
||||
title: '1',
|
||||
pushedDate: '2020-06-03T15:09:13.606Z',
|
||||
url: 'https://resilient.elastic.co/#incidents/1',
|
||||
})
|
||||
),
|
||||
updateIncident: jest.fn().mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
id: '1',
|
||||
title: '1',
|
||||
pushedDate: '2020-06-03T15:09:13.606Z',
|
||||
url: 'https://resilient.elastic.co/#incidents/1',
|
||||
})
|
||||
),
|
||||
createComment: jest.fn(),
|
||||
findIncidents: jest.fn(),
|
||||
getIncidentTypes: jest.fn().mockImplementation(() => [
|
||||
{ id: 17, name: 'Communication error (fax; email)' },
|
||||
{ id: 1001, name: 'Custom type' },
|
||||
]),
|
||||
getSeverity: jest.fn().mockImplementation(() => [
|
||||
{
|
||||
id: 4,
|
||||
name: 'Low',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Medium',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'High',
|
||||
},
|
||||
]),
|
||||
};
|
||||
|
||||
service.createComment.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
commentId: 'case-comment-1',
|
||||
pushedDate: '2020-06-03T15:09:13.606Z',
|
||||
externalCommentId: '1',
|
||||
})
|
||||
);
|
||||
|
||||
service.createComment.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
commentId: 'case-comment-2',
|
||||
pushedDate: '2020-06-03T15:09:13.606Z',
|
||||
externalCommentId: '2',
|
||||
})
|
||||
);
|
||||
|
||||
return service;
|
||||
};
|
||||
|
||||
const externalServiceMock = {
|
||||
create: createMock,
|
||||
};
|
||||
|
||||
const executorParams: ExecutorSubActionPushParams = {
|
||||
incident: {
|
||||
externalId: 'incident-3',
|
||||
name: 'Incident title',
|
||||
description: 'Incident description',
|
||||
incidentTypes: [1001],
|
||||
severityCode: 6,
|
||||
},
|
||||
comments: [
|
||||
export const incidentTypes = {
|
||||
id: 16,
|
||||
name: 'incident_type_ids',
|
||||
text: 'Incident Type',
|
||||
values: [
|
||||
{
|
||||
commentId: 'case-comment-1',
|
||||
comment: 'A comment',
|
||||
value: 17,
|
||||
label: 'Communication error (fax; email)',
|
||||
enabled: true,
|
||||
properties: null,
|
||||
uuid: '4a8d22f7-d89e-4403-85c7-2bafe3b7f2ae',
|
||||
hidden: false,
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
commentId: 'case-comment-2',
|
||||
comment: 'Another comment',
|
||||
value: 1001,
|
||||
label: 'Custom type',
|
||||
enabled: true,
|
||||
properties: null,
|
||||
uuid: '3b51c8c2-9758-48f8-b013-bd141f1d2ec9',
|
||||
hidden: false,
|
||||
default: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const apiParams: PushToServiceApiParams = {
|
||||
...executorParams,
|
||||
};
|
||||
|
||||
const incidentTypes = [
|
||||
{
|
||||
value: 17,
|
||||
label: 'Communication error (fax; email)',
|
||||
enabled: true,
|
||||
properties: null,
|
||||
uuid: '4a8d22f7-d89e-4403-85c7-2bafe3b7f2ae',
|
||||
hidden: false,
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
value: 1001,
|
||||
label: 'Custom type',
|
||||
enabled: true,
|
||||
properties: null,
|
||||
uuid: '3b51c8c2-9758-48f8-b013-bd141f1d2ec9',
|
||||
hidden: false,
|
||||
default: false,
|
||||
},
|
||||
];
|
||||
|
||||
const severity = [
|
||||
export const severity = [
|
||||
{
|
||||
value: 4,
|
||||
label: 'Low',
|
||||
|
@ -421,5 +82,3 @@ const severity = [
|
|||
default: false,
|
||||
},
|
||||
];
|
||||
|
||||
export { externalServiceMock, executorParams, apiParams, incidentTypes, severity };
|
||||
|
|
|
@ -0,0 +1,587 @@
|
|||
/*
|
||||
* 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 { request, createAxiosResponse } from '@kbn/actions-plugin/server/lib/axios_utils';
|
||||
import { loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
import { resilientFields, incidentTypes, severity } from './mocks';
|
||||
import { actionsConfigMock } from '@kbn/actions-plugin/server/actions_config.mock';
|
||||
import { ResilientConnector } from './resilient';
|
||||
import { actionsMock } from '@kbn/actions-plugin/server/mocks';
|
||||
import { RESILIENT_CONNECTOR_ID } from './constants';
|
||||
import { PushToServiceIncidentSchema } from './schema';
|
||||
|
||||
jest.mock('axios');
|
||||
jest.mock('@kbn/actions-plugin/server/lib/axios_utils', () => {
|
||||
const originalUtils = jest.requireActual('@kbn/actions-plugin/server/lib/axios_utils');
|
||||
return {
|
||||
...originalUtils,
|
||||
request: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const requestMock = request as jest.Mock;
|
||||
const TIMESTAMP = 1589391874472;
|
||||
const apiUrl = 'https://resilient.elastic.co/';
|
||||
const orgId = '201';
|
||||
const apiKeyId = 'keyId';
|
||||
const apiKeySecret = 'secret';
|
||||
const ignoredRequestFields = {
|
||||
axios: undefined,
|
||||
timeout: undefined,
|
||||
configurationUtilities: expect.anything(),
|
||||
logger: expect.anything(),
|
||||
};
|
||||
const token = Buffer.from(apiKeyId + ':' + apiKeySecret, 'utf8').toString('base64');
|
||||
const mockIncidentUpdate = (withUpdateError = false) => {
|
||||
requestMock.mockImplementationOnce(() =>
|
||||
createAxiosResponse({
|
||||
data: {
|
||||
id: '1',
|
||||
name: 'title',
|
||||
description: {
|
||||
format: 'html',
|
||||
content: 'description',
|
||||
},
|
||||
incident_type_ids: [1001, 16, 12],
|
||||
severity_code: 6,
|
||||
inc_last_modified_date: 1589391874472,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
if (withUpdateError) {
|
||||
requestMock.mockImplementationOnce(() => {
|
||||
throw new Error('An error has occurred');
|
||||
});
|
||||
} else {
|
||||
requestMock.mockImplementationOnce(() =>
|
||||
createAxiosResponse({
|
||||
data: {
|
||||
success: true,
|
||||
id: '1',
|
||||
inc_last_modified_date: 1589391874472,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
requestMock.mockImplementationOnce(() =>
|
||||
createAxiosResponse({
|
||||
data: {
|
||||
id: '1',
|
||||
name: 'title_updated',
|
||||
description: {
|
||||
format: 'html',
|
||||
content: 'desc_updated',
|
||||
},
|
||||
inc_last_modified_date: 1589391874472,
|
||||
},
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
describe('IBM Resilient connector', () => {
|
||||
const connector = new ResilientConnector(
|
||||
{
|
||||
connector: { id: '1', type: RESILIENT_CONNECTOR_ID },
|
||||
configurationUtilities: actionsConfigMock.create(),
|
||||
logger: loggingSystemMock.createLogger(),
|
||||
services: actionsMock.createServices(),
|
||||
config: { orgId, apiUrl },
|
||||
secrets: { apiKeyId, apiKeySecret },
|
||||
},
|
||||
PushToServiceIncidentSchema
|
||||
);
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.setSystemTime(TIMESTAMP);
|
||||
});
|
||||
|
||||
describe('getIncident', () => {
|
||||
const incidentMock = {
|
||||
id: '1',
|
||||
name: '1',
|
||||
description: {
|
||||
format: 'html',
|
||||
content: 'description',
|
||||
},
|
||||
inc_last_modified_date: TIMESTAMP,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
requestMock.mockImplementation(() =>
|
||||
createAxiosResponse({
|
||||
data: incidentMock,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the incident correctly', async () => {
|
||||
const res = await connector.getIncident({ id: '1' });
|
||||
expect(res).toEqual(incidentMock);
|
||||
});
|
||||
|
||||
it('should call request with correct arguments', async () => {
|
||||
await connector.getIncident({ id: '1' });
|
||||
expect(requestMock).toHaveBeenCalledWith({
|
||||
...ignoredRequestFields,
|
||||
method: 'GET',
|
||||
data: {},
|
||||
url: `${apiUrl}rest/orgs/${orgId}/incidents/1`,
|
||||
headers: {
|
||||
Authorization: `Basic ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
params: {
|
||||
text_content_output_format: 'objects_convert',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('it should throw an error', async () => {
|
||||
requestMock.mockImplementation(() => {
|
||||
throw new Error('An error has occurred');
|
||||
});
|
||||
await expect(connector.getIncident({ id: '1' })).rejects.toThrow(
|
||||
'Unable to get incident with id 1. Error: An error has occurred'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createIncident', () => {
|
||||
const incidentMock = {
|
||||
name: 'title',
|
||||
description: 'desc',
|
||||
incidentTypes: [1001],
|
||||
severityCode: 6,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
requestMock.mockImplementation(() =>
|
||||
createAxiosResponse({
|
||||
data: {
|
||||
id: '1',
|
||||
name: 'title',
|
||||
description: 'description',
|
||||
discovered_date: 1589391874472,
|
||||
create_date: 1589391874472,
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('creates the incident correctly', async () => {
|
||||
const res = await connector.createIncident(incidentMock);
|
||||
|
||||
expect(res).toEqual({
|
||||
title: '1',
|
||||
id: '1',
|
||||
pushedDate: '2020-05-13T17:44:34.472Z',
|
||||
url: 'https://resilient.elastic.co/#incidents/1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should call request with correct arguments', async () => {
|
||||
await connector.createIncident(incidentMock);
|
||||
|
||||
expect(requestMock).toHaveBeenCalledWith({
|
||||
...ignoredRequestFields,
|
||||
method: 'POST',
|
||||
data: {
|
||||
name: 'title',
|
||||
description: {
|
||||
format: 'html',
|
||||
content: 'desc',
|
||||
},
|
||||
discovered_date: TIMESTAMP,
|
||||
incident_type_ids: [{ id: 1001 }],
|
||||
severity_code: { id: 6 },
|
||||
},
|
||||
url: `${apiUrl}rest/orgs/${orgId}/incidents?text_content_output_format=objects_convert`,
|
||||
headers: {
|
||||
Authorization: `Basic ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error', async () => {
|
||||
requestMock.mockImplementation(() => {
|
||||
throw new Error('An error has occurred');
|
||||
});
|
||||
|
||||
await expect(
|
||||
connector.createIncident({
|
||||
name: 'title',
|
||||
description: 'desc',
|
||||
incidentTypes: [1001],
|
||||
severityCode: 6,
|
||||
})
|
||||
).rejects.toThrow(
|
||||
'[Action][IBM Resilient]: Unable to create incident. Error: An error has occurred'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw if the required attributes are not received in response', async () => {
|
||||
requestMock.mockImplementation(() => createAxiosResponse({ data: { notRequired: 'test' } }));
|
||||
|
||||
await expect(connector.createIncident(incidentMock)).rejects.toThrow(
|
||||
'[Action][IBM Resilient]: Unable to create incident. Error: Response validation failed (Error: [id]: expected value of type [number] but got [undefined]).'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateIncident', () => {
|
||||
const req = {
|
||||
incidentId: '1',
|
||||
incident: {
|
||||
name: 'title',
|
||||
description: 'desc',
|
||||
incidentTypes: [1001],
|
||||
severityCode: 6,
|
||||
},
|
||||
};
|
||||
it('updates the incident correctly', async () => {
|
||||
mockIncidentUpdate();
|
||||
const res = await connector.updateIncident(req);
|
||||
|
||||
expect(res).toEqual({
|
||||
title: '1',
|
||||
id: '1',
|
||||
pushedDate: '2020-05-13T17:44:34.472Z',
|
||||
url: 'https://resilient.elastic.co/#incidents/1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should call request with correct arguments', async () => {
|
||||
mockIncidentUpdate();
|
||||
|
||||
await connector.updateIncident({
|
||||
incidentId: '1',
|
||||
incident: {
|
||||
name: 'title_updated',
|
||||
description: 'desc_updated',
|
||||
incidentTypes: [1001],
|
||||
severityCode: 5,
|
||||
},
|
||||
});
|
||||
|
||||
expect(requestMock.mock.calls[1][0]).toEqual({
|
||||
...ignoredRequestFields,
|
||||
url: `${apiUrl}rest/orgs/${orgId}/incidents/1`,
|
||||
headers: {
|
||||
Authorization: `Basic ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'PATCH',
|
||||
data: {
|
||||
changes: [
|
||||
{
|
||||
field: { name: 'name' },
|
||||
old_value: { text: 'title' },
|
||||
new_value: { text: 'title_updated' },
|
||||
},
|
||||
{
|
||||
field: { name: 'description' },
|
||||
old_value: {
|
||||
textarea: {
|
||||
content: 'description',
|
||||
format: 'html',
|
||||
},
|
||||
},
|
||||
new_value: {
|
||||
textarea: {
|
||||
content: 'desc_updated',
|
||||
format: 'html',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
name: 'incident_type_ids',
|
||||
},
|
||||
old_value: {
|
||||
ids: [1001, 16, 12],
|
||||
},
|
||||
new_value: {
|
||||
ids: [1001],
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
name: 'severity_code',
|
||||
},
|
||||
old_value: {
|
||||
id: 6,
|
||||
},
|
||||
new_value: {
|
||||
id: 5,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('it should throw an error', async () => {
|
||||
mockIncidentUpdate(true);
|
||||
|
||||
await expect(connector.updateIncident(req)).rejects.toThrow(
|
||||
'[Action][IBM Resilient]: Unable to update incident with id 1. Error: An error has occurred'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw if the required attributes are not received in response', async () => {
|
||||
requestMock.mockImplementationOnce(() =>
|
||||
createAxiosResponse({
|
||||
data: {
|
||||
id: '1',
|
||||
name: 'title',
|
||||
description: {
|
||||
format: 'html',
|
||||
content: 'description',
|
||||
},
|
||||
incident_type_ids: [1001, 16, 12],
|
||||
severity_code: 6,
|
||||
inc_last_modified_date: 1589391874472,
|
||||
},
|
||||
})
|
||||
);
|
||||
requestMock.mockImplementation(() => createAxiosResponse({ data: { notRequired: 'test' } }));
|
||||
|
||||
await expect(connector.updateIncident(req)).rejects.toThrow(
|
||||
'[Action][IBM Resilient]: Unable to update incident with id 1. Error: Response validation failed (Error: [success]: expected value of type [boolean] but got [undefined]).'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createComment', () => {
|
||||
const req = {
|
||||
incidentId: '1',
|
||||
comment: 'comment',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
requestMock.mockImplementation(() =>
|
||||
createAxiosResponse({
|
||||
data: {
|
||||
id: '1',
|
||||
create_date: 1589391874472,
|
||||
comment: {
|
||||
id: '5',
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should call request with correct arguments', async () => {
|
||||
await connector.addComment(req);
|
||||
|
||||
expect(requestMock).toHaveBeenCalledWith({
|
||||
...ignoredRequestFields,
|
||||
url: `${apiUrl}rest/orgs/${orgId}/incidents/1/comments`,
|
||||
headers: {
|
||||
Authorization: `Basic ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
data: {
|
||||
text: {
|
||||
content: 'comment',
|
||||
format: 'text',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('it should throw an error', async () => {
|
||||
requestMock.mockImplementation(() => {
|
||||
throw new Error('An error has occurred');
|
||||
});
|
||||
|
||||
await expect(connector.addComment(req)).rejects.toThrow(
|
||||
'[Action][IBM Resilient]: Unable to create comment at incident with id 1. Error: An error has occurred.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getIncidentTypes', () => {
|
||||
beforeEach(() => {
|
||||
requestMock.mockImplementation(() =>
|
||||
createAxiosResponse({
|
||||
data: incidentTypes,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should call request with correct arguments', async () => {
|
||||
await connector.getIncidentTypes();
|
||||
expect(requestMock).toBeCalledTimes(1);
|
||||
expect(requestMock).toHaveBeenCalledWith({
|
||||
...ignoredRequestFields,
|
||||
method: 'GET',
|
||||
data: {},
|
||||
url: `${apiUrl}rest/orgs/${orgId}/types/incident/fields/incident_type_ids`,
|
||||
headers: {
|
||||
Authorization: `Basic ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns incident types correctly', async () => {
|
||||
const res = await connector.getIncidentTypes();
|
||||
|
||||
expect(res).toEqual([
|
||||
{ id: '17', name: 'Communication error (fax; email)' },
|
||||
{ id: '1001', name: 'Custom type' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should throw an error', async () => {
|
||||
requestMock.mockImplementation(() => {
|
||||
throw new Error('An error has occurred');
|
||||
});
|
||||
|
||||
await expect(connector.getIncidentTypes()).rejects.toThrow(
|
||||
'[Action][IBM Resilient]: Unable to get incident types. Error: An error has occurred.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw if the required attributes are not received in response', async () => {
|
||||
requestMock.mockImplementation(() =>
|
||||
createAxiosResponse({ data: { id: '1001', name: 'Custom type' } })
|
||||
);
|
||||
|
||||
await expect(connector.getIncidentTypes()).rejects.toThrow(
|
||||
'[Action][IBM Resilient]: Unable to get incident types. Error: Response validation failed (Error: [values]: expected value of type [array] but got [undefined]).'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSeverity', () => {
|
||||
beforeEach(() => {
|
||||
requestMock.mockImplementation(() =>
|
||||
createAxiosResponse({
|
||||
data: {
|
||||
values: severity,
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should call request with correct arguments', async () => {
|
||||
await connector.getSeverity();
|
||||
expect(requestMock).toBeCalledTimes(1);
|
||||
expect(requestMock).toHaveBeenCalledWith({
|
||||
...ignoredRequestFields,
|
||||
method: 'GET',
|
||||
data: {},
|
||||
url: `${apiUrl}rest/orgs/${orgId}/types/incident/fields/severity_code`,
|
||||
headers: {
|
||||
Authorization: `Basic ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns severity correctly', async () => {
|
||||
const res = await connector.getSeverity();
|
||||
|
||||
expect(res).toEqual([
|
||||
{
|
||||
id: '4',
|
||||
name: 'Low',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'Medium',
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
name: 'High',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should throw an error', async () => {
|
||||
requestMock.mockImplementation(() => {
|
||||
throw new Error('An error has occurred');
|
||||
});
|
||||
|
||||
await expect(connector.getSeverity()).rejects.toThrow(
|
||||
'[Action][IBM Resilient]: Unable to get severity. Error: An error has occurred.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw if the required attributes are not received in response', async () => {
|
||||
requestMock.mockImplementation(() =>
|
||||
createAxiosResponse({ data: { id: '10', name: 'Critical' } })
|
||||
);
|
||||
|
||||
await expect(connector.getSeverity()).rejects.toThrow(
|
||||
'[Action][IBM Resilient]: Unable to get severity. Error: Response validation failed (Error: [values]: expected value of type [array] but got [undefined]).'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFields', () => {
|
||||
beforeEach(() => {
|
||||
requestMock.mockImplementation(() =>
|
||||
createAxiosResponse({
|
||||
data: resilientFields,
|
||||
})
|
||||
);
|
||||
});
|
||||
it('should call request with correct arguments', async () => {
|
||||
await connector.getFields();
|
||||
|
||||
expect(requestMock).toBeCalledTimes(1);
|
||||
expect(requestMock).toHaveBeenCalledWith({
|
||||
...ignoredRequestFields,
|
||||
method: 'GET',
|
||||
data: {},
|
||||
url: `${apiUrl}rest/orgs/${orgId}/types/incident/fields`,
|
||||
headers: {
|
||||
Authorization: `Basic ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns common fields correctly', async () => {
|
||||
const res = await connector.getFields();
|
||||
expect(res).toEqual(resilientFields);
|
||||
});
|
||||
|
||||
it('should throw an error', async () => {
|
||||
requestMock.mockImplementation(() => {
|
||||
throw new Error('An error has occurred');
|
||||
});
|
||||
await expect(connector.getFields()).rejects.toThrow(
|
||||
'Unable to get fields. Error: An error has occurred'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw if the required attributes are not received in response', async () => {
|
||||
requestMock.mockImplementation(() => createAxiosResponse({ data: { someField: 'test' } }));
|
||||
|
||||
await expect(connector.getFields()).rejects.toThrow(
|
||||
'[Action][IBM Resilient]: Unable to get fields. Error: Response validation failed (Error: expected value of type [array] but got [Object]).'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,331 @@
|
|||
/*
|
||||
* 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 { AxiosError } from 'axios';
|
||||
import { omitBy, isNil } from 'lodash/fp';
|
||||
import { CaseConnector, ServiceParams } from '@kbn/actions-plugin/server';
|
||||
import { schema, Type } from '@kbn/config-schema';
|
||||
import { getErrorMessage } from '@kbn/actions-plugin/server/lib/axios_utils';
|
||||
import {
|
||||
CreateIncidentData,
|
||||
ExternalServiceIncidentResponse,
|
||||
GetIncidentResponse,
|
||||
GetIncidentTypesResponse,
|
||||
GetSeverityResponse,
|
||||
Incident,
|
||||
ResilientConfig,
|
||||
ResilientSecrets,
|
||||
UpdateIncidentParams,
|
||||
} from './types';
|
||||
import * as i18n from './translations';
|
||||
import { SUB_ACTION } from './constants';
|
||||
import {
|
||||
ExecutorSubActionCommonFieldsParamsSchema,
|
||||
ExecutorSubActionGetIncidentTypesParamsSchema,
|
||||
ExecutorSubActionGetSeverityParamsSchema,
|
||||
GetCommonFieldsResponseSchema,
|
||||
GetIncidentTypesResponseSchema,
|
||||
GetSeverityResponseSchema,
|
||||
GetIncidentResponseSchema,
|
||||
} from './schema';
|
||||
import { formatUpdateRequest } from './utils';
|
||||
|
||||
const VIEW_INCIDENT_URL = `#incidents`;
|
||||
|
||||
export class ResilientConnector extends CaseConnector<
|
||||
ResilientConfig,
|
||||
ResilientSecrets,
|
||||
Incident,
|
||||
GetIncidentResponse
|
||||
> {
|
||||
private urls: {
|
||||
incidentTypes: string;
|
||||
incident: string;
|
||||
comment: string;
|
||||
severity: string;
|
||||
};
|
||||
|
||||
constructor(
|
||||
params: ServiceParams<ResilientConfig, ResilientSecrets>,
|
||||
pushToServiceParamsExtendedSchema: Record<string, Type<unknown>>
|
||||
) {
|
||||
super(params, pushToServiceParamsExtendedSchema);
|
||||
|
||||
this.urls = {
|
||||
incidentTypes: `${this.getIncidentFieldsUrl()}/incident_type_ids`,
|
||||
incident: `${this.getOrgUrl()}/incidents`,
|
||||
comment: `${this.getOrgUrl()}/incidents/{inc_id}/comments`,
|
||||
severity: `${this.getIncidentFieldsUrl()}/severity_code`,
|
||||
};
|
||||
|
||||
this.registerSubActions();
|
||||
}
|
||||
|
||||
protected getResponseErrorMessage(error: AxiosError) {
|
||||
if (!error.response?.status) {
|
||||
return i18n.UNKNOWN_API_ERROR;
|
||||
}
|
||||
if (error.response.status === 401) {
|
||||
return i18n.UNAUTHORIZED_API_ERROR;
|
||||
}
|
||||
return `API Error: ${error.response?.statusText}`;
|
||||
}
|
||||
|
||||
private registerSubActions() {
|
||||
this.registerSubAction({
|
||||
name: SUB_ACTION.INCIDENT_TYPES,
|
||||
method: 'getIncidentTypes',
|
||||
schema: ExecutorSubActionGetIncidentTypesParamsSchema,
|
||||
});
|
||||
|
||||
this.registerSubAction({
|
||||
name: SUB_ACTION.SEVERITY,
|
||||
method: 'getSeverity',
|
||||
schema: ExecutorSubActionGetSeverityParamsSchema,
|
||||
});
|
||||
|
||||
this.registerSubAction({
|
||||
name: SUB_ACTION.FIELDS,
|
||||
method: 'getFields',
|
||||
schema: ExecutorSubActionCommonFieldsParamsSchema,
|
||||
});
|
||||
}
|
||||
|
||||
private getAuthHeaders() {
|
||||
const token = Buffer.from(
|
||||
this.secrets.apiKeyId + ':' + this.secrets.apiKeySecret,
|
||||
'utf8'
|
||||
).toString('base64');
|
||||
|
||||
return { Authorization: `Basic ${token}` };
|
||||
}
|
||||
|
||||
private getOrgUrl() {
|
||||
const { apiUrl: url, orgId } = this.config;
|
||||
|
||||
return `${url}/rest/orgs/${orgId}`;
|
||||
}
|
||||
|
||||
private getIncidentFieldsUrl = () => `${this.getOrgUrl()}/types/incident/fields`;
|
||||
|
||||
private getIncidentViewURL(key: string) {
|
||||
const url = this.config.apiUrl;
|
||||
const urlWithoutTrailingSlash = url.endsWith('/') ? url.slice(0, -1) : url;
|
||||
|
||||
return `${urlWithoutTrailingSlash}/${VIEW_INCIDENT_URL}/${key}`;
|
||||
}
|
||||
|
||||
public async createIncident(incident: Incident): Promise<ExternalServiceIncidentResponse> {
|
||||
try {
|
||||
let data: CreateIncidentData = {
|
||||
name: incident.name,
|
||||
discovered_date: Date.now(),
|
||||
};
|
||||
|
||||
if (incident?.description) {
|
||||
data = {
|
||||
...data,
|
||||
description: {
|
||||
format: 'html',
|
||||
content: incident.description ?? '',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (incident?.incidentTypes) {
|
||||
data = {
|
||||
...data,
|
||||
incident_type_ids: incident.incidentTypes.map((id: number | string) => ({
|
||||
id: Number(id),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
if (incident?.severityCode) {
|
||||
data = {
|
||||
...data,
|
||||
severity_code: { id: Number(incident.severityCode) },
|
||||
};
|
||||
}
|
||||
|
||||
const res = await this.request({
|
||||
url: `${this.urls.incident}?text_content_output_format=objects_convert`,
|
||||
method: 'POST',
|
||||
data,
|
||||
headers: this.getAuthHeaders(),
|
||||
responseSchema: schema.object(
|
||||
{
|
||||
id: schema.number(),
|
||||
create_date: schema.number(),
|
||||
},
|
||||
{ unknowns: 'allow' }
|
||||
),
|
||||
});
|
||||
|
||||
const { id, create_date: createDate } = res.data;
|
||||
|
||||
return {
|
||||
title: `${id}`,
|
||||
id: `${id}`,
|
||||
pushedDate: new Date(createDate).toISOString(),
|
||||
url: this.getIncidentViewURL(id.toString()),
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
getErrorMessage(i18n.NAME, `Unable to create incident. Error: ${error.message}.`)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async updateIncident({
|
||||
incidentId,
|
||||
incident,
|
||||
}: UpdateIncidentParams): Promise<ExternalServiceIncidentResponse> {
|
||||
try {
|
||||
const latestIncident = await this.getIncident({ id: incidentId });
|
||||
|
||||
// Remove null or undefined values. Allowing null values sets the field in IBM Resilient to empty.
|
||||
const newIncident = omitBy(isNil, incident);
|
||||
const data = formatUpdateRequest({ oldIncident: latestIncident, newIncident });
|
||||
|
||||
const res = await this.request({
|
||||
method: 'PATCH',
|
||||
url: `${this.urls.incident}/${incidentId}`,
|
||||
data,
|
||||
headers: this.getAuthHeaders(),
|
||||
responseSchema: schema.object({ success: schema.boolean() }, { unknowns: 'allow' }),
|
||||
});
|
||||
|
||||
if (!res.data.success) {
|
||||
throw new Error('Error while updating incident');
|
||||
}
|
||||
|
||||
const updatedIncident = await this.getIncident({ id: incidentId });
|
||||
|
||||
return {
|
||||
title: `${updatedIncident.id}`,
|
||||
id: `${updatedIncident.id}`,
|
||||
pushedDate: new Date(updatedIncident.inc_last_modified_date).toISOString(),
|
||||
url: this.getIncidentViewURL(updatedIncident.id.toString()),
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
getErrorMessage(
|
||||
i18n.NAME,
|
||||
`Unable to update incident with id ${incidentId}. Error: ${error.message}.`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async addComment({ incidentId, comment }: { incidentId: string; comment: string }) {
|
||||
try {
|
||||
await this.request({
|
||||
method: 'POST',
|
||||
url: this.urls.comment.replace('{inc_id}', incidentId),
|
||||
data: { text: { format: 'text', content: comment } },
|
||||
headers: this.getAuthHeaders(),
|
||||
responseSchema: schema.object({}, { unknowns: 'allow' }),
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
getErrorMessage(
|
||||
i18n.NAME,
|
||||
`Unable to create comment at incident with id ${incidentId}. Error: ${error.message}.`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async getIncident({ id }: { id: string }): Promise<GetIncidentResponse> {
|
||||
try {
|
||||
const res = await this.request({
|
||||
method: 'GET',
|
||||
url: `${this.urls.incident}/${id}`,
|
||||
params: {
|
||||
text_content_output_format: 'objects_convert',
|
||||
},
|
||||
headers: this.getAuthHeaders(),
|
||||
responseSchema: GetIncidentResponseSchema,
|
||||
});
|
||||
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
getErrorMessage(i18n.NAME, `Unable to get incident with id ${id}. Error: ${error.message}.`)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async getIncidentTypes(): Promise<GetIncidentTypesResponse> {
|
||||
try {
|
||||
const res = await this.request({
|
||||
method: 'GET',
|
||||
url: this.urls.incidentTypes,
|
||||
headers: this.getAuthHeaders(),
|
||||
responseSchema: GetIncidentTypesResponseSchema,
|
||||
});
|
||||
|
||||
const incidentTypes = res.data?.values ?? [];
|
||||
|
||||
return incidentTypes.map((type: { value: number; label: string }) => ({
|
||||
id: type.value.toString(),
|
||||
name: type.label,
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
getErrorMessage(i18n.NAME, `Unable to get incident types. Error: ${error.message}.`)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async getSeverity(): Promise<GetSeverityResponse> {
|
||||
try {
|
||||
const res = await this.request({
|
||||
method: 'GET',
|
||||
url: this.urls.severity,
|
||||
headers: this.getAuthHeaders(),
|
||||
responseSchema: GetSeverityResponseSchema,
|
||||
});
|
||||
|
||||
const severities = res.data?.values ?? [];
|
||||
return severities.map((type: { value: number; label: string }) => ({
|
||||
id: type.value.toString(),
|
||||
name: type.label,
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
getErrorMessage(i18n.NAME, `Unable to get severity. Error: ${error.message}.`)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async getFields() {
|
||||
try {
|
||||
const res = await this.request({
|
||||
method: 'GET',
|
||||
url: this.getIncidentFieldsUrl(),
|
||||
headers: this.getAuthHeaders(),
|
||||
responseSchema: GetCommonFieldsResponseSchema,
|
||||
});
|
||||
|
||||
const fields = res.data.map((field) => {
|
||||
return {
|
||||
name: field.name,
|
||||
input_type: field.input_type,
|
||||
read_only: field.read_only,
|
||||
required: field.required,
|
||||
text: field.text,
|
||||
};
|
||||
});
|
||||
|
||||
return fields;
|
||||
} catch (error) {
|
||||
throw new Error(getErrorMessage(i18n.NAME, `Unable to get fields. Error: ${error.message}.`));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -43,39 +43,66 @@ export const ExecutorSubActionPushParamsSchema = schema.object({
|
|||
),
|
||||
});
|
||||
|
||||
export const ExecutorSubActionGetIncidentParamsSchema = schema.object({
|
||||
externalId: schema.string(),
|
||||
});
|
||||
export const PushToServiceIncidentSchema = {
|
||||
name: schema.string(),
|
||||
description: schema.nullable(schema.string()),
|
||||
incidentTypes: schema.nullable(schema.arrayOf(schema.number())),
|
||||
severityCode: schema.nullable(schema.number()),
|
||||
};
|
||||
|
||||
// Reserved for future implementation
|
||||
export const ExecutorSubActionCommonFieldsParamsSchema = schema.object({});
|
||||
export const ExecutorSubActionHandshakeParamsSchema = schema.object({});
|
||||
export const ExecutorSubActionGetIncidentTypesParamsSchema = schema.object({});
|
||||
export const ExecutorSubActionGetSeverityParamsSchema = schema.object({});
|
||||
|
||||
export const ExecutorParamsSchema = schema.oneOf([
|
||||
schema.object({
|
||||
subAction: schema.literal('getFields'),
|
||||
subActionParams: ExecutorSubActionCommonFieldsParamsSchema,
|
||||
}),
|
||||
schema.object({
|
||||
subAction: schema.literal('getIncident'),
|
||||
subActionParams: ExecutorSubActionGetIncidentParamsSchema,
|
||||
}),
|
||||
schema.object({
|
||||
subAction: schema.literal('handshake'),
|
||||
subActionParams: ExecutorSubActionHandshakeParamsSchema,
|
||||
}),
|
||||
schema.object({
|
||||
subAction: schema.literal('pushToService'),
|
||||
subActionParams: ExecutorSubActionPushParamsSchema,
|
||||
}),
|
||||
schema.object({
|
||||
subAction: schema.literal('incidentTypes'),
|
||||
subActionParams: ExecutorSubActionGetIncidentTypesParamsSchema,
|
||||
}),
|
||||
schema.object({
|
||||
subAction: schema.literal('severity'),
|
||||
subActionParams: ExecutorSubActionGetSeverityParamsSchema,
|
||||
}),
|
||||
]);
|
||||
const ArrayOfValuesSchema = schema.arrayOf(
|
||||
schema.object(
|
||||
{
|
||||
value: schema.number(),
|
||||
label: schema.string(),
|
||||
},
|
||||
{ unknowns: 'allow' }
|
||||
)
|
||||
);
|
||||
|
||||
export const GetIncidentTypesResponseSchema = schema.object(
|
||||
{
|
||||
values: ArrayOfValuesSchema,
|
||||
},
|
||||
{ unknowns: 'allow' }
|
||||
);
|
||||
|
||||
export const GetSeverityResponseSchema = schema.object(
|
||||
{
|
||||
values: ArrayOfValuesSchema,
|
||||
},
|
||||
{ unknowns: 'allow' }
|
||||
);
|
||||
|
||||
export const ExternalServiceFieldsSchema = schema.object(
|
||||
{
|
||||
input_type: schema.string(),
|
||||
name: schema.string(),
|
||||
read_only: schema.boolean(),
|
||||
required: schema.nullable(schema.string()),
|
||||
text: schema.string(),
|
||||
},
|
||||
{ unknowns: 'allow' }
|
||||
);
|
||||
|
||||
export const GetCommonFieldsResponseSchema = schema.arrayOf(ExternalServiceFieldsSchema);
|
||||
|
||||
export const ExternalServiceIncidentResponseSchema = schema.object({
|
||||
id: schema.string(),
|
||||
title: schema.string(),
|
||||
url: schema.string(),
|
||||
pushedDate: schema.string(),
|
||||
});
|
||||
|
||||
export const GetIncidentResponseSchema = schema.object(
|
||||
{
|
||||
id: schema.number(),
|
||||
inc_last_modified_date: schema.number(),
|
||||
},
|
||||
{ unknowns: 'allow' }
|
||||
);
|
||||
|
|
|
@ -1,714 +0,0 @@
|
|||
/*
|
||||
* 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 axios from 'axios';
|
||||
|
||||
import { createExternalService, getValueTextContent, formatUpdateRequest } from './service';
|
||||
import { request, createAxiosResponse } from '@kbn/actions-plugin/server/lib/axios_utils';
|
||||
import { ExternalService } from './types';
|
||||
import { Logger } from '@kbn/core/server';
|
||||
import { loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
import { incidentTypes, resilientFields, severity } from './mocks';
|
||||
import { actionsConfigMock } from '@kbn/actions-plugin/server/actions_config.mock';
|
||||
|
||||
const logger = loggingSystemMock.create().get() as jest.Mocked<Logger>;
|
||||
|
||||
jest.mock('axios');
|
||||
jest.mock('@kbn/actions-plugin/server/lib/axios_utils', () => {
|
||||
const originalUtils = jest.requireActual('@kbn/actions-plugin/server/lib/axios_utils');
|
||||
return {
|
||||
...originalUtils,
|
||||
request: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
axios.create = jest.fn(() => axios);
|
||||
const requestMock = request as jest.Mock;
|
||||
const now = Date.now;
|
||||
const TIMESTAMP = 1589391874472;
|
||||
const configurationUtilities = actionsConfigMock.create();
|
||||
|
||||
// Incident update makes three calls to the API.
|
||||
// The function below mocks this calls.
|
||||
// a) Get the latest incident
|
||||
// b) Update the incident
|
||||
// c) Get the updated incident
|
||||
const mockIncidentUpdate = (withUpdateError = false) => {
|
||||
requestMock.mockImplementationOnce(() =>
|
||||
createAxiosResponse({
|
||||
data: {
|
||||
id: '1',
|
||||
name: 'title',
|
||||
description: {
|
||||
format: 'html',
|
||||
content: 'description',
|
||||
},
|
||||
incident_type_ids: [1001, 16, 12],
|
||||
severity_code: 6,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
if (withUpdateError) {
|
||||
requestMock.mockImplementationOnce(() => {
|
||||
throw new Error('An error has occurred');
|
||||
});
|
||||
} else {
|
||||
requestMock.mockImplementationOnce(() =>
|
||||
createAxiosResponse({
|
||||
data: {
|
||||
success: true,
|
||||
id: '1',
|
||||
inc_last_modified_date: 1589391874472,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
requestMock.mockImplementationOnce(() =>
|
||||
createAxiosResponse({
|
||||
data: {
|
||||
id: '1',
|
||||
name: 'title_updated',
|
||||
description: {
|
||||
format: 'html',
|
||||
content: 'desc_updated',
|
||||
},
|
||||
inc_last_modified_date: 1589391874472,
|
||||
},
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
describe('IBM Resilient service', () => {
|
||||
let service: ExternalService;
|
||||
|
||||
beforeAll(() => {
|
||||
service = createExternalService(
|
||||
{
|
||||
// The trailing slash at the end of the url is intended.
|
||||
// All API calls need to have the trailing slash removed.
|
||||
config: { apiUrl: 'https://resilient.elastic.co/', orgId: '201' },
|
||||
secrets: { apiKeyId: 'keyId', apiKeySecret: 'secret' },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
Date.now = now;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
Date.now = jest.fn().mockReturnValue(TIMESTAMP);
|
||||
});
|
||||
|
||||
describe('getValueTextContent', () => {
|
||||
test('transforms correctly', () => {
|
||||
expect(getValueTextContent('name', 'title')).toEqual({
|
||||
text: 'title',
|
||||
});
|
||||
});
|
||||
|
||||
test('transforms correctly the description', () => {
|
||||
expect(getValueTextContent('description', 'desc')).toEqual({
|
||||
textarea: {
|
||||
format: 'html',
|
||||
content: 'desc',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatUpdateRequest', () => {
|
||||
test('transforms correctly', () => {
|
||||
const oldIncident = { name: 'title', description: 'desc' };
|
||||
const newIncident = { name: 'title_updated', description: 'desc_updated' };
|
||||
expect(formatUpdateRequest({ oldIncident, newIncident })).toEqual({
|
||||
changes: [
|
||||
{
|
||||
field: { name: 'name' },
|
||||
old_value: { text: 'title' },
|
||||
new_value: { text: 'title_updated' },
|
||||
},
|
||||
{
|
||||
field: { name: 'description' },
|
||||
old_value: {
|
||||
textarea: {
|
||||
format: 'html',
|
||||
content: 'desc',
|
||||
},
|
||||
},
|
||||
new_value: {
|
||||
textarea: {
|
||||
format: 'html',
|
||||
content: 'desc_updated',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createExternalService', () => {
|
||||
test('throws without url', () => {
|
||||
expect(() =>
|
||||
createExternalService(
|
||||
{
|
||||
config: { apiUrl: null, orgId: '201' },
|
||||
secrets: { apiKeyId: 'token', apiKeySecret: 'secret' },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
)
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
test('throws without orgId', () => {
|
||||
expect(() =>
|
||||
createExternalService(
|
||||
{
|
||||
config: { apiUrl: 'test.com', orgId: null },
|
||||
secrets: { apiKeyId: 'token', apiKeySecret: 'secret' },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
)
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
test('throws without username', () => {
|
||||
expect(() =>
|
||||
createExternalService(
|
||||
{
|
||||
config: { apiUrl: 'test.com', orgId: '201' },
|
||||
secrets: { apiKeyId: '', apiKeySecret: 'secret' },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
)
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
test('throws without password', () => {
|
||||
expect(() =>
|
||||
createExternalService(
|
||||
{
|
||||
config: { apiUrl: 'test.com', orgId: '201' },
|
||||
secrets: { apiKeyId: '', apiKeySecret: undefined },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
)
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getIncident', () => {
|
||||
test('it returns the incident correctly', async () => {
|
||||
requestMock.mockImplementation(() =>
|
||||
createAxiosResponse({
|
||||
data: {
|
||||
id: '1',
|
||||
name: '1',
|
||||
description: {
|
||||
format: 'html',
|
||||
content: 'description',
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
const res = await service.getIncident('1');
|
||||
expect(res).toEqual({ id: '1', name: '1', description: 'description' });
|
||||
});
|
||||
|
||||
test('it should call request with correct arguments', async () => {
|
||||
requestMock.mockImplementation(() =>
|
||||
createAxiosResponse({
|
||||
data: { id: '1' },
|
||||
})
|
||||
);
|
||||
|
||||
await service.getIncident('1');
|
||||
expect(requestMock).toHaveBeenCalledWith({
|
||||
axios,
|
||||
logger,
|
||||
url: 'https://resilient.elastic.co/rest/orgs/201/incidents/1',
|
||||
params: {
|
||||
text_content_output_format: 'objects_convert',
|
||||
},
|
||||
configurationUtilities,
|
||||
});
|
||||
});
|
||||
|
||||
test('it should throw an error', async () => {
|
||||
requestMock.mockImplementation(() => {
|
||||
throw new Error('An error has occurred');
|
||||
});
|
||||
await expect(service.getIncident('1')).rejects.toThrow(
|
||||
'Unable to get incident with id 1. Error: An error has occurred'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should throw if the request is not a JSON', async () => {
|
||||
requestMock.mockImplementation(() =>
|
||||
createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } })
|
||||
);
|
||||
|
||||
await expect(service.getIncident('1')).rejects.toThrow(
|
||||
'[Action][IBM Resilient]: Unable to get incident with id 1. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createIncident', () => {
|
||||
const incident = {
|
||||
incident: {
|
||||
name: 'title',
|
||||
description: 'desc',
|
||||
incidentTypes: [1001],
|
||||
severityCode: 6,
|
||||
},
|
||||
};
|
||||
|
||||
test('it creates the incident correctly', async () => {
|
||||
requestMock.mockImplementation(() =>
|
||||
createAxiosResponse({
|
||||
data: {
|
||||
id: '1',
|
||||
name: 'title',
|
||||
description: 'description',
|
||||
discovered_date: 1589391874472,
|
||||
create_date: 1589391874472,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const res = await service.createIncident(incident);
|
||||
|
||||
expect(res).toEqual({
|
||||
title: '1',
|
||||
id: '1',
|
||||
pushedDate: '2020-05-13T17:44:34.472Z',
|
||||
url: 'https://resilient.elastic.co/#incidents/1',
|
||||
});
|
||||
});
|
||||
|
||||
test('it should call request with correct arguments', async () => {
|
||||
requestMock.mockImplementation(() =>
|
||||
createAxiosResponse({
|
||||
data: {
|
||||
id: '1',
|
||||
name: 'title',
|
||||
description: 'description',
|
||||
discovered_date: 1589391874472,
|
||||
create_date: 1589391874472,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
await service.createIncident(incident);
|
||||
|
||||
expect(requestMock).toHaveBeenCalledWith({
|
||||
axios,
|
||||
url: 'https://resilient.elastic.co/rest/orgs/201/incidents?text_content_output_format=objects_convert',
|
||||
logger,
|
||||
method: 'post',
|
||||
configurationUtilities,
|
||||
data: {
|
||||
name: 'title',
|
||||
description: {
|
||||
format: 'html',
|
||||
content: 'desc',
|
||||
},
|
||||
discovered_date: TIMESTAMP,
|
||||
incident_type_ids: [{ id: 1001 }],
|
||||
severity_code: { id: 6 },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('it should throw an error', async () => {
|
||||
requestMock.mockImplementation(() => {
|
||||
throw new Error('An error has occurred');
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.createIncident({
|
||||
incident: {
|
||||
name: 'title',
|
||||
description: 'desc',
|
||||
incidentTypes: [1001],
|
||||
severityCode: 6,
|
||||
},
|
||||
})
|
||||
).rejects.toThrow(
|
||||
'[Action][IBM Resilient]: Unable to create incident. Error: An error has occurred'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should throw if the request is not a JSON', async () => {
|
||||
requestMock.mockImplementation(() =>
|
||||
createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } })
|
||||
);
|
||||
|
||||
await expect(service.createIncident(incident)).rejects.toThrow(
|
||||
'[Action][IBM Resilient]: Unable to create incident. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json.'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should throw if the required attributes are not there', async () => {
|
||||
requestMock.mockImplementation(() => createAxiosResponse({ data: { notRequired: 'test' } }));
|
||||
|
||||
await expect(service.createIncident(incident)).rejects.toThrow(
|
||||
'[Action][IBM Resilient]: Unable to create incident. Error: Response is missing at least one of the expected fields: id,create_date.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateIncident', () => {
|
||||
const req = {
|
||||
incidentId: '1',
|
||||
incident: {
|
||||
name: 'title',
|
||||
description: 'desc',
|
||||
incidentTypes: [1001],
|
||||
severityCode: 6,
|
||||
},
|
||||
};
|
||||
test('it updates the incident correctly', async () => {
|
||||
mockIncidentUpdate();
|
||||
const res = await service.updateIncident(req);
|
||||
|
||||
expect(res).toEqual({
|
||||
title: '1',
|
||||
id: '1',
|
||||
pushedDate: '2020-05-13T17:44:34.472Z',
|
||||
url: 'https://resilient.elastic.co/#incidents/1',
|
||||
});
|
||||
});
|
||||
|
||||
test('it should call request with correct arguments', async () => {
|
||||
mockIncidentUpdate();
|
||||
|
||||
await service.updateIncident({
|
||||
incidentId: '1',
|
||||
incident: {
|
||||
name: 'title_updated',
|
||||
description: 'desc_updated',
|
||||
incidentTypes: [1001],
|
||||
severityCode: 5,
|
||||
},
|
||||
});
|
||||
|
||||
// Incident update makes three calls to the API.
|
||||
// The second call to the API is the update call.
|
||||
expect(requestMock.mock.calls[1][0]).toEqual({
|
||||
axios,
|
||||
logger,
|
||||
method: 'patch',
|
||||
configurationUtilities,
|
||||
url: 'https://resilient.elastic.co/rest/orgs/201/incidents/1',
|
||||
data: {
|
||||
changes: [
|
||||
{
|
||||
field: { name: 'name' },
|
||||
old_value: { text: 'title' },
|
||||
new_value: { text: 'title_updated' },
|
||||
},
|
||||
{
|
||||
field: { name: 'description' },
|
||||
old_value: {
|
||||
textarea: {
|
||||
content: 'description',
|
||||
format: 'html',
|
||||
},
|
||||
},
|
||||
new_value: {
|
||||
textarea: {
|
||||
content: 'desc_updated',
|
||||
format: 'html',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
name: 'incident_type_ids',
|
||||
},
|
||||
old_value: {
|
||||
ids: [1001, 16, 12],
|
||||
},
|
||||
new_value: {
|
||||
ids: [1001],
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
name: 'severity_code',
|
||||
},
|
||||
old_value: {
|
||||
id: 6,
|
||||
},
|
||||
new_value: {
|
||||
id: 5,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('it should throw an error', async () => {
|
||||
mockIncidentUpdate(true);
|
||||
|
||||
await expect(service.updateIncident(req)).rejects.toThrow(
|
||||
'[Action][IBM Resilient]: Unable to update incident with id 1. Error: An error has occurred'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should throw if the request is not a JSON', async () => {
|
||||
// get incident request
|
||||
requestMock.mockImplementationOnce(() =>
|
||||
createAxiosResponse({
|
||||
data: {
|
||||
id: '1',
|
||||
name: 'title',
|
||||
description: {
|
||||
format: 'html',
|
||||
content: 'description',
|
||||
},
|
||||
incident_type_ids: [1001, 16, 12],
|
||||
severity_code: 6,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
// update incident request
|
||||
requestMock.mockImplementation(() =>
|
||||
createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } })
|
||||
);
|
||||
|
||||
await expect(service.updateIncident(req)).rejects.toThrow(
|
||||
'[Action][IBM Resilient]: Unable to update incident with id 1. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createComment', () => {
|
||||
const req = {
|
||||
incidentId: '1',
|
||||
comment: {
|
||||
comment: 'comment',
|
||||
commentId: 'comment-1',
|
||||
},
|
||||
};
|
||||
|
||||
test('it creates the comment correctly', async () => {
|
||||
requestMock.mockImplementation(() =>
|
||||
createAxiosResponse({
|
||||
data: {
|
||||
id: '1',
|
||||
create_date: 1589391874472,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const res = await service.createComment(req);
|
||||
|
||||
expect(res).toEqual({
|
||||
commentId: 'comment-1',
|
||||
pushedDate: '2020-05-13T17:44:34.472Z',
|
||||
externalCommentId: '1',
|
||||
});
|
||||
});
|
||||
|
||||
test('it should call request with correct arguments', async () => {
|
||||
requestMock.mockImplementation(() =>
|
||||
createAxiosResponse({
|
||||
data: {
|
||||
id: '1',
|
||||
create_date: 1589391874472,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
await service.createComment(req);
|
||||
|
||||
expect(requestMock).toHaveBeenCalledWith({
|
||||
axios,
|
||||
logger,
|
||||
method: 'post',
|
||||
configurationUtilities,
|
||||
url: 'https://resilient.elastic.co/rest/orgs/201/incidents/1/comments',
|
||||
data: {
|
||||
text: {
|
||||
content: 'comment',
|
||||
format: 'text',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('it should throw an error', async () => {
|
||||
requestMock.mockImplementation(() => {
|
||||
throw new Error('An error has occurred');
|
||||
});
|
||||
|
||||
await expect(service.createComment(req)).rejects.toThrow(
|
||||
'[Action][IBM Resilient]: Unable to create comment at incident with id 1. Error: An error has occurred'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should throw if the request is not a JSON', async () => {
|
||||
requestMock.mockImplementation(() =>
|
||||
createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } })
|
||||
);
|
||||
|
||||
await expect(service.createComment(req)).rejects.toThrow(
|
||||
'[Action][IBM Resilient]: Unable to create comment at incident with id 1. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getIncidentTypes', () => {
|
||||
test('it creates the incident correctly', async () => {
|
||||
requestMock.mockImplementation(() =>
|
||||
createAxiosResponse({
|
||||
data: {
|
||||
values: incidentTypes,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const res = await service.getIncidentTypes();
|
||||
|
||||
expect(res).toEqual([
|
||||
{ id: 17, name: 'Communication error (fax; email)' },
|
||||
{ id: 1001, name: 'Custom type' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('it should throw an error', async () => {
|
||||
requestMock.mockImplementation(() => {
|
||||
throw new Error('An error has occurred');
|
||||
});
|
||||
|
||||
await expect(service.getIncidentTypes()).rejects.toThrow(
|
||||
'[Action][IBM Resilient]: Unable to get incident types. Error: An error has occurred.'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should throw if the request is not a JSON', async () => {
|
||||
requestMock.mockImplementation(() =>
|
||||
createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } })
|
||||
);
|
||||
|
||||
await expect(service.getIncidentTypes()).rejects.toThrow(
|
||||
'[Action][IBM Resilient]: Unable to get incident types. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSeverity', () => {
|
||||
test('it creates the incident correctly', async () => {
|
||||
requestMock.mockImplementation(() =>
|
||||
createAxiosResponse({
|
||||
data: {
|
||||
values: severity,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const res = await service.getSeverity();
|
||||
|
||||
expect(res).toEqual([
|
||||
{
|
||||
id: 4,
|
||||
name: 'Low',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Medium',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'High',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('it should throw an error', async () => {
|
||||
requestMock.mockImplementation(() => {
|
||||
throw new Error('An error has occurred');
|
||||
});
|
||||
|
||||
await expect(service.getSeverity()).rejects.toThrow(
|
||||
'[Action][IBM Resilient]: Unable to get severity. Error: An error has occurred.'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should throw if the request is not a JSON', async () => {
|
||||
requestMock.mockImplementation(() =>
|
||||
createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } })
|
||||
);
|
||||
|
||||
await expect(service.getSeverity()).rejects.toThrow(
|
||||
'[Action][IBM Resilient]: Unable to get severity. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFields', () => {
|
||||
test('it should call request with correct arguments', async () => {
|
||||
requestMock.mockImplementation(() =>
|
||||
createAxiosResponse({
|
||||
data: resilientFields,
|
||||
})
|
||||
);
|
||||
await service.getFields();
|
||||
|
||||
expect(requestMock).toHaveBeenCalledWith({
|
||||
axios,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
url: 'https://resilient.elastic.co/rest/orgs/201/types/incident/fields',
|
||||
});
|
||||
});
|
||||
|
||||
test('it returns common fields correctly', async () => {
|
||||
requestMock.mockImplementation(() =>
|
||||
createAxiosResponse({
|
||||
data: resilientFields,
|
||||
})
|
||||
);
|
||||
const res = await service.getFields();
|
||||
expect(res).toEqual(resilientFields);
|
||||
});
|
||||
|
||||
test('it should throw an error', async () => {
|
||||
requestMock.mockImplementation(() => {
|
||||
throw new Error('An error has occurred');
|
||||
});
|
||||
await expect(service.getFields()).rejects.toThrow(
|
||||
'Unable to get fields. Error: An error has occurred'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should throw if the request is not a JSON', async () => {
|
||||
requestMock.mockImplementation(() =>
|
||||
createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } })
|
||||
);
|
||||
|
||||
await expect(service.getFields()).rejects.toThrow(
|
||||
'[Action][IBM Resilient]: Unable to get fields. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json.'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,364 +0,0 @@
|
|||
/*
|
||||
* 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 axios from 'axios';
|
||||
import { omitBy, isNil } from 'lodash/fp';
|
||||
|
||||
import { Logger } from '@kbn/core/server';
|
||||
import {
|
||||
getErrorMessage,
|
||||
request,
|
||||
throwIfResponseIsNotValid,
|
||||
} from '@kbn/actions-plugin/server/lib/axios_utils';
|
||||
import { ActionsConfigurationUtilities } from '@kbn/actions-plugin/server/actions_config';
|
||||
import {
|
||||
ExternalServiceCredentials,
|
||||
ExternalService,
|
||||
ExternalServiceParams,
|
||||
CreateCommentParams,
|
||||
UpdateIncidentParams,
|
||||
CreateIncidentParams,
|
||||
CreateIncidentData,
|
||||
ResilientPublicConfigurationType,
|
||||
ResilientSecretConfigurationType,
|
||||
UpdateIncidentRequest,
|
||||
GetValueTextContentResponse,
|
||||
} from './types';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
const VIEW_INCIDENT_URL = `#incidents`;
|
||||
|
||||
export const getValueTextContent = (
|
||||
field: string,
|
||||
value: string | number | number[]
|
||||
): GetValueTextContentResponse => {
|
||||
if (field === 'description') {
|
||||
return {
|
||||
textarea: {
|
||||
format: 'html',
|
||||
content: value as string,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (field === 'incidentTypes') {
|
||||
return {
|
||||
ids: value as number[],
|
||||
};
|
||||
}
|
||||
|
||||
if (field === 'severityCode') {
|
||||
return {
|
||||
id: value as number,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
text: value as string,
|
||||
};
|
||||
};
|
||||
|
||||
export const formatUpdateRequest = ({
|
||||
oldIncident,
|
||||
newIncident,
|
||||
}: ExternalServiceParams): UpdateIncidentRequest => {
|
||||
return {
|
||||
changes: Object.keys(newIncident as Record<string, unknown>).map((key) => {
|
||||
let name = key;
|
||||
|
||||
if (key === 'incidentTypes') {
|
||||
name = 'incident_type_ids';
|
||||
}
|
||||
|
||||
if (key === 'severityCode') {
|
||||
name = 'severity_code';
|
||||
}
|
||||
|
||||
return {
|
||||
field: { name },
|
||||
// TODO: Fix ugly casting
|
||||
old_value: getValueTextContent(
|
||||
key,
|
||||
(oldIncident as Record<string, unknown>)[name] as string
|
||||
),
|
||||
new_value: getValueTextContent(
|
||||
key,
|
||||
(newIncident as Record<string, unknown>)[key] as string
|
||||
),
|
||||
};
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
export const createExternalService = (
|
||||
{ config, secrets }: ExternalServiceCredentials,
|
||||
logger: Logger,
|
||||
configurationUtilities: ActionsConfigurationUtilities
|
||||
): ExternalService => {
|
||||
const { apiUrl: url, orgId } = config as ResilientPublicConfigurationType;
|
||||
const { apiKeyId, apiKeySecret } = secrets as ResilientSecretConfigurationType;
|
||||
|
||||
if (!url || !orgId || !apiKeyId || !apiKeySecret) {
|
||||
throw Error(`[Action]${i18n.NAME}: Wrong configuration.`);
|
||||
}
|
||||
|
||||
const urlWithoutTrailingSlash = url.endsWith('/') ? url.slice(0, -1) : url;
|
||||
const orgUrl = `${urlWithoutTrailingSlash}/rest/orgs/${orgId}`;
|
||||
const incidentUrl = `${orgUrl}/incidents`;
|
||||
const commentUrl = `${incidentUrl}/{inc_id}/comments`;
|
||||
const incidentFieldsUrl = `${orgUrl}/types/incident/fields`;
|
||||
const incidentTypesUrl = `${incidentFieldsUrl}/incident_type_ids`;
|
||||
const severityUrl = `${incidentFieldsUrl}/severity_code`;
|
||||
const axiosInstance = axios.create({
|
||||
auth: { username: apiKeyId, password: apiKeySecret },
|
||||
});
|
||||
|
||||
const getIncidentViewURL = (key: string) => {
|
||||
return `${urlWithoutTrailingSlash}/${VIEW_INCIDENT_URL}/${key}`;
|
||||
};
|
||||
|
||||
const getCommentsURL = (incidentId: string) => {
|
||||
return commentUrl.replace('{inc_id}', incidentId);
|
||||
};
|
||||
|
||||
const getIncident = async (id: string) => {
|
||||
try {
|
||||
const res = await request({
|
||||
axios: axiosInstance,
|
||||
url: `${incidentUrl}/${id}`,
|
||||
logger,
|
||||
params: {
|
||||
text_content_output_format: 'objects_convert',
|
||||
},
|
||||
configurationUtilities,
|
||||
});
|
||||
|
||||
throwIfResponseIsNotValid({
|
||||
res,
|
||||
});
|
||||
|
||||
return { ...res.data, description: res.data.description?.content ?? '' };
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
getErrorMessage(i18n.NAME, `Unable to get incident with id ${id}. Error: ${error.message}.`)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const createIncident = async ({ incident }: CreateIncidentParams) => {
|
||||
let data: CreateIncidentData = {
|
||||
name: incident.name,
|
||||
discovered_date: Date.now(),
|
||||
};
|
||||
|
||||
if (incident.description) {
|
||||
data = {
|
||||
...data,
|
||||
description: {
|
||||
format: 'html',
|
||||
content: incident.description ?? '',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (incident.incidentTypes) {
|
||||
data = {
|
||||
...data,
|
||||
incident_type_ids: incident.incidentTypes.map((id) => ({ id })),
|
||||
};
|
||||
}
|
||||
|
||||
if (incident.severityCode) {
|
||||
data = {
|
||||
...data,
|
||||
severity_code: { id: incident.severityCode },
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await request({
|
||||
axios: axiosInstance,
|
||||
url: `${incidentUrl}?text_content_output_format=objects_convert`,
|
||||
method: 'post',
|
||||
logger,
|
||||
data,
|
||||
configurationUtilities,
|
||||
});
|
||||
|
||||
throwIfResponseIsNotValid({
|
||||
res,
|
||||
requiredAttributesToBeInTheResponse: ['id', 'create_date'],
|
||||
});
|
||||
|
||||
return {
|
||||
title: `${res.data.id}`,
|
||||
id: `${res.data.id}`,
|
||||
pushedDate: new Date(res.data.create_date).toISOString(),
|
||||
url: getIncidentViewURL(res.data.id),
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
getErrorMessage(i18n.NAME, `Unable to create incident. Error: ${error.message}.`)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const updateIncident = async ({ incidentId, incident }: UpdateIncidentParams) => {
|
||||
try {
|
||||
const latestIncident = await getIncident(incidentId);
|
||||
|
||||
// Remove null or undefined values. Allowing null values sets the field in IBM Resilient to empty.
|
||||
const newIncident = omitBy(isNil, incident);
|
||||
const data = formatUpdateRequest({ oldIncident: latestIncident, newIncident });
|
||||
|
||||
const res = await request({
|
||||
axios: axiosInstance,
|
||||
method: 'patch',
|
||||
url: `${incidentUrl}/${incidentId}`,
|
||||
logger,
|
||||
data,
|
||||
configurationUtilities,
|
||||
});
|
||||
|
||||
throwIfResponseIsNotValid({
|
||||
res,
|
||||
});
|
||||
|
||||
if (!res.data.success) {
|
||||
throw new Error(res.data.message);
|
||||
}
|
||||
|
||||
const updatedIncident = await getIncident(incidentId);
|
||||
|
||||
return {
|
||||
title: `${updatedIncident.id}`,
|
||||
id: `${updatedIncident.id}`,
|
||||
pushedDate: new Date(updatedIncident.inc_last_modified_date).toISOString(),
|
||||
url: getIncidentViewURL(updatedIncident.id),
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
getErrorMessage(
|
||||
i18n.NAME,
|
||||
`Unable to update incident with id ${incidentId}. Error: ${error.message}`
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const createComment = async ({ incidentId, comment }: CreateCommentParams) => {
|
||||
try {
|
||||
const res = await request({
|
||||
axios: axiosInstance,
|
||||
method: 'post',
|
||||
url: getCommentsURL(incidentId),
|
||||
logger,
|
||||
data: { text: { format: 'text', content: comment.comment } },
|
||||
configurationUtilities,
|
||||
});
|
||||
|
||||
throwIfResponseIsNotValid({
|
||||
res,
|
||||
});
|
||||
|
||||
return {
|
||||
commentId: comment.commentId,
|
||||
externalCommentId: res.data.id,
|
||||
pushedDate: new Date(res.data.create_date).toISOString(),
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
getErrorMessage(
|
||||
i18n.NAME,
|
||||
`Unable to create comment at incident with id ${incidentId}. Error: ${error.message}.`
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const getIncidentTypes = async () => {
|
||||
try {
|
||||
const res = await request({
|
||||
axios: axiosInstance,
|
||||
method: 'get',
|
||||
url: incidentTypesUrl,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
});
|
||||
|
||||
throwIfResponseIsNotValid({
|
||||
res,
|
||||
});
|
||||
|
||||
const incidentTypes = res.data?.values ?? [];
|
||||
return incidentTypes.map((type: { value: string; label: string }) => ({
|
||||
id: type.value,
|
||||
name: type.label,
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
getErrorMessage(i18n.NAME, `Unable to get incident types. Error: ${error.message}.`)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const getSeverity = async () => {
|
||||
try {
|
||||
const res = await request({
|
||||
axios: axiosInstance,
|
||||
method: 'get',
|
||||
url: severityUrl,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
});
|
||||
|
||||
throwIfResponseIsNotValid({
|
||||
res,
|
||||
});
|
||||
|
||||
const incidentTypes = res.data?.values ?? [];
|
||||
return incidentTypes.map((type: { value: string; label: string }) => ({
|
||||
id: type.value,
|
||||
name: type.label,
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
getErrorMessage(i18n.NAME, `Unable to get severity. Error: ${error.message}.`)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const getFields = async () => {
|
||||
try {
|
||||
const res = await request({
|
||||
axios: axiosInstance,
|
||||
url: incidentFieldsUrl,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
});
|
||||
|
||||
throwIfResponseIsNotValid({
|
||||
res,
|
||||
});
|
||||
|
||||
return res.data ?? [];
|
||||
} catch (error) {
|
||||
throw new Error(getErrorMessage(i18n.NAME, `Unable to get fields. Error: ${error.message}.`));
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
createComment,
|
||||
createIncident,
|
||||
getFields,
|
||||
getIncident,
|
||||
getIncidentTypes,
|
||||
getSeverity,
|
||||
updateIncident,
|
||||
};
|
||||
};
|
|
@ -18,3 +18,14 @@ export const ALLOWED_HOSTS_ERROR = (message: string) =>
|
|||
message,
|
||||
},
|
||||
});
|
||||
|
||||
export const UNKNOWN_API_ERROR = i18n.translate('xpack.stackConnectors.resilient.unknownError', {
|
||||
defaultMessage: 'Unknown API Error',
|
||||
});
|
||||
|
||||
export const UNAUTHORIZED_API_ERROR = i18n.translate(
|
||||
'xpack.stackConnectors.resilient.unauthorizedError',
|
||||
{
|
||||
defaultMessage: 'Unauthorized API Error',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -8,34 +8,15 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { TypeOf } from '@kbn/config-schema';
|
||||
import { Logger } from '@kbn/core/server';
|
||||
import { ValidatorServices } from '@kbn/actions-plugin/server/types';
|
||||
import {
|
||||
ExecutorParamsSchema,
|
||||
ExecutorSubActionCommonFieldsParamsSchema,
|
||||
ExecutorSubActionGetIncidentParamsSchema,
|
||||
ExecutorSubActionGetIncidentTypesParamsSchema,
|
||||
ExecutorSubActionGetSeverityParamsSchema,
|
||||
ExecutorSubActionHandshakeParamsSchema,
|
||||
ExecutorSubActionPushParamsSchema,
|
||||
ExternalIncidentServiceConfigurationSchema,
|
||||
ExternalIncidentServiceSecretConfigurationSchema,
|
||||
ExternalServiceIncidentResponseSchema,
|
||||
GetIncidentResponseSchema,
|
||||
} from './schema';
|
||||
|
||||
export type ResilientPublicConfigurationType = TypeOf<
|
||||
typeof ExternalIncidentServiceConfigurationSchema
|
||||
>;
|
||||
export type ResilientSecretConfigurationType = TypeOf<
|
||||
typeof ExternalIncidentServiceSecretConfigurationSchema
|
||||
>;
|
||||
|
||||
export type ExecutorSubActionCommonFieldsParams = TypeOf<
|
||||
typeof ExecutorSubActionCommonFieldsParamsSchema
|
||||
>;
|
||||
|
||||
export type ExecutorParams = TypeOf<typeof ExecutorParamsSchema>;
|
||||
export type ExecutorSubActionPushParams = TypeOf<typeof ExecutorSubActionPushParamsSchema>;
|
||||
|
||||
export interface ExternalServiceCredentials {
|
||||
config: Record<string, unknown>;
|
||||
secrets: Record<string, unknown>;
|
||||
|
@ -46,14 +27,9 @@ export interface ExternalServiceValidation {
|
|||
secrets: (secrets: any, validatorServices: ValidatorServices) => void;
|
||||
}
|
||||
|
||||
export interface ExternalServiceIncidentResponse {
|
||||
id: string;
|
||||
title: string;
|
||||
url: string;
|
||||
pushedDate: string;
|
||||
}
|
||||
export type GetIncidentTypesResponse = Array<{ id: string; name: string }>;
|
||||
export type GetSeverityResponse = Array<{ id: string; name: string }>;
|
||||
|
||||
export type ExternalServiceParams = Record<string, unknown>;
|
||||
export interface ExternalServiceFields {
|
||||
input_type: string;
|
||||
name: string;
|
||||
|
@ -65,104 +41,11 @@ export type GetCommonFieldsResponse = ExternalServiceFields[];
|
|||
|
||||
export type Incident = Omit<ExecutorSubActionPushParams['incident'], 'externalId'>;
|
||||
|
||||
export interface CreateIncidentParams {
|
||||
incident: Incident;
|
||||
}
|
||||
|
||||
export interface UpdateIncidentParams {
|
||||
incidentId: string;
|
||||
incident: Incident;
|
||||
}
|
||||
|
||||
export interface CreateCommentParams {
|
||||
incidentId: string;
|
||||
comment: SimpleComment;
|
||||
}
|
||||
|
||||
export type GetIncidentTypesResponse = Array<{ id: string; name: string }>;
|
||||
export type GetSeverityResponse = Array<{ id: string; name: string }>;
|
||||
|
||||
export interface ExternalService {
|
||||
createComment: (params: CreateCommentParams) => Promise<ExternalServiceCommentResponse>;
|
||||
createIncident: (params: CreateIncidentParams) => Promise<ExternalServiceIncidentResponse>;
|
||||
getFields: () => Promise<GetCommonFieldsResponse>;
|
||||
getIncident: (id: string) => Promise<ExternalServiceParams | undefined>;
|
||||
getIncidentTypes: () => Promise<GetIncidentTypesResponse>;
|
||||
getSeverity: () => Promise<GetSeverityResponse>;
|
||||
updateIncident: (params: UpdateIncidentParams) => Promise<ExternalServiceIncidentResponse>;
|
||||
}
|
||||
|
||||
export type PushToServiceApiParams = ExecutorSubActionPushParams;
|
||||
export type ExecutorSubActionGetIncidentTypesParams = TypeOf<
|
||||
typeof ExecutorSubActionGetIncidentTypesParamsSchema
|
||||
>;
|
||||
|
||||
export type ExecutorSubActionGetSeverityParams = TypeOf<
|
||||
typeof ExecutorSubActionGetSeverityParamsSchema
|
||||
>;
|
||||
|
||||
export interface ExternalServiceApiHandlerArgs {
|
||||
externalService: ExternalService;
|
||||
}
|
||||
|
||||
export type ExecutorSubActionGetIncidentParams = TypeOf<
|
||||
typeof ExecutorSubActionGetIncidentParamsSchema
|
||||
>;
|
||||
|
||||
export type ExecutorSubActionHandshakeParams = TypeOf<
|
||||
typeof ExecutorSubActionHandshakeParamsSchema
|
||||
>;
|
||||
|
||||
export interface PushToServiceApiHandlerArgs extends ExternalServiceApiHandlerArgs {
|
||||
params: PushToServiceApiParams;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
export interface GetIncidentApiHandlerArgs extends ExternalServiceApiHandlerArgs {
|
||||
params: ExecutorSubActionGetIncidentParams;
|
||||
}
|
||||
|
||||
export interface HandshakeApiHandlerArgs extends ExternalServiceApiHandlerArgs {
|
||||
params: ExecutorSubActionHandshakeParams;
|
||||
}
|
||||
|
||||
export interface GetCommonFieldsHandlerArgs {
|
||||
externalService: ExternalService;
|
||||
params: ExecutorSubActionCommonFieldsParams;
|
||||
}
|
||||
|
||||
export interface GetIncidentTypesHandlerArgs {
|
||||
externalService: ExternalService;
|
||||
params: ExecutorSubActionGetIncidentTypesParams;
|
||||
}
|
||||
|
||||
export interface GetSeverityHandlerArgs {
|
||||
externalService: ExternalService;
|
||||
params: ExecutorSubActionGetSeverityParams;
|
||||
}
|
||||
|
||||
export interface PushToServiceResponse extends ExternalServiceIncidentResponse {
|
||||
comments?: ExternalServiceCommentResponse[];
|
||||
}
|
||||
|
||||
export interface ExternalServiceApi {
|
||||
getFields: (args: GetCommonFieldsHandlerArgs) => Promise<GetCommonFieldsResponse>;
|
||||
handshake: (args: HandshakeApiHandlerArgs) => Promise<void>;
|
||||
pushToService: (args: PushToServiceApiHandlerArgs) => Promise<PushToServiceResponse>;
|
||||
getIncident: (args: GetIncidentApiHandlerArgs) => Promise<void>;
|
||||
incidentTypes: (args: GetIncidentTypesHandlerArgs) => Promise<GetIncidentTypesResponse>;
|
||||
severity: (args: GetSeverityHandlerArgs) => Promise<GetSeverityResponse>;
|
||||
}
|
||||
|
||||
export type ResilientExecutorResultData =
|
||||
| PushToServiceResponse
|
||||
| GetCommonFieldsResponse
|
||||
| GetIncidentTypesResponse
|
||||
| GetSeverityResponse;
|
||||
|
||||
export interface UpdateFieldText {
|
||||
text: string;
|
||||
}
|
||||
export interface UpdateFieldText {
|
||||
text: string;
|
||||
}
|
||||
|
@ -170,7 +53,6 @@ export interface UpdateFieldText {
|
|||
export interface UpdateIdsField {
|
||||
ids: number[];
|
||||
}
|
||||
|
||||
export interface UpdateIdField {
|
||||
id: number;
|
||||
}
|
||||
|
@ -202,12 +84,11 @@ export interface CreateIncidentData {
|
|||
incident_type_ids?: Array<{ id: number }>;
|
||||
severity_code?: { id: number };
|
||||
}
|
||||
export interface SimpleComment {
|
||||
comment: string;
|
||||
commentId: string;
|
||||
}
|
||||
export interface ExternalServiceCommentResponse {
|
||||
commentId: string;
|
||||
pushedDate: string;
|
||||
externalCommentId?: string;
|
||||
}
|
||||
|
||||
export type ResilientConfig = TypeOf<typeof ExternalIncidentServiceConfigurationSchema>;
|
||||
export type ResilientSecrets = TypeOf<typeof ExternalIncidentServiceSecretConfigurationSchema>;
|
||||
|
||||
export type ExecutorSubActionPushParams = TypeOf<typeof ExecutorSubActionPushParamsSchema>;
|
||||
|
||||
export type ExternalServiceIncidentResponse = TypeOf<typeof ExternalServiceIncidentResponseSchema>;
|
||||
export type GetIncidentResponse = TypeOf<typeof GetIncidentResponseSchema>;
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* 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 { formatUpdateRequest, getValueTextContent } from './utils';
|
||||
|
||||
describe('utils', () => {
|
||||
describe('getValueTextContent', () => {
|
||||
test('transforms name correctly', () => {
|
||||
expect(getValueTextContent('name', 'title')).toEqual({
|
||||
text: 'title',
|
||||
});
|
||||
});
|
||||
|
||||
test('transforms correctly the description', () => {
|
||||
expect(getValueTextContent('description', 'desc')).toEqual({
|
||||
textarea: {
|
||||
format: 'html',
|
||||
content: 'desc',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('transforms correctly the severityCode', () => {
|
||||
expect(getValueTextContent('severityCode', 6)).toEqual({
|
||||
id: 6,
|
||||
});
|
||||
});
|
||||
|
||||
test('transforms correctly the severityCode as string', () => {
|
||||
expect(getValueTextContent('severityCode', '6')).toEqual({
|
||||
id: 6,
|
||||
});
|
||||
});
|
||||
|
||||
test('transforms correctly the incidentTypes', () => {
|
||||
expect(getValueTextContent('incidentTypes', [1101, 12])).toEqual({
|
||||
ids: [1101, 12],
|
||||
});
|
||||
});
|
||||
|
||||
test('transforms default correctly', () => {
|
||||
expect(getValueTextContent('randomField', 'this is random')).toEqual({
|
||||
text: 'this is random',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatUpdateRequest', () => {
|
||||
test('transforms correctly', () => {
|
||||
const oldIncident = {
|
||||
name: 'title',
|
||||
description: { format: 'html', content: 'desc' },
|
||||
severity_code: '5',
|
||||
incident_type_ids: [12, 16],
|
||||
};
|
||||
const newIncident = {
|
||||
name: 'title_updated',
|
||||
description: 'desc_updated',
|
||||
severityCode: '6',
|
||||
incidentTypes: [12, 16, 1001],
|
||||
};
|
||||
expect(formatUpdateRequest({ oldIncident, newIncident })).toEqual({
|
||||
changes: [
|
||||
{
|
||||
field: { name: 'name' },
|
||||
old_value: { text: 'title' },
|
||||
new_value: { text: 'title_updated' },
|
||||
},
|
||||
{
|
||||
field: { name: 'description' },
|
||||
old_value: {
|
||||
textarea: {
|
||||
format: 'html',
|
||||
content: 'desc',
|
||||
},
|
||||
},
|
||||
new_value: {
|
||||
textarea: {
|
||||
format: 'html',
|
||||
content: 'desc_updated',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: { name: 'severity_code' },
|
||||
old_value: {
|
||||
id: 5,
|
||||
},
|
||||
new_value: { id: 6 },
|
||||
},
|
||||
{
|
||||
field: { name: 'incident_type_ids' },
|
||||
old_value: { ids: [12, 16] },
|
||||
new_value: {
|
||||
ids: [12, 16, 1001],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* 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 { isArray } from 'lodash';
|
||||
import { GetValueTextContentResponse, UpdateIncidentRequest } from './types';
|
||||
|
||||
export const getValueTextContent = (
|
||||
field: string,
|
||||
value: string | number | number[]
|
||||
): GetValueTextContentResponse => {
|
||||
if (field === 'description') {
|
||||
return {
|
||||
textarea: {
|
||||
format: 'html',
|
||||
content: value.toString(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (field === 'incidentTypes') {
|
||||
if (isArray(value)) {
|
||||
return { ids: value.map((item) => Number(item)) };
|
||||
}
|
||||
return {
|
||||
ids: [Number(value)],
|
||||
};
|
||||
}
|
||||
|
||||
if (field === 'severityCode') {
|
||||
return {
|
||||
id: Number(value),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
text: value.toString(),
|
||||
};
|
||||
};
|
||||
|
||||
export const formatUpdateRequest = ({
|
||||
oldIncident,
|
||||
newIncident,
|
||||
}: {
|
||||
oldIncident: Record<string, unknown>;
|
||||
newIncident: Record<string, unknown>;
|
||||
}): UpdateIncidentRequest => {
|
||||
return {
|
||||
changes: Object.keys(newIncident).map((key) => {
|
||||
let name = key;
|
||||
|
||||
if (key === 'incidentTypes') {
|
||||
name = 'incident_type_ids';
|
||||
}
|
||||
|
||||
if (key === 'severityCode') {
|
||||
name = 'severity_code';
|
||||
}
|
||||
|
||||
return {
|
||||
field: { name },
|
||||
old_value: getValueTextContent(
|
||||
key,
|
||||
name === 'description'
|
||||
? (oldIncident as { description: { content: string } }).description.content
|
||||
: (oldIncident[name] as string | number | number[])
|
||||
),
|
||||
new_value: getValueTextContent(key, newIncident[key] as string),
|
||||
};
|
||||
}),
|
||||
};
|
||||
};
|
|
@ -1,37 +0,0 @@
|
|||
/*
|
||||
* 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 { ValidatorServices } from '@kbn/actions-plugin/server/types';
|
||||
import {
|
||||
ResilientPublicConfigurationType,
|
||||
ResilientSecretConfigurationType,
|
||||
ExternalServiceValidation,
|
||||
} from './types';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
export const validateCommonConfig = (
|
||||
configObject: ResilientPublicConfigurationType,
|
||||
validatorServices: ValidatorServices
|
||||
) => {
|
||||
const { configurationUtilities } = validatorServices;
|
||||
try {
|
||||
configurationUtilities.ensureUriAllowed(configObject.apiUrl);
|
||||
} catch (allowedListError) {
|
||||
throw new Error(i18n.ALLOWED_HOSTS_ERROR(allowedListError.message));
|
||||
}
|
||||
};
|
||||
|
||||
export const validateCommonSecrets = (
|
||||
secrets: ResilientSecretConfigurationType,
|
||||
validatorServices: ValidatorServices
|
||||
) => {};
|
||||
|
||||
export const validate: ExternalServiceValidation = {
|
||||
config: validateCommonConfig,
|
||||
secrets: validateCommonSecrets,
|
||||
};
|
|
@ -25,7 +25,7 @@ describe('Stack Connectors Plugin', () => {
|
|||
it('should register built in connector types', () => {
|
||||
const actionsSetup = actionsMock.createSetup();
|
||||
plugin.setup(coreSetup, { actions: actionsSetup });
|
||||
expect(actionsSetup.registerType).toHaveBeenCalledTimes(17);
|
||||
expect(actionsSetup.registerType).toHaveBeenCalledTimes(16);
|
||||
expect(actionsSetup.registerType).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
|
@ -119,26 +119,19 @@ describe('Stack Connectors Plugin', () => {
|
|||
);
|
||||
expect(actionsSetup.registerType).toHaveBeenNthCalledWith(
|
||||
15,
|
||||
expect.objectContaining({
|
||||
id: '.resilient',
|
||||
name: 'IBM Resilient',
|
||||
})
|
||||
);
|
||||
expect(actionsSetup.registerType).toHaveBeenNthCalledWith(
|
||||
16,
|
||||
expect.objectContaining({
|
||||
id: '.teams',
|
||||
name: 'Microsoft Teams',
|
||||
})
|
||||
);
|
||||
expect(actionsSetup.registerType).toHaveBeenNthCalledWith(
|
||||
17,
|
||||
16,
|
||||
expect.objectContaining({
|
||||
id: '.torq',
|
||||
name: 'Torq',
|
||||
})
|
||||
);
|
||||
expect(actionsSetup.registerSubActionConnectorType).toHaveBeenCalledTimes(6);
|
||||
expect(actionsSetup.registerSubActionConnectorType).toHaveBeenCalledTimes(7);
|
||||
expect(actionsSetup.registerSubActionConnectorType).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
|
@ -174,6 +167,13 @@ describe('Stack Connectors Plugin', () => {
|
|||
name: 'D3 Security',
|
||||
})
|
||||
);
|
||||
expect(actionsSetup.registerSubActionConnectorType).toHaveBeenNthCalledWith(
|
||||
6,
|
||||
expect.objectContaining({
|
||||
id: '.resilient',
|
||||
name: 'IBM Resilient',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -32,8 +32,6 @@ export type {
|
|||
ServiceNowActionParams,
|
||||
JiraConnectorTypeId,
|
||||
JiraActionParams,
|
||||
ResilientConnectorTypeId,
|
||||
ResilientActionParams,
|
||||
TeamsConnectorTypeId,
|
||||
TeamsActionParams,
|
||||
} from './connector_types';
|
||||
|
|
|
@ -51,12 +51,13 @@ export default function resilientTest({ getService }: FtrProviderContext) {
|
|||
|
||||
it('should return 403 when creating a resilient action', async () => {
|
||||
await supertest
|
||||
.post('/api/actions/action')
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'A resilient action',
|
||||
actionTypeId: '.resilient',
|
||||
connector_type_id: '.resilient',
|
||||
config: {
|
||||
...mockResilient.config,
|
||||
apiUrl: resilientSimulatorURL,
|
||||
},
|
||||
secrets: mockResilient.secrets,
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import http from 'http';
|
||||
import {
|
||||
RequestHandlerContext,
|
||||
KibanaRequest,
|
||||
|
@ -12,6 +13,55 @@ import {
|
|||
IKibanaResponse,
|
||||
IRouter,
|
||||
} from '@kbn/core/server';
|
||||
import { ProxyArgs, Simulator } from './simulator';
|
||||
|
||||
export const resilientFailedResponse = {
|
||||
errors: {
|
||||
message: 'failed',
|
||||
},
|
||||
};
|
||||
|
||||
export class ResilientSimulator 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 ResilientSimulator.sendErrorResponse(response);
|
||||
}
|
||||
return ResilientSimulator.sendResponse(request, response);
|
||||
}
|
||||
|
||||
private static sendResponse(request: http.IncomingMessage, response: http.ServerResponse) {
|
||||
response.statusCode = 202;
|
||||
response.setHeader('Content-Type', 'application/json');
|
||||
response.end(
|
||||
JSON.stringify(
|
||||
{
|
||||
id: '123',
|
||||
create_date: 1589391874472,
|
||||
},
|
||||
null,
|
||||
4
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private static sendErrorResponse(response: http.ServerResponse) {
|
||||
response.statusCode = 422;
|
||||
response.setHeader('Content-Type', 'application/json;charset=UTF-8');
|
||||
response.end(JSON.stringify(resilientFailedResponse, null, 4));
|
||||
}
|
||||
}
|
||||
|
||||
export function initPlugin(router: IRouter, path: string) {
|
||||
router.post(
|
||||
|
|
|
@ -5,21 +5,15 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import httpProxy from 'http-proxy';
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
import { getHttpProxyServer } from '@kbn/alerting-api-integration-helpers';
|
||||
import {
|
||||
getExternalServiceSimulatorPath,
|
||||
ExternalServiceSimulator,
|
||||
} from '@kbn/actions-simulators-plugin/server/plugin';
|
||||
import { TaskErrorSource } from '@kbn/task-manager-plugin/common';
|
||||
import { ResilientSimulator } from '@kbn/actions-simulators-plugin/server/resilient_simulation';
|
||||
import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function resilientTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const configService = getService('config');
|
||||
|
||||
const mockResilient = {
|
||||
|
@ -51,17 +45,25 @@ export default function resilientTest({ getService }: FtrProviderContext) {
|
|||
},
|
||||
};
|
||||
|
||||
let resilientSimulatorURL: string = '<could not determine kibana url>';
|
||||
|
||||
describe('IBM Resilient', () => {
|
||||
before(() => {
|
||||
resilientSimulatorURL = kibanaServer.resolveUrl(
|
||||
getExternalServiceSimulatorPath(ExternalServiceSimulator.RESILIENT)
|
||||
);
|
||||
});
|
||||
|
||||
describe('IBM Resilient - Action Creation', () => {
|
||||
it('should return 200 when creating a ibm resilient action successfully', async () => {
|
||||
const simulator = new ResilientSimulator({
|
||||
proxy: {
|
||||
config: configService.get('kbnTestServer.serverArgs'),
|
||||
},
|
||||
});
|
||||
|
||||
let resilientSimulatorURL: string = '<could not determine kibana url>';
|
||||
|
||||
before(async () => {
|
||||
resilientSimulatorURL = await simulator.start();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
simulator.close();
|
||||
});
|
||||
|
||||
it('should return 200 when creating a ibm resilient connector successfully', async () => {
|
||||
const { body: createdAction } = await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
|
@ -168,7 +170,7 @@ export default function resilientTest({ getService }: FtrProviderContext) {
|
|||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'error validating action type config: error configuring connector action: target url "http://resilient.mynonexistent.com" is not added to the Kibana config xpack.actions.allowedHosts',
|
||||
'error validating action type config: error validating url: target url "http://resilient.mynonexistent.com" is not added to the Kibana config xpack.actions.allowedHosts',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -198,37 +200,28 @@ export default function resilientTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
describe('IBM Resilient - Executor', () => {
|
||||
let simulatedActionId: string;
|
||||
let proxyServer: httpProxy | undefined;
|
||||
let proxyHaveBeenCalled = false;
|
||||
before(async () => {
|
||||
const { body } = await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'A ibm resilient simulator',
|
||||
connector_type_id: '.resilient',
|
||||
config: {
|
||||
apiUrl: resilientSimulatorURL,
|
||||
orgId: mockResilient.config.orgId,
|
||||
},
|
||||
secrets: mockResilient.secrets,
|
||||
});
|
||||
simulatedActionId = body.id;
|
||||
|
||||
proxyServer = await getHttpProxyServer(
|
||||
kibanaServer.resolveUrl('/'),
|
||||
configService.get('kbnTestServer.serverArgs'),
|
||||
() => {
|
||||
proxyHaveBeenCalled = true;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('Validation', () => {
|
||||
const simulator = new ResilientSimulator({
|
||||
proxy: {
|
||||
config: configService.get('kbnTestServer.serverArgs'),
|
||||
},
|
||||
});
|
||||
|
||||
let resilientActionId: string;
|
||||
let resilientSimulatorURL: string = '<could not determine kibana url>';
|
||||
|
||||
before(async () => {
|
||||
resilientSimulatorURL = await simulator.start();
|
||||
resilientActionId = await createConnector(resilientSimulatorURL);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
simulator.close();
|
||||
});
|
||||
|
||||
it('should handle failing with a simulated success without action', async () => {
|
||||
await supertest
|
||||
.post(`/api/actions/connector/${simulatedActionId}/_execute`)
|
||||
.post(`/api/actions/connector/${resilientActionId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {},
|
||||
|
@ -241,25 +234,25 @@ export default function resilientTest({ getService }: FtrProviderContext) {
|
|||
'errorSource',
|
||||
'connector_id',
|
||||
]);
|
||||
expect(resp.body.connector_id).to.eql(simulatedActionId);
|
||||
expect(resp.body.connector_id).to.eql(resilientActionId);
|
||||
expect(resp.body.status).to.eql('error');
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle failing with a simulated success without unsupported action', async () => {
|
||||
await supertest
|
||||
.post(`/api/actions/connector/${simulatedActionId}/_execute`)
|
||||
.post(`/api/actions/connector/${resilientActionId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: { subAction: 'non-supported' },
|
||||
})
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
connector_id: simulatedActionId,
|
||||
connector_id: resilientActionId,
|
||||
status: 'error',
|
||||
retry: false,
|
||||
message:
|
||||
'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.subAction]: expected value to equal [pushToService]\n- [4.subAction]: expected value to equal [incidentTypes]\n- [5.subAction]: expected value to equal [severity]',
|
||||
retry: true,
|
||||
message: 'an error occurred while running the action',
|
||||
service_message: `Sub action "non-supported" is not registered. Connector id: ${resilientActionId}. Connector name: IBM Resilient. Connector type: .resilient`,
|
||||
errorSource: TaskErrorSource.FRAMEWORK,
|
||||
});
|
||||
});
|
||||
|
@ -267,18 +260,19 @@ export default function resilientTest({ getService }: FtrProviderContext) {
|
|||
|
||||
it('should handle failing with a simulated success without subActionParams', async () => {
|
||||
await supertest
|
||||
.post(`/api/actions/connector/${simulatedActionId}/_execute`)
|
||||
.post(`/api/actions/connector/${resilientActionId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: { subAction: 'pushToService' },
|
||||
})
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
connector_id: simulatedActionId,
|
||||
connector_id: resilientActionId,
|
||||
status: 'error',
|
||||
retry: false,
|
||||
message:
|
||||
'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.subActionParams.incident.name]: expected value of type [string] but got [undefined]\n- [4.subAction]: expected value to equal [incidentTypes]\n- [5.subAction]: expected value to equal [severity]',
|
||||
retry: true,
|
||||
message: 'an error occurred while running the action',
|
||||
service_message:
|
||||
'Request validation failed (Error: [incident.name]: expected value of type [string] but got [undefined])',
|
||||
errorSource: TaskErrorSource.FRAMEWORK,
|
||||
});
|
||||
});
|
||||
|
@ -286,7 +280,7 @@ export default function resilientTest({ getService }: FtrProviderContext) {
|
|||
|
||||
it('should handle failing with a simulated success without title', async () => {
|
||||
await supertest
|
||||
.post(`/api/actions/connector/${simulatedActionId}/_execute`)
|
||||
.post(`/api/actions/connector/${resilientActionId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
|
@ -301,19 +295,20 @@ export default function resilientTest({ getService }: FtrProviderContext) {
|
|||
})
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
connector_id: simulatedActionId,
|
||||
connector_id: resilientActionId,
|
||||
status: 'error',
|
||||
retry: false,
|
||||
retry: true,
|
||||
message: 'an error occurred while running the action',
|
||||
errorSource: TaskErrorSource.FRAMEWORK,
|
||||
message:
|
||||
'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.subActionParams.incident.name]: expected value of type [string] but got [undefined]\n- [4.subAction]: expected value to equal [incidentTypes]\n- [5.subAction]: expected value to equal [severity]',
|
||||
service_message:
|
||||
'Request validation failed (Error: [incident.name]: expected value of type [string] but got [undefined])',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle failing with a simulated success without commentId', async () => {
|
||||
await supertest
|
||||
.post(`/api/actions/connector/${simulatedActionId}/_execute`)
|
||||
.post(`/api/actions/connector/${resilientActionId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
|
@ -329,23 +324,24 @@ export default function resilientTest({ getService }: FtrProviderContext) {
|
|||
})
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
connector_id: simulatedActionId,
|
||||
connector_id: resilientActionId,
|
||||
status: 'error',
|
||||
retry: false,
|
||||
retry: true,
|
||||
message: 'an error occurred while running the action',
|
||||
errorSource: TaskErrorSource.FRAMEWORK,
|
||||
message:
|
||||
'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.commentId]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]\n- [4.subAction]: expected value to equal [incidentTypes]\n- [5.subAction]: expected value to equal [severity]',
|
||||
service_message:
|
||||
'Request validation failed (Error: [comments]: types that failed validation:\n- [comments.0.0.commentId]: expected value of type [string] but got [undefined]\n- [comments.1]: expected value to equal [null])',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle failing with a simulated success without comment message', async () => {
|
||||
await supertest
|
||||
.post(`/api/actions/connector/${simulatedActionId}/_execute`)
|
||||
.post(`/api/actions/connector/${resilientActionId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
...mockResilient.params,
|
||||
subAction: 'pushToService',
|
||||
subActionParams: {
|
||||
incident: {
|
||||
...mockResilient.params.subActionParams.incident,
|
||||
|
@ -357,21 +353,40 @@ export default function resilientTest({ getService }: FtrProviderContext) {
|
|||
})
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
connector_id: simulatedActionId,
|
||||
connector_id: resilientActionId,
|
||||
status: 'error',
|
||||
retry: false,
|
||||
retry: true,
|
||||
message: 'an error occurred while running the action',
|
||||
errorSource: TaskErrorSource.FRAMEWORK,
|
||||
message:
|
||||
'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.comment]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]\n- [4.subAction]: expected value to equal [incidentTypes]\n- [5.subAction]: expected value to equal [severity]',
|
||||
service_message:
|
||||
'Request validation failed (Error: [comments]: types that failed validation:\n- [comments.0.0.comment]: expected value of type [string] but got [undefined]\n- [comments.1]: expected value to equal [null])',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Execution', () => {
|
||||
const simulator = new ResilientSimulator({
|
||||
proxy: {
|
||||
config: configService.get('kbnTestServer.serverArgs'),
|
||||
},
|
||||
});
|
||||
|
||||
let simulatorUrl: string;
|
||||
let resilientActionId: string;
|
||||
|
||||
before(async () => {
|
||||
simulatorUrl = await simulator.start();
|
||||
resilientActionId = await createConnector(simulatorUrl);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
simulator.close();
|
||||
});
|
||||
|
||||
it('should handle creating an incident without comments', async () => {
|
||||
const { body } = await supertest
|
||||
.post(`/api/actions/connector/${simulatedActionId}/_execute`)
|
||||
.post(`/api/actions/connector/${resilientActionId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
|
@ -384,25 +399,33 @@ export default function resilientTest({ getService }: FtrProviderContext) {
|
|||
})
|
||||
.expect(200);
|
||||
|
||||
expect(proxyHaveBeenCalled).to.equal(true);
|
||||
expect(body).to.eql({
|
||||
status: 'ok',
|
||||
connector_id: simulatedActionId,
|
||||
connector_id: resilientActionId,
|
||||
data: {
|
||||
id: '123',
|
||||
title: '123',
|
||||
pushedDate: '2020-05-13T17:44:34.472Z',
|
||||
url: `${resilientSimulatorURL}/#incidents/123`,
|
||||
url: `${simulatorUrl}/#incidents/123`,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
after(() => {
|
||||
if (proxyServer) {
|
||||
proxyServer.close();
|
||||
}
|
||||
});
|
||||
const createConnector = async (url: string) => {
|
||||
const { body } = await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'A resilient action',
|
||||
connector_type_id: '.resilient',
|
||||
config: { ...mockResilient.config, apiUrl: url },
|
||||
secrets: mockResilient.secrets,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
return body.id;
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue