diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index 3bc8acead6c1..c55b21b2f902 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -331,15 +331,17 @@ const result = await actionsClient.execute({ Kibana ships with a set of built-in action types: -| Type | Id | Description | -| ------------------------- | ------------- | ------------------------------------------------------------------ | -| [Server log](#server-log) | `.server-log` | Logs messages to the Kibana log using Kibana's logger | -| [Email](#email) | `.email` | Sends an email using SMTP | -| [Slack](#slack) | `.slack` | Posts a message to a slack channel | -| [Index](#index) | `.index` | Indexes document(s) into Elasticsearch | -| [Webhook](#webhook) | `.webhook` | Send a payload to a web service using HTTP POST or PUT | -| [PagerDuty](#pagerduty) | `.pagerduty` | Trigger, resolve, or acknowlege an incident to a PagerDuty service | -| [ServiceNow](#servicenow) | `.servicenow` | Create or update an incident to a ServiceNow instance | +| Type | Id | Description | +| ------------------------------- | ------------- | ------------------------------------------------------------------ | +| [Server log](#server-log) | `.server-log` | Logs messages to the Kibana log using Kibana's logger | +| [Email](#email) | `.email` | Sends an email using SMTP | +| [Slack](#slack) | `.slack` | Posts a message to a slack channel | +| [Index](#index) | `.index` | Indexes document(s) into Elasticsearch | +| [Webhook](#webhook) | `.webhook` | Send a payload to a web service using HTTP POST or PUT | +| [PagerDuty](#pagerduty) | `.pagerduty` | Trigger, resolve, or acknowlege an incident to a PagerDuty service | +| [ServiceNow](#servicenow) | `.servicenow` | Create or update an incident to a ServiceNow instance | +| [Jira](#jira) | `.jira` | Create or update an issue to a Jira instance | +| [IBM Resilient](#ibm-resilient) | `.resilient` | Create or update an incident to a IBM Resilient instance | --- @@ -561,8 +563,8 @@ The ServiceNow action uses the [V2 Table API](https://developer.servicenow.com/a | Property | Description | Type | | ------------- | ------------------------------------------------------------------------------------------------------------------------- | --------------------- | | savedObjectId | The id of the saved object. | string | -| title | The title of the case. | string _(optional)_ | -| description | The description of the case. | string _(optional)_ | +| title | The title of the incident. | string _(optional)_ | +| description | The description of the incident. | string _(optional)_ | | comment | A comment. | string _(optional)_ | | comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }`. | object[] _(optional)_ | | externalId | The id of the incident in ServiceNow. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ | @@ -601,16 +603,16 @@ The Jira action uses the [V2 API](https://developer.atlassian.com/cloud/jira/pla #### `subActionParams (pushToService)` -| Property | Description | Type | -| ------------- | ------------------------------------------------------------------------------------------------------------------- | --------------------- | -| savedObjectId | The id of the saved object | string | -| title | The title of the case | string _(optional)_ | -| description | The description of the case | string _(optional)_ | -| externalId | The id of the incident in Jira. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ | -| issueType | The id of the issue type in Jira. | string _(optional)_ | -| priority | The name of the priority in Jira. Example: `Medium`. | string _(optional)_ | -| labels | An array of labels. | string[] _(optional)_ | -| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }` | object[] _(optional)_ | +| Property | Description | Type | +| ------------- | ---------------------------------------------------------------------------------------------------------------- | --------------------- | +| savedObjectId | The id of the saved object | string | +| title | The title of the issue | string _(optional)_ | +| description | The description of the issue | string _(optional)_ | +| externalId | The id of the issue in Jira. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ | +| issueType | The id of the issue type in Jira. | string _(optional)_ | +| priority | The name of the priority in Jira. Example: `Medium`. | string _(optional)_ | +| labels | An array of labels. | string[] _(optional)_ | +| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }` | object[] _(optional)_ | #### `subActionParams (issueTypes)` @@ -628,10 +630,10 @@ ID: `.resilient` ### `config` -| Property | Description | Type | -| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------ | -| apiUrl | IBM Resilient instance URL. | string | -| incidentConfiguration | Case configuration object. The object should contain an attribute called `mapping`. A `mapping` is an array of objects. Each mapping object should be of the form `{ source: string, target: string, actionType: string }`. `source` is the Case field. `target` is the Jira field where `source` will be mapped to. `actionType` can be one of `nothing`, `overwrite` or `append`. For example the `{ source: 'title', target: 'summary', actionType: 'overwrite' }` record, inside mapping array, means that the title of a case will be mapped to the short description of an incident in IBM Resilient and will be overwrite on each update. | object | +| Property | Description | Type | +| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | +| apiUrl | IBM Resilient instance URL. | string | +| incidentConfiguration | Optional property and specific to **Cases only**. If defined, the object should contain an attribute called `mapping`. A `mapping` is an array of objects. Each mapping object should be of the form `{ source: string, target: string, actionType: string }`. `source` is the Case field. `target` is the Jira field where `source` will be mapped to. `actionType` can be one of `nothing`, `overwrite` or `append`. For example the `{ source: 'title', target: 'summary', actionType: 'overwrite' }` record, inside mapping array, means that the title of a case will be mapped to the short description of an incident in IBM Resilient and will be overwrite on each update. | object | ### `secrets` @@ -652,10 +654,12 @@ ID: `.resilient` | Property | Description | Type | | ------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------- | | savedObjectId | The id of the saved object | string | -| title | The title of the case | string _(optional)_ | -| description | The description of the case | string _(optional)_ | -| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }` | object[] _(optional)_ | +| title | The title of the incident | string _(optional)_ | +| description | The description of the incident | string _(optional)_ | +| comments | The comments of the incident. A comment is of the form `{ commentId: string, version: string, comment: string }` | object[] _(optional)_ | | externalId | The id of the incident in IBM Resilient. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ | +| incidentTypes | An array with the ids of IBM Resilient incident types. | number[] _(optional)_ | +| severityCode | IBM Resilient id of the severity code. | number _(optional)_ | # Command Line Utility diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/api.ts b/x-pack/plugins/actions/server/builtin_action_types/case/api.ts deleted file mode 100644 index de4b7edaed3d..000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/case/api.ts +++ /dev/null @@ -1,93 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - ExternalServiceApi, - ExternalServiceParams, - PushToServiceResponse, - GetIncidentApiHandlerArgs, - HandshakeApiHandlerArgs, - PushToServiceApiHandlerArgs, -} from './types'; -import { prepareFieldsForTransformation, transformFields, transformComments } from './utils'; - -const handshakeHandler = async ({ - externalService, - mapping, - params, -}: HandshakeApiHandlerArgs) => {}; -const getIncidentHandler = async ({ - externalService, - mapping, - params, -}: GetIncidentApiHandlerArgs) => {}; - -const pushToServiceHandler = async ({ - externalService, - mapping, - params, -}: PushToServiceApiHandlerArgs): Promise => { - const { externalId, comments } = params; - const updateIncident = externalId ? true : false; - const defaultPipes = updateIncident ? ['informationUpdated'] : ['informationCreated']; - let currentIncident: ExternalServiceParams | undefined; - let res: PushToServiceResponse; - - if (externalId) { - currentIncident = await externalService.getIncident(externalId); - } - - const fields = prepareFieldsForTransformation({ - externalCase: params.externalCase, - mapping, - defaultPipes, - }); - - const incident = transformFields({ - params, - fields, - currentIncident, - }); - - if (updateIncident) { - res = await externalService.updateIncident({ incidentId: externalId, incident }); - } else { - res = await externalService.createIncident({ incident }); - } - - if ( - comments && - Array.isArray(comments) && - comments.length > 0 && - mapping.get('comments')?.actionType !== 'nothing' - ) { - const commentsTransformed = transformComments(comments, ['informationAdded']); - - res.comments = []; - for (const currentComment of commentsTransformed) { - const comment = await externalService.createComment({ - incidentId: res.id, - comment: currentComment, - field: mapping.get('comments')?.target ?? 'comments', - }); - res.comments = [ - ...(res.comments ?? []), - { - commentId: comment.commentId, - pushedDate: comment.pushedDate, - }, - ]; - } - } - - return res; -}; - -export const api: ExternalServiceApi = { - handshake: handshakeHandler, - pushToService: pushToServiceHandler, - getIncident: getIncidentHandler, -}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/common_schema.ts b/x-pack/plugins/actions/server/builtin_action_types/case/common_schema.ts deleted file mode 100644 index 5a23eb89339e..000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/case/common_schema.ts +++ /dev/null @@ -1,43 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { schema } from '@kbn/config-schema'; - -export const MappingActionType = schema.oneOf([ - schema.literal('nothing'), - schema.literal('overwrite'), - schema.literal('append'), -]); - -export const MapRecordSchema = schema.object({ - source: schema.string(), - target: schema.string(), - actionType: MappingActionType, -}); - -export const IncidentConfigurationSchema = schema.object({ - mapping: schema.arrayOf(MapRecordSchema), -}); - -export const UserSchema = schema.object({ - fullName: schema.nullable(schema.string()), - username: schema.nullable(schema.string()), -}); - -export const EntityInformation = { - createdAt: schema.nullable(schema.string()), - createdBy: schema.nullable(UserSchema), - updatedAt: schema.nullable(schema.string()), - updatedBy: schema.nullable(UserSchema), -}; - -export const EntityInformationSchema = schema.object(EntityInformation); - -export const CommentSchema = schema.object({ - commentId: schema.string(), - comment: schema.string(), - ...EntityInformation, -}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/common_types.ts b/x-pack/plugins/actions/server/builtin_action_types/case/common_types.ts deleted file mode 100644 index cca83fb88ca9..000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/case/common_types.ts +++ /dev/null @@ -1,48 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { TypeOf } from '@kbn/config-schema'; -import { - IncidentConfigurationSchema, - MapRecordSchema, - CommentSchema, - EntityInformationSchema, -} from './common_schema'; - -export interface CreateCommentRequest { - [key: string]: string; -} - -export type IncidentConfiguration = TypeOf; -export type MapRecord = TypeOf; -export type Comment = TypeOf; -export type EntityInformation = TypeOf; - -export interface ExternalServiceCommentResponse { - commentId: string; - pushedDate: string; - externalCommentId?: string; -} - -export interface PipedField { - key: string; - value: string; - actionType: string; - pipes: string[]; -} - -export interface TransformFieldsArgs { - params: P; - fields: PipedField[]; - currentIncident?: S; -} - -export interface TransformerArgs { - value: string; - date?: string; - user?: string; - previousValue?: string; -} diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/case/schema.ts index f47686c911ff..5a23eb89339e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/case/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/case/schema.ts @@ -18,36 +18,18 @@ export const MapRecordSchema = schema.object({ actionType: MappingActionType, }); -export const CaseConfigurationSchema = schema.object({ +export const IncidentConfigurationSchema = schema.object({ mapping: schema.arrayOf(MapRecordSchema), }); -export const ExternalIncidentServiceConfiguration = { - apiUrl: schema.string(), - casesConfiguration: CaseConfigurationSchema, -}; - -export const ExternalIncidentServiceConfigurationSchema = schema.object( - ExternalIncidentServiceConfiguration -); - -export const ExternalIncidentServiceSecretConfiguration = { - password: schema.string(), - username: schema.string(), -}; - -export const ExternalIncidentServiceSecretConfigurationSchema = schema.object( - ExternalIncidentServiceSecretConfiguration -); - export const UserSchema = schema.object({ fullName: schema.nullable(schema.string()), username: schema.nullable(schema.string()), }); -const EntityInformation = { - createdAt: schema.string(), - createdBy: UserSchema, +export const EntityInformation = { + createdAt: schema.nullable(schema.string()), + createdBy: schema.nullable(UserSchema), updatedAt: schema.nullable(schema.string()), updatedBy: schema.nullable(UserSchema), }; @@ -59,40 +41,3 @@ export const CommentSchema = schema.object({ comment: schema.string(), ...EntityInformation, }); - -export const ExecutorSubActionSchema = schema.oneOf([ - schema.literal('getIncident'), - schema.literal('pushToService'), - schema.literal('handshake'), -]); - -export const ExecutorSubActionPushParamsSchema = schema.object({ - savedObjectId: schema.string(), - title: schema.string(), - description: schema.nullable(schema.string()), - comments: schema.nullable(schema.arrayOf(CommentSchema)), - externalId: schema.nullable(schema.string()), - ...EntityInformation, -}); - -export const ExecutorSubActionGetIncidentParamsSchema = schema.object({ - externalId: schema.string(), -}); - -// Reserved for future implementation -export const ExecutorSubActionHandshakeParamsSchema = schema.object({}); - -export const ExecutorParamsSchema = schema.oneOf([ - schema.object({ - subAction: schema.literal('getIncident'), - subActionParams: ExecutorSubActionGetIncidentParamsSchema, - }), - schema.object({ - subAction: schema.literal('handshake'), - subActionParams: ExecutorSubActionHandshakeParamsSchema, - }), - schema.object({ - subAction: schema.literal('pushToService'), - subActionParams: ExecutorSubActionPushParamsSchema, - }), -]); diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/types.ts b/x-pack/plugins/actions/server/builtin_action_types/case/types.ts index 1030e3d9c5d8..73d8297c638d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/case/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/case/types.ts @@ -4,74 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -// This will have to remain `any` until we can extend connectors with generics -// and circular dependencies eliminated. -/* eslint-disable @typescript-eslint/no-explicit-any */ - import { TypeOf } from '@kbn/config-schema'; -import { Logger } from '../../../../../../src/core/server'; - import { - ExternalIncidentServiceConfigurationSchema, - ExternalIncidentServiceSecretConfigurationSchema, - ExecutorParamsSchema, - CaseConfigurationSchema, + IncidentConfigurationSchema, MapRecordSchema, CommentSchema, - ExecutorSubActionPushParamsSchema, - ExecutorSubActionGetIncidentParamsSchema, - ExecutorSubActionHandshakeParamsSchema, + EntityInformationSchema, } from './schema'; -import { LicenseType } from '../../../../../legacy/common/constants'; -export interface AnyParams { - [index: string]: string | number | object | undefined | null; -} - -export type ExternalIncidentServiceConfiguration = TypeOf< - typeof ExternalIncidentServiceConfigurationSchema ->; -export type ExternalIncidentServiceSecretConfiguration = TypeOf< - typeof ExternalIncidentServiceSecretConfigurationSchema ->; - -export type ExecutorParams = TypeOf; -export type ExecutorSubActionPushParams = TypeOf; - -export type ExecutorSubActionGetIncidentParams = TypeOf< - typeof ExecutorSubActionGetIncidentParamsSchema ->; - -export type ExecutorSubActionHandshakeParams = TypeOf< - typeof ExecutorSubActionHandshakeParamsSchema ->; - -export type CaseConfiguration = TypeOf; +export type IncidentConfiguration = TypeOf; export type MapRecord = TypeOf; export type Comment = TypeOf; - -export interface ExternalServiceConfiguration { - id: string; - name: string; - minimumLicenseRequired: LicenseType; -} - -export interface ExternalServiceCredentials { - config: Record; - secrets: Record; -} - -export interface ExternalServiceValidation { - config: (configurationUtilities: any, configObject: any) => void; - secrets: (configurationUtilities: any, secrets: any) => void; -} - -export interface ExternalServiceIncidentResponse { - id: string; - title: string; - url: string; - pushedDate: string; -} +export type EntityInformation = TypeOf; export interface ExternalServiceCommentResponse { commentId: string; @@ -79,69 +23,6 @@ export interface ExternalServiceCommentResponse { externalCommentId?: string; } -export interface ExternalServiceParams { - [index: string]: any; -} - -export interface ExternalService { - getIncident: (id: string) => Promise; - createIncident: (params: ExternalServiceParams) => Promise; - updateIncident: (params: ExternalServiceParams) => Promise; - createComment: (params: ExternalServiceParams) => Promise; -} - -export interface PushToServiceApiParams extends ExecutorSubActionPushParams { - externalCase: Record; -} - -export interface ExternalServiceApiHandlerArgs { - externalService: ExternalService; - mapping: Map; -} - -export interface PushToServiceApiHandlerArgs extends ExternalServiceApiHandlerArgs { - params: PushToServiceApiParams; -} - -export interface GetIncidentApiHandlerArgs extends ExternalServiceApiHandlerArgs { - params: ExecutorSubActionGetIncidentParams; -} - -export interface HandshakeApiHandlerArgs extends ExternalServiceApiHandlerArgs { - params: ExecutorSubActionHandshakeParams; -} - -export interface PushToServiceResponse extends ExternalServiceIncidentResponse { - comments?: ExternalServiceCommentResponse[]; -} - -export interface ExternalServiceApi { - handshake: (args: HandshakeApiHandlerArgs) => Promise; - pushToService: (args: PushToServiceApiHandlerArgs) => Promise; - getIncident: (args: GetIncidentApiHandlerArgs) => Promise; -} - -export interface CreateExternalServiceBasicArgs { - api: ExternalServiceApi; - createExternalService: ( - credentials: ExternalServiceCredentials, - logger: Logger, - proxySettings?: any - ) => ExternalService; - logger: Logger; -} - -export interface CreateExternalServiceArgs extends CreateExternalServiceBasicArgs { - config: ExternalServiceConfiguration; - validate: ExternalServiceValidation; - validationSchema: { config: any; secrets: any }; -} - -export interface CreateActionTypeArgs { - configurationUtilities: any; - executor?: any; -} - export interface PipedField { key: string; value: string; @@ -149,16 +30,10 @@ export interface PipedField { pipes: string[]; } -export interface PrepareFieldsForTransformArgs { - externalCase: Record; - mapping: Map; - defaultPipes?: string[]; -} - -export interface TransformFieldsArgs { - params: PushToServiceApiParams; +export interface TransformFieldsArgs { + params: P; fields: PipedField[]; - currentIncident?: ExternalServiceParams; + currentIncident?: S; } export interface TransformerArgs { @@ -167,3 +42,13 @@ export interface TransformerArgs { user?: string; previousValue?: string; } + +export interface AnyParams { + [index: string]: string | number | object | undefined | null; +} + +export interface PrepareFieldsForTransformArgs { + externalCase: Record; + mapping: Map; + defaultPipes?: string[]; +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/case/utils.test.ts index 2e3cee3946d6..600e18eb5daf 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/case/utils.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/case/utils.test.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + import { normalizeMapping, buildMap, @@ -14,7 +16,23 @@ import { } from './utils'; import { SUPPORTED_SOURCE_FIELDS } from './constants'; -import { Comment, MapRecord, PushToServiceApiParams } from './types'; +import { Comment, MapRecord } from './types'; + +interface Entity { + createdAt: string | null; + createdBy: { fullName: string; username: string } | null; + updatedAt: string | null; + updatedBy: { fullName: string; username: string } | null; +} + +interface PushToServiceApiParams extends Entity { + savedObjectId: string; + title: string; + description: string | null; + externalId: string | null; + externalObject: Record; + comments: Comment[]; +} const mapping: MapRecord[] = [ { source: 'title', target: 'short_description', actionType: 'overwrite' }, @@ -22,7 +40,6 @@ const mapping: MapRecord[] = [ { source: 'comments', target: 'comments', actionType: 'append' }, ]; -// eslint-disable-next-line @typescript-eslint/no-explicit-any const finalMapping: Map = new Map(); finalMapping.set('title', { @@ -61,7 +78,7 @@ const fullParams: PushToServiceApiParams = { updatedAt: null, updatedBy: null, externalId: null, - externalCase: { + externalObject: { short_description: 'a title', description: 'a description', }, @@ -154,7 +171,7 @@ describe('mapParams', () => { describe('prepareFieldsForTransformation', () => { test('prepare fields with defaults', () => { const res = prepareFieldsForTransformation({ - externalCase: fullParams.externalCase, + externalCase: fullParams.externalObject, mapping: finalMapping, }); expect(res).toEqual([ @@ -175,7 +192,7 @@ describe('prepareFieldsForTransformation', () => { test('prepare fields with default pipes', () => { const res = prepareFieldsForTransformation({ - externalCase: fullParams.externalCase, + externalCase: fullParams.externalObject, mapping: finalMapping, defaultPipes: ['myTestPipe'], }); @@ -199,11 +216,15 @@ describe('prepareFieldsForTransformation', () => { describe('transformFields', () => { test('transform fields for creation correctly', () => { const fields = prepareFieldsForTransformation({ - externalCase: fullParams.externalCase, + externalCase: fullParams.externalObject, mapping: finalMapping, }); - const res = transformFields({ + const res = transformFields< + PushToServiceApiParams, + {}, + { short_description: string; description: string } + >({ params: fullParams, fields, }); @@ -216,12 +237,16 @@ describe('transformFields', () => { test('transform fields for update correctly', () => { const fields = prepareFieldsForTransformation({ - externalCase: fullParams.externalCase, + externalCase: fullParams.externalObject, mapping: finalMapping, defaultPipes: ['informationUpdated'], }); - const res = transformFields({ + const res = transformFields< + PushToServiceApiParams, + {}, + { short_description: string; description: string } + >({ params: { ...fullParams, updatedAt: '2020-03-15T08:34:53.450Z', @@ -245,12 +270,16 @@ describe('transformFields', () => { test('add newline character to description', () => { const fields = prepareFieldsForTransformation({ - externalCase: fullParams.externalCase, + externalCase: fullParams.externalObject, mapping: finalMapping, defaultPipes: ['informationUpdated'], }); - const res = transformFields({ + const res = transformFields< + PushToServiceApiParams, + {}, + { short_description: string; description: string } + >({ params: fullParams, fields, currentIncident: { @@ -263,11 +292,15 @@ describe('transformFields', () => { test('append username if fullname is undefined when create', () => { const fields = prepareFieldsForTransformation({ - externalCase: fullParams.externalCase, + externalCase: fullParams.externalObject, mapping: finalMapping, }); - const res = transformFields({ + const res = transformFields< + PushToServiceApiParams, + {}, + { short_description: string; description: string } + >({ params: { ...fullParams, createdBy: { fullName: '', username: 'elastic' }, @@ -283,12 +316,16 @@ describe('transformFields', () => { test('append username if fullname is undefined when update', () => { const fields = prepareFieldsForTransformation({ - externalCase: fullParams.externalCase, + externalCase: fullParams.externalObject, mapping: finalMapping, defaultPipes: ['informationUpdated'], }); - const res = transformFields({ + const res = transformFields< + PushToServiceApiParams, + {}, + { short_description: string; description: string } + >({ params: { ...fullParams, updatedAt: '2020-03-15T08:34:53.450Z', diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts index 701bbea14fde..3d51f5e82627 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts @@ -4,30 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { curry, flow, get } from 'lodash'; -import { schema } from '@kbn/config-schema'; - -import { ActionTypeExecutorOptions, ActionTypeExecutorResult, ActionType } from '../../types'; - -import { ExecutorParamsSchema } from './schema'; -import { - ExternalIncidentServiceConfiguration, - ExternalIncidentServiceSecretConfiguration, -} from './types'; +import { flow, get } from 'lodash'; import { - CreateExternalServiceArgs, - CreateActionTypeArgs, - ExecutorParams, MapRecord, - AnyParams, - CreateExternalServiceBasicArgs, - PrepareFieldsForTransformArgs, - PipedField, TransformFieldsArgs, Comment, - ExecutorSubActionPushParams, - PushToServiceResponse, + EntityInformation, + PipedField, + AnyParams, + PrepareFieldsForTransformArgs, } from './types'; import { transformers } from './transformers'; @@ -61,92 +47,6 @@ export const mapParams = (params: T, mapping: Map async ( - execOptions: ActionTypeExecutorOptions< - ExternalIncidentServiceConfiguration, - ExternalIncidentServiceSecretConfiguration, - ExecutorParams - > -): Promise> => { - const { actionId, config, params, secrets } = execOptions; - const { subAction, subActionParams } = params; - let data = {}; - - const res: ActionTypeExecutorResult = { - status: 'ok', - actionId, - }; - - const externalService = createExternalService( - { - config, - secrets, - }, - logger, - execOptions.proxySettings - ); - - if (!api[subAction]) { - throw new Error('[Action][ExternalService] Unsupported subAction type.'); - } - - if (subAction !== 'pushToService') { - throw new Error('[Action][ExternalService] subAction not implemented.'); - } - - if (subAction === 'pushToService') { - const pushToServiceParams = subActionParams as ExecutorSubActionPushParams; - const { comments, externalId, ...restParams } = pushToServiceParams; - - const mapping = buildMap(config.casesConfiguration.mapping); - const externalCase = mapParams( - restParams as ExecutorSubActionPushParams, - mapping - ); - - data = await api.pushToService({ - externalService, - mapping, - params: { ...pushToServiceParams, externalCase }, - }); - } - - return { - ...res, - data, - }; -}; - -export const createConnector = ({ - api, - config, - validate, - createExternalService, - validationSchema, - logger, -}: CreateExternalServiceArgs) => { - return ({ - configurationUtilities, - executor = createConnectorExecutor({ api, createExternalService, logger }), - }: CreateActionTypeArgs): ActionType => ({ - ...config, - validate: { - config: schema.object(validationSchema.config, { - validate: curry(validate.config)(configurationUtilities), - }), - secrets: schema.object(validationSchema.secrets, { - validate: curry(validate.secrets)(configurationUtilities), - }), - params: ExecutorParamsSchema, - }, - executor, - }); -}; - export const prepareFieldsForTransformation = ({ externalCase, mapping, @@ -165,11 +65,15 @@ export const prepareFieldsForTransformation = ({ }); }; -export const transformFields = ({ +export const transformFields = < + P extends EntityInformation, + S extends Record, + R extends {} +>({ params, fields, currentIncident, -}: TransformFieldsArgs): Record => { +}: TransformFieldsArgs): R => { return fields.reduce((prev, cur) => { const transform = flow(...cur.pipes.map((p) => transformers[p])); return { @@ -177,18 +81,11 @@ export const transformFields = ({ [cur.key]: transform({ value: cur.value, date: params.updatedAt ?? params.createdAt, - user: - (params.updatedBy != null - ? params.updatedBy.fullName - ? params.updatedBy.fullName - : params.updatedBy.username - : params.createdBy.fullName - ? params.createdBy.fullName - : params.createdBy.username) ?? '', + user: getEntity(params), previousValue: currentIncident ? currentIncident[cur.key] : '', }).value, }; - }, {}); + }, {} as R); }; export const transformComments = (comments: Comment[], pipes: string[]): Comment[] => { @@ -197,18 +94,18 @@ export const transformComments = (comments: Comment[], pipes: string[]): Comment comment: flow(...pipes.map((p) => transformers[p]))({ value: c.comment, date: c.updatedAt ?? c.createdAt, - user: - (c.updatedBy != null - ? c.updatedBy.fullName - ? c.updatedBy.fullName - : c.updatedBy.username - : c.createdBy.fullName - ? c.createdBy.fullName - : c.createdBy.username) ?? '', + user: getEntity(c), }).value, })); }; -export const getErrorMessage = (connector: string, msg: string) => { - return `[Action][${connector}]: ${msg}`; -}; +export const getEntity = (entity: EntityInformation): string => + (entity.updatedBy != null + ? entity.updatedBy.fullName + ? entity.updatedBy.fullName + : entity.updatedBy.username + : entity.createdBy != null + ? entity.createdBy.fullName + ? entity.createdBy.fullName + : entity.createdBy.username + : '') ?? ''; diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/case/validators.ts deleted file mode 100644 index 08e8a8be6a3e..000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/case/validators.ts +++ /dev/null @@ -1,35 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { isEmpty } from 'lodash'; - -import { ActionsConfigurationUtilities } from '../../actions_config'; -import { - ExternalIncidentServiceConfiguration, - ExternalIncidentServiceSecretConfiguration, -} from './types'; - -import * as i18n from './translations'; - -export const validateCommonConfig = ( - configurationUtilities: ActionsConfigurationUtilities, - configObject: ExternalIncidentServiceConfiguration -) => { - try { - if (isEmpty(configObject.casesConfiguration.mapping)) { - return i18n.MAPPING_EMPTY; - } - - configurationUtilities.ensureUriAllowed(configObject.apiUrl); - } catch (allowListError) { - return i18n.WHITE_LISTED_ERROR(allowListError.message); - } -}; - -export const validateCommonSecrets = ( - configurationUtilities: ActionsConfigurationUtilities, - secrets: ExternalIncidentServiceSecretConfiguration -) => {}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts index da47a4bfb839..a64eb7a2036c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { flow } from 'lodash'; import { ExternalServiceParams, PushToServiceApiHandlerArgs, @@ -15,14 +14,11 @@ import { GetFieldsByIssueTypeHandlerArgs, GetIssueTypesHandlerArgs, PushToServiceApiParams, + PushToServiceResponse, } from './types'; // TODO: to remove, need to support Case -import { transformers } from '../case/transformers'; -import { TransformFieldsArgs, Comment, EntityInformation } from '../case/common_types'; - -import { PushToServiceResponse } from './types'; -import { prepareFieldsForTransformation } from '../case/utils'; +import { prepareFieldsForTransformation, transformFields, transformComments } from '../case/utils'; const handshakeHandler = async ({ externalService, @@ -81,7 +77,7 @@ const pushToServiceHandler = async ({ defaultPipes, }); - incident = transformFields({ + incident = transformFields({ params, fields, currentIncident, @@ -132,47 +128,6 @@ const pushToServiceHandler = async ({ return res; }; -export const transformFields = ({ - params, - fields, - currentIncident, -}: TransformFieldsArgs): Incident => { - return fields.reduce((prev, cur) => { - const transform = flow(...cur.pipes.map((p) => transformers[p])); - return { - ...prev, - [cur.key]: transform({ - value: cur.value, - date: params.updatedAt ?? params.createdAt, - user: getEntity(params), - previousValue: currentIncident ? currentIncident[cur.key] : '', - }).value, - }; - }, {} as Incident); -}; - -export const transformComments = (comments: Comment[], pipes: string[]): Comment[] => { - return comments.map((c) => ({ - ...c, - comment: flow(...pipes.map((p) => transformers[p]))({ - value: c.comment, - date: c.updatedAt ?? c.createdAt, - user: getEntity(c), - }).value, - })); -}; - -export const getEntity = (entity: EntityInformation): string => - (entity.updatedBy != null - ? entity.updatedBy.fullName - ? entity.updatedBy.fullName - : entity.updatedBy.username - : entity.createdBy != null - ? entity.createdBy.fullName - ? entity.createdBy.fullName - : entity.createdBy.username - : '') ?? ''; - export const api: ExternalServiceApi = { handshake: handshakeHandler, pushToService: pushToServiceHandler, diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts index e7841996fede..53f8d43ebc2d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts @@ -6,7 +6,7 @@ import { ExternalService, PushToServiceApiParams, ExecutorSubActionPushParams } from './types'; -import { MapRecord } from '../case/common_types'; +import { MapRecord } from '../case/types'; const createMock = (): jest.Mocked => { const service = { diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts index 07c8e22812b2..9fee465e72ef 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts @@ -5,11 +5,7 @@ */ import { schema } from '@kbn/config-schema'; -import { - CommentSchema, - EntityInformation, - IncidentConfigurationSchema, -} from '../case/common_schema'; +import { CommentSchema, EntityInformation, IncidentConfigurationSchema } from '../case/schema'; export const ExternalIncidentServiceConfiguration = { apiUrl: schema.string(), diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts index 5e97f5309f8e..6fe7c62976f2 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts @@ -19,8 +19,8 @@ import { ExecutorSubActionGetFieldsByIssueTypeParamsSchema, } from './schema'; import { ActionsConfigurationUtilities } from '../../actions_config'; -import { IncidentConfigurationSchema } from '../case/common_schema'; -import { Comment } from '../case/common_types'; +import { IncidentConfigurationSchema } from '../case/schema'; +import { Comment } from '../case/types'; import { Logger } from '../../../../../../src/core/server'; export type JiraPublicConfigurationType = TypeOf; diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/api.test.ts index 734f6be38262..e974fedd0775 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/api.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/api.test.ts @@ -4,9 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { api } from '../case/api'; +import { Logger } from '../../../../../../src/core/server'; +import { api } from './api'; import { externalServiceMock, mapping, apiParams } from './mocks'; -import { ExternalService } from '../case/types'; +import { ExternalService } from './types'; + +let mockedLogger: jest.Mocked; describe('api', () => { let externalService: jest.Mocked; @@ -23,7 +26,12 @@ describe('api', () => { describe('create incident', () => { test('it creates an incident', async () => { const params = { ...apiParams, externalId: null }; - const res = await api.pushToService({ externalService, mapping, params }); + const res = await api.pushToService({ + externalService, + mapping, + params, + logger: mockedLogger, + }); expect(res).toEqual({ id: '1', @@ -45,7 +53,12 @@ describe('api', () => { test('it creates an incident without comments', async () => { const params = { ...apiParams, externalId: null, comments: [] }; - const res = await api.pushToService({ externalService, mapping, params }); + const res = await api.pushToService({ + externalService, + mapping, + params, + logger: mockedLogger, + }); expect(res).toEqual({ id: '1', @@ -57,7 +70,7 @@ describe('api', () => { test('it calls createIncident correctly', async () => { const params = { ...apiParams, externalId: null }; - await api.pushToService({ externalService, mapping, params }); + await api.pushToService({ externalService, mapping, params, logger: mockedLogger }); expect(externalService.createIncident).toHaveBeenCalledWith({ incident: { @@ -71,7 +84,7 @@ describe('api', () => { test('it calls createComment correctly', async () => { const params = { ...apiParams, externalId: null }; - await api.pushToService({ externalService, mapping, params }); + await api.pushToService({ externalService, mapping, params, logger: mockedLogger }); expect(externalService.createComment).toHaveBeenCalledTimes(2); expect(externalService.createComment).toHaveBeenNthCalledWith(1, { incidentId: '1', @@ -89,7 +102,6 @@ describe('api', () => { username: 'elastic', }, }, - field: 'comments', }); expect(externalService.createComment).toHaveBeenNthCalledWith(2, { @@ -108,14 +120,18 @@ describe('api', () => { username: 'elastic', }, }, - field: 'comments', }); }); }); describe('update incident', () => { test('it updates an incident', async () => { - const res = await api.pushToService({ externalService, mapping, params: apiParams }); + const res = await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(res).toEqual({ id: '1', @@ -137,7 +153,12 @@ describe('api', () => { test('it updates an incident without comments', async () => { const params = { ...apiParams, comments: [] }; - const res = await api.pushToService({ externalService, mapping, params }); + const res = await api.pushToService({ + externalService, + mapping, + params, + logger: mockedLogger, + }); expect(res).toEqual({ id: '1', @@ -149,7 +170,7 @@ describe('api', () => { test('it calls updateIncident correctly', async () => { const params = { ...apiParams }; - await api.pushToService({ externalService, mapping, params }); + await api.pushToService({ externalService, mapping, params, logger: mockedLogger }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', @@ -164,7 +185,7 @@ describe('api', () => { test('it calls createComment correctly', async () => { const params = { ...apiParams }; - await api.pushToService({ externalService, mapping, params }); + await api.pushToService({ externalService, mapping, params, logger: mockedLogger }); expect(externalService.createComment).toHaveBeenCalledTimes(2); expect(externalService.createComment).toHaveBeenNthCalledWith(1, { incidentId: '1', @@ -182,7 +203,6 @@ describe('api', () => { username: 'elastic', }, }, - field: 'comments', }); expect(externalService.createComment).toHaveBeenNthCalledWith(2, { @@ -201,11 +221,52 @@ describe('api', () => { username: 'elastic', }, }, - field: 'comments', }); }); }); + 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', + }, + ]); + }); + }); + describe('mapping variations', () => { test('overwrite & append', async () => { mapping.set('title', { @@ -228,7 +289,12 @@ describe('api', () => { actionType: 'overwrite', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -260,7 +326,12 @@ describe('api', () => { actionType: 'nothing', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -291,7 +362,12 @@ describe('api', () => { actionType: 'append', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -324,7 +400,12 @@ describe('api', () => { actionType: 'nothing', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: {}, @@ -352,7 +433,12 @@ describe('api', () => { actionType: 'overwrite', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -382,7 +468,12 @@ describe('api', () => { actionType: 'overwrite', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -414,7 +505,12 @@ describe('api', () => { actionType: 'nothing', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -445,7 +541,12 @@ describe('api', () => { actionType: 'append', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -478,7 +579,12 @@ describe('api', () => { actionType: 'append', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -509,7 +615,12 @@ describe('api', () => { actionType: 'overwrite', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(externalService.createComment).not.toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/api.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/api.ts index 3db66e5884af..af3984bf5f0f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/api.ts @@ -4,4 +4,129 @@ * you may not use this file except in compliance with the Elastic License. */ -export { api } from '../case/api'; +import { + ExternalServiceParams, + PushToServiceApiHandlerArgs, + HandshakeApiHandlerArgs, + GetIncidentApiHandlerArgs, + ExternalServiceApi, + Incident, + GetIncidentTypesHandlerArgs, + GetSeverityHandlerArgs, + PushToServiceApiParams, + PushToServiceResponse, +} from './types'; + +// TODO: to remove, need to support Case +import { transformFields, prepareFieldsForTransformation, transformComments } from '../case/utils'; + +const handshakeHandler = async ({ + externalService, + mapping, + params, +}: HandshakeApiHandlerArgs) => {}; + +const getIncidentHandler = async ({ + externalService, + mapping, + params, +}: GetIncidentApiHandlerArgs) => {}; + +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, + mapping, + params, + logger, +}: PushToServiceApiHandlerArgs): Promise => { + const { externalId, comments } = params; + const updateIncident = externalId ? true : false; + const defaultPipes = updateIncident ? ['informationUpdated'] : ['informationCreated']; + let currentIncident: ExternalServiceParams | undefined; + let res: PushToServiceResponse; + + if (externalId) { + try { + currentIncident = await externalService.getIncident(externalId); + } catch (ex) { + logger.debug( + `Retrieving Incident by id ${externalId} from IBM Resilient was failed with exception: ${ex}` + ); + } + } + + let incident: Incident; + // TODO: should be removed later but currently keep it for the Case implementation support + if (mapping) { + const fields = prepareFieldsForTransformation({ + externalCase: params.externalObject, + mapping, + defaultPipes, + }); + + incident = transformFields({ + params, + fields, + currentIncident, + }); + } else { + const { title, description, incidentTypes, severityCode } = params; + incident = { name: title, description, incidentTypes, severityCode }; + } + + if (externalId != null) { + res = await externalService.updateIncident({ + incidentId: externalId, + incident, + }); + } else { + res = await externalService.createIncident({ + incident: { + ...incident, + }, + }); + } + + if (comments && Array.isArray(comments) && comments.length > 0) { + if (mapping && mapping.get('comments')?.actionType === 'nothing') { + return res; + } + const commentsTransformed = mapping + ? transformComments(comments, ['informationAdded']) + : comments; + + res.comments = []; + for (const currentComment of commentsTransformed) { + 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 = { + handshake: handshakeHandler, + pushToService: pushToServiceHandler, + getIncident: getIncidentHandler, + incidentTypes: getIncidentTypesHandler, + severity: getSeverityHandler, +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/config.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/config.ts deleted file mode 100644 index 4ce9417bfa9a..000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/config.ts +++ /dev/null @@ -1,14 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ExternalServiceConfiguration } from '../case/types'; -import * as i18n from './translations'; - -export const config: ExternalServiceConfiguration = { - id: '.resilient', - name: i18n.NAME, - minimumLicenseRequired: 'platinum', -}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts index 1e9cb1558970..53285a2a350a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts @@ -4,33 +4,139 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Logger } from '../../../../../../src/core/server'; -import { createConnector } from '../case/utils'; +import { curry } from 'lodash'; +import { schema } from '@kbn/config-schema'; -import { api } from './api'; -import { config } from './config'; import { validate } from './validators'; -import { createExternalService } from './service'; -import { ResilientSecretConfiguration, ResilientPublicConfiguration } from './schema'; +import { + ExternalIncidentServiceConfiguration, + ExternalIncidentServiceSecretConfiguration, + ExecutorParamsSchema, +} from './schema'; import { ActionsConfigurationUtilities } from '../../actions_config'; -import { ActionType } from '../../types'; +import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../../types'; +import { createExternalService } from './service'; +import { api } from './api'; +import { + ExecutorParams, + ExecutorSubActionPushParams, + ResilientPublicConfigurationType, + ResilientSecretConfigurationType, + ResilientExecutorResultData, + ExecutorSubActionGetIncidentTypesParams, + ExecutorSubActionGetSeverityParams, +} from './types'; +import * as i18n from './translations'; +import { Logger } from '../../../../../../src/core/server'; -export function getActionType({ - logger, - configurationUtilities, -}: { +// TODO: to remove, need to support Case +import { buildMap, mapParams } from '../case/utils'; + +interface GetActionTypeParams { logger: Logger; configurationUtilities: ActionsConfigurationUtilities; -}): ActionType { - return createConnector({ - api, - config, - validate, - createExternalService, - validationSchema: { - config: ResilientPublicConfiguration, - secrets: ResilientSecretConfiguration, +} + +const supportedSubActions: string[] = ['pushToService', 'incidentTypes', 'severity']; + +// action type definition +export function getActionType( + params: GetActionTypeParams +): ActionType< + ResilientPublicConfigurationType, + ResilientSecretConfigurationType, + ExecutorParams, + ResilientExecutorResultData | {} +> { + const { logger, configurationUtilities } = params; + return { + id: '.resilient', + minimumLicenseRequired: 'platinum', + name: i18n.NAME, + validate: { + config: schema.object(ExternalIncidentServiceConfiguration, { + validate: curry(validate.config)(configurationUtilities), + }), + secrets: schema.object(ExternalIncidentServiceSecretConfiguration, { + validate: curry(validate.secrets)(configurationUtilities), + }), + params: ExecutorParamsSchema, + }, + executor: curry(executor)({ logger }), + }; +} + +// action executor +async function executor( + { logger }: { logger: Logger }, + execOptions: ActionTypeExecutorOptions< + ResilientPublicConfigurationType, + ResilientSecretConfigurationType, + ExecutorParams + > +): Promise> { + const { actionId, config, params, secrets } = execOptions; + const { subAction, subActionParams } = params as ExecutorParams; + let data: ResilientExecutorResultData | null = null; + + const externalService = createExternalService( + { + config, + secrets, }, logger, - })({ configurationUtilities }); + execOptions.proxySettings + ); + + 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; + + const { comments, externalId, ...restParams } = pushToServiceParams; + const mapping = config.incidentConfiguration + ? buildMap(config.incidentConfiguration.mapping) + : null; + const externalObject = + config.incidentConfiguration && mapping + ? mapParams(restParams as ExecutorSubActionPushParams, mapping) + : {}; + + data = await api.pushToService({ + externalService, + mapping, + params: { ...pushToServiceParams, externalObject }, + logger, + }); + + logger.debug(`response push to service for incident id: ${data.id}`); + } + + 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 }; } diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/mocks.ts index bba9c58bf28c..2e841728159a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/mocks.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/mocks.ts @@ -4,12 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - ExternalService, - PushToServiceApiParams, - ExecutorSubActionPushParams, - MapRecord, -} from '../case/types'; +import { ExternalService, PushToServiceApiParams, ExecutorSubActionPushParams } from './types'; + +import { MapRecord } from '../case/types'; const createMock = (): jest.Mocked => { const service = { @@ -40,6 +37,25 @@ const createMock = (): jest.Mocked => { }) ), 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(() => @@ -96,6 +112,8 @@ const executorParams: ExecutorSubActionPushParams = { updatedBy: { fullName: 'Elastic User', username: 'elastic' }, title: 'Incident title', description: 'Incident description', + incidentTypes: [1001], + severityCode: 6, comments: [ { commentId: 'case-comment-1', @@ -118,7 +136,58 @@ const executorParams: ExecutorSubActionPushParams = { const apiParams: PushToServiceApiParams = { ...executorParams, - externalCase: { name: 'Incident title', description: 'Incident description' }, + externalObject: { name: 'Incident title', description: 'Incident description' }, }; -export { externalServiceMock, mapping, executorParams, apiParams }; +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 = [ + { + value: 4, + label: 'Low', + enabled: true, + properties: null, + uuid: '97cae239-963d-4e36-be34-07e47ef2cc86', + hidden: false, + default: true, + }, + { + value: 5, + label: 'Medium', + enabled: true, + properties: null, + uuid: 'c2c354c9-6d1e-4a48-82e5-bd5dc5068339', + hidden: false, + default: false, + }, + { + value: 6, + label: 'High', + enabled: true, + properties: null, + uuid: '93e5c99c-563b-48b9-80a3-9572307622d8', + hidden: false, + default: false, + }, +]; + +export { externalServiceMock, mapping, executorParams, apiParams, incidentTypes, severity }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/schema.ts index c13de2b27e2b..151f703dcc07 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/schema.ts @@ -5,18 +5,77 @@ */ import { schema } from '@kbn/config-schema'; -import { ExternalIncidentServiceConfiguration } from '../case/schema'; +import { CommentSchema, EntityInformation, IncidentConfigurationSchema } from '../case/schema'; -export const ResilientPublicConfiguration = { +export const ExternalIncidentServiceConfiguration = { + apiUrl: schema.string(), orgId: schema.string(), - ...ExternalIncidentServiceConfiguration, + // TODO: to remove - set it optional for the current stage to support Case implementation + incidentConfiguration: schema.nullable(IncidentConfigurationSchema), + isCaseOwned: schema.nullable(schema.boolean()), }; -export const ResilientPublicConfigurationSchema = schema.object(ResilientPublicConfiguration); +export const ExternalIncidentServiceConfigurationSchema = schema.object( + ExternalIncidentServiceConfiguration +); -export const ResilientSecretConfiguration = { +export const ExternalIncidentServiceSecretConfiguration = { apiKeyId: schema.string(), apiKeySecret: schema.string(), }; -export const ResilientSecretConfigurationSchema = schema.object(ResilientSecretConfiguration); +export const ExternalIncidentServiceSecretConfigurationSchema = schema.object( + ExternalIncidentServiceSecretConfiguration +); + +export const ExecutorSubActionSchema = schema.oneOf([ + schema.literal('getIncident'), + schema.literal('pushToService'), + schema.literal('handshake'), + schema.literal('incidentTypes'), + schema.literal('severity'), +]); + +export const ExecutorSubActionPushParamsSchema = schema.object({ + savedObjectId: schema.string(), + title: schema.string(), + description: schema.nullable(schema.string()), + externalId: schema.nullable(schema.string()), + incidentTypes: schema.nullable(schema.arrayOf(schema.number())), + severityCode: schema.nullable(schema.number()), + // TODO: remove later - need for support Case push multiple comments + comments: schema.nullable(schema.arrayOf(CommentSchema)), + ...EntityInformation, +}); + +export const ExecutorSubActionGetIncidentParamsSchema = schema.object({ + externalId: schema.string(), +}); + +// Reserved for future implementation +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('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, + }), +]); diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts index a9271671f68b..86ea352625a5 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts @@ -8,9 +8,11 @@ import axios from 'axios'; import { createExternalService, getValueTextContent, formatUpdateRequest } from './service'; import * as utils from '../lib/axios_utils'; -import { ExternalService } from '../case/types'; +import { ExternalService } from './types'; import { Logger } from '../../../../../../src/core/server'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { incidentTypes, severity } from './mocks'; + const logger = loggingSystemMock.create().get() as jest.Mocked; jest.mock('axios'); @@ -41,6 +43,8 @@ const mockIncidentUpdate = (withUpdateError = false) => { format: 'html', content: 'description', }, + incident_type_ids: [1001, 16, 12], + severity_code: 6, }, })); @@ -246,7 +250,12 @@ describe('IBM Resilient service', () => { })); const res = await service.createIncident({ - incident: { name: 'title', description: 'desc' }, + incident: { + name: 'title', + description: 'desc', + incidentTypes: [1001], + severityCode: 6, + }, }); expect(res).toEqual({ @@ -269,12 +278,18 @@ describe('IBM Resilient service', () => { })); await service.createIncident({ - incident: { name: 'title', description: 'desc' }, + incident: { + name: 'title', + description: 'desc', + incidentTypes: [1001], + severityCode: 6, + }, }); expect(requestMock).toHaveBeenCalledWith({ axios, - url: 'https://resilient.elastic.co/rest/orgs/201/incidents', + url: + 'https://resilient.elastic.co/rest/orgs/201/incidents?text_content_output_format=objects_convert', logger, method: 'post', data: { @@ -284,6 +299,8 @@ describe('IBM Resilient service', () => { content: 'desc', }, discovered_date: TIMESTAMP, + incident_type_ids: [{ id: 1001 }], + severity_code: { id: 6 }, }, }); }); @@ -295,7 +312,12 @@ describe('IBM Resilient service', () => { expect( service.createIncident({ - incident: { name: 'title', description: 'desc' }, + incident: { + name: 'title', + description: 'desc', + incidentTypes: [1001], + severityCode: 6, + }, }) ).rejects.toThrow( '[Action][IBM Resilient]: Unable to create incident. Error: An error has occurred' @@ -308,7 +330,12 @@ describe('IBM Resilient service', () => { mockIncidentUpdate(); const res = await service.updateIncident({ incidentId: '1', - incident: { name: 'title_updated', description: 'desc_updated' }, + incident: { + name: 'title', + description: 'desc', + incidentTypes: [1001], + severityCode: 6, + }, }); expect(res).toEqual({ @@ -324,7 +351,12 @@ describe('IBM Resilient service', () => { await service.updateIncident({ incidentId: '1', - incident: { name: 'title_updated', description: 'desc_updated' }, + incident: { + name: 'title_updated', + description: 'desc_updated', + incidentTypes: [1001], + severityCode: 5, + }, }); // Incident update makes three calls to the API. @@ -356,6 +388,28 @@ describe('IBM Resilient service', () => { }, }, }, + { + 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, + }, + }, ], }, }); @@ -367,7 +421,12 @@ describe('IBM Resilient service', () => { expect( service.updateIncident({ incidentId: '1', - incident: { name: 'title', description: 'desc' }, + incident: { + name: 'title', + description: 'desc', + incidentTypes: [1001], + severityCode: 5, + }, }) ).rejects.toThrow( '[Action][IBM Resilient]: Unable to update incident with id 1. Error: An error has occurred' @@ -386,8 +445,14 @@ describe('IBM Resilient service', () => { const res = await service.createComment({ incidentId: '1', - comment: { comment: 'comment', commentId: 'comment-1' }, - field: 'comments', + comment: { + comment: 'comment', + commentId: 'comment-1', + createdBy: null, + createdAt: null, + updatedAt: null, + updatedBy: null, + }, }); expect(res).toEqual({ @@ -407,8 +472,14 @@ describe('IBM Resilient service', () => { await service.createComment({ incidentId: '1', - comment: { comment: 'comment', commentId: 'comment-1' }, - field: 'my_field', + comment: { + comment: 'comment', + commentId: 'comment-1', + createdBy: null, + createdAt: null, + updatedAt: null, + updatedBy: null, + }, }); expect(requestMock).toHaveBeenCalledWith({ @@ -434,12 +505,82 @@ describe('IBM Resilient service', () => { expect( service.createComment({ incidentId: '1', - comment: { comment: 'comment', commentId: 'comment-1' }, - field: 'comments', + comment: { + comment: 'comment', + commentId: 'comment-1', + createdBy: null, + createdAt: null, + updatedAt: null, + updatedBy: null, + }, }) ).rejects.toThrow( '[Action][IBM Resilient]: Unable to create comment at incident with id 1. Error: An error has occurred' ); }); }); + + describe('getIncidentTypes', () => { + test('it creates the incident correctly', async () => { + requestMock.mockImplementation(() => ({ + 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'); + }); + + expect(service.getIncidentTypes()).rejects.toThrow( + '[Action][IBM Resilient]: Unable to get incident types. Error: An error has occurred.' + ); + }); + }); + + describe('getSeverity', () => { + test('it creates the incident correctly', async () => { + requestMock.mockImplementation(() => ({ + 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'); + }); + + expect(service.getIncidentTypes()).rejects.toThrow( + '[Action][IBM Resilient]: Unable to get incident types. Error: An error has occurred.' + ); + }); + }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/service.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.ts index b2150081f2c8..4bf1453641e4 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.ts @@ -5,44 +5,56 @@ */ import axios from 'axios'; +import { omitBy, isNil } from 'lodash/fp'; import { Logger } from '../../../../../../src/core/server'; -import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from '../case/types'; import { + ExternalServiceCredentials, + ExternalService, + ExternalServiceParams, + CreateCommentParams, + UpdateIncidentParams, + CreateIncidentParams, + CreateIncidentData, ResilientPublicConfigurationType, ResilientSecretConfigurationType, - CreateIncidentRequest, UpdateIncidentRequest, - CreateCommentRequest, - UpdateFieldText, - UpdateFieldTextArea, + GetValueTextContentResponse, } from './types'; import * as i18n from './translations'; import { getErrorMessage, request } from '../lib/axios_utils'; import { ProxySettings } from '../../types'; -const BASE_URL = `rest`; -const INCIDENT_URL = `incidents`; -const COMMENT_URL = `comments`; - const VIEW_INCIDENT_URL = `#incidents`; export const getValueTextContent = ( field: string, - value: string -): UpdateFieldText | UpdateFieldTextArea => { + value: string | number | number[] +): GetValueTextContentResponse => { if (field === 'description') { return { textarea: { format: 'html', - content: value, + content: value as string, }, }; } + if (field === 'incidentTypes') { + return { + ids: value as number[], + }; + } + + if (field === 'severityCode') { + return { + id: value as number, + }; + } + return { - text: value, + text: value as string, }; }; @@ -51,11 +63,30 @@ export const formatUpdateRequest = ({ newIncident, }: ExternalServiceParams): UpdateIncidentRequest => { return { - changes: Object.keys(newIncident).map((key) => ({ - field: { name: key }, - old_value: getValueTextContent(key, oldIncident[key]), - new_value: getValueTextContent(key, newIncident[key]), - })), + changes: Object.keys(newIncident as Record).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)[name] as string + ), + new_value: getValueTextContent( + key, + (newIncident as Record)[key] as string + ), + }; + }), }; }; @@ -72,8 +103,12 @@ export const createExternalService = ( } const urlWithoutTrailingSlash = url.endsWith('/') ? url.slice(0, -1) : url; - const incidentUrl = `${urlWithoutTrailingSlash}/${BASE_URL}/orgs/${orgId}/${INCIDENT_URL}`; - const commentUrl = `${incidentUrl}/{inc_id}/${COMMENT_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 }, }); @@ -101,26 +136,48 @@ export const createExternalService = ( 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}`) + getErrorMessage(i18n.NAME, `Unable to get incident with id ${id}. Error: ${error.message}.`) ); } }; - const createIncident = async ({ incident }: ExternalServiceParams) => { + 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({ + const res = await request({ axios: axiosInstance, - url: `${incidentUrl}`, + url: `${incidentUrl}?text_content_output_format=objects_convert`, method: 'post', logger, - data: { - ...incident, - description: { - format: 'html', - content: incident.description ?? '', - }, - discovered_date: Date.now(), - }, + data, proxySettings, }); @@ -132,17 +189,20 @@ export const createExternalService = ( }; } catch (error) { throw new Error( - getErrorMessage(i18n.NAME, `Unable to create incident. Error: ${error.message}`) + getErrorMessage(i18n.NAME, `Unable to create incident. Error: ${error.message}.`) ); } }; - const updateIncident = async ({ incidentId, incident }: ExternalServiceParams) => { + const updateIncident = async ({ incidentId, incident }: UpdateIncidentParams) => { try { const latestIncident = await getIncident(incidentId); - const data = formatUpdateRequest({ oldIncident: latestIncident, newIncident: incident }); - const res = await request({ + // 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}`, @@ -173,9 +233,9 @@ export const createExternalService = ( } }; - const createComment = async ({ incidentId, comment, field }: ExternalServiceParams) => { + const createComment = async ({ incidentId, comment }: CreateCommentParams) => { try { - const res = await request({ + const res = await request({ axios: axiosInstance, method: 'post', url: getCommentsURL(incidentId), @@ -193,16 +253,62 @@ export const createExternalService = ( throw new Error( getErrorMessage( i18n.NAME, - `Unable to create comment at incident with id ${incidentId}. Error: ${error.message}` + `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, + proxySettings, + }); + + 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, + proxySettings, + }); + + 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}.`) + ); + } + }; + return { getIncident, createIncident, updateIncident, createComment, + getIncidentTypes, + getSeverity, }; }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/translations.ts index d952838d5a2b..8c6ce9902da8 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/translations.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/translations.ts @@ -9,3 +9,19 @@ import { i18n } from '@kbn/i18n'; export const NAME = i18n.translate('xpack.actions.builtin.case.resilientTitle', { defaultMessage: 'IBM Resilient', }); + +export const ALLOWED_HOSTS_ERROR = (message: string) => + i18n.translate('xpack.actions.builtin.configuration.apiAllowedHostsError', { + defaultMessage: 'error configuring connector action: {message}', + values: { + message, + }, + }); + +// TODO: remove when Case mappings will be removed +export const MAPPING_EMPTY = i18n.translate( + 'xpack.actions.builtin.servicenow.configuration.emptyMapping', + { + defaultMessage: '[incidentConfiguration.mapping]: expected non-empty but got empty', + } +); diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/types.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/types.ts index 6869e2ff3a10..ed622ee473b6 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/types.ts @@ -4,29 +4,175 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + import { TypeOf } from '@kbn/config-schema'; -import { ResilientPublicConfigurationSchema, ResilientSecretConfigurationSchema } from './schema'; +import { + ExternalIncidentServiceConfigurationSchema, + ExternalIncidentServiceSecretConfigurationSchema, + ExecutorParamsSchema, + ExecutorSubActionPushParamsSchema, + ExecutorSubActionGetIncidentParamsSchema, + ExecutorSubActionHandshakeParamsSchema, + ExecutorSubActionGetIncidentTypesParamsSchema, + ExecutorSubActionGetSeverityParamsSchema, +} from './schema'; -export type ResilientPublicConfigurationType = TypeOf; -export type ResilientSecretConfigurationType = TypeOf; +import { ActionsConfigurationUtilities } from '../../actions_config'; +import { Logger } from '../../../../../../src/core/server'; -interface CreateIncidentBasicRequestArgs { +import { IncidentConfigurationSchema } from '../case/schema'; +import { Comment } from '../case/types'; + +export type ResilientPublicConfigurationType = TypeOf< + typeof ExternalIncidentServiceConfigurationSchema +>; +export type ResilientSecretConfigurationType = TypeOf< + typeof ExternalIncidentServiceSecretConfigurationSchema +>; + +export type ExecutorParams = TypeOf; +export type ExecutorSubActionPushParams = TypeOf; + +export type IncidentConfiguration = TypeOf; + +export interface ExternalServiceCredentials { + config: Record; + secrets: Record; +} + +export interface ExternalServiceValidation { + config: (configurationUtilities: ActionsConfigurationUtilities, configObject: any) => void; + secrets: (configurationUtilities: ActionsConfigurationUtilities, secrets: any) => void; +} + +export interface ExternalServiceIncidentResponse { + id: string; + title: string; + url: string; + pushedDate: string; +} + +export interface ExternalServiceCommentResponse { + commentId: string; + pushedDate: string; + externalCommentId?: string; +} + +export type ExternalServiceParams = Record; + +export type Incident = Pick< + ExecutorSubActionPushParams, + 'description' | 'incidentTypes' | 'severityCode' +> & { name: string; - description: string; - discovered_date: number; +}; + +export interface CreateIncidentParams { + incident: Incident; } -interface Comment { - text: { format: string; content: string }; +export interface UpdateIncidentParams { + incidentId: string; + incident: Incident; } -interface CreateIncidentRequestArgs extends CreateIncidentBasicRequestArgs { - comments?: Comment[]; +export interface CreateCommentParams { + incidentId: string; + comment: Comment; } +export type GetIncidentTypesResponse = Array<{ id: string; name: string }>; +export type GetSeverityResponse = Array<{ id: string; name: string }>; + +export interface ExternalService { + getIncident: (id: string) => Promise; + createIncident: (params: CreateIncidentParams) => Promise; + updateIncident: (params: UpdateIncidentParams) => Promise; + createComment: (params: CreateCommentParams) => Promise; + getIncidentTypes: () => Promise; + getSeverity: () => Promise; +} + +export interface PushToServiceApiParams extends ExecutorSubActionPushParams { + externalObject: Record; +} + +export type ExecutorSubActionGetIncidentTypesParams = TypeOf< + typeof ExecutorSubActionGetIncidentTypesParamsSchema +>; + +export type ExecutorSubActionGetSeverityParams = TypeOf< + typeof ExecutorSubActionGetSeverityParamsSchema +>; + +export interface ExternalServiceApiHandlerArgs { + externalService: ExternalService; + mapping: Map | null; +} + +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 GetIncidentTypesHandlerArgs { + externalService: ExternalService; + params: ExecutorSubActionGetIncidentTypesParams; +} + +export interface GetSeverityHandlerArgs { + externalService: ExternalService; + params: ExecutorSubActionGetSeverityParams; +} + +export interface PushToServiceResponse extends ExternalServiceIncidentResponse { + comments?: ExternalServiceCommentResponse[]; +} + +export interface ExternalServiceApi { + handshake: (args: HandshakeApiHandlerArgs) => Promise; + pushToService: (args: PushToServiceApiHandlerArgs) => Promise; + getIncident: (args: GetIncidentApiHandlerArgs) => Promise; + incidentTypes: (args: GetIncidentTypesHandlerArgs) => Promise; + severity: (args: GetSeverityHandlerArgs) => Promise; +} + +export type ResilientExecutorResultData = + | PushToServiceResponse + | GetIncidentTypesResponse + | GetSeverityResponse; + export interface UpdateFieldText { text: string; } +export interface UpdateFieldText { + text: string; +} + +export interface UpdateIdsField { + ids: number[]; +} + +export interface UpdateIdField { + id: number; +} export interface UpdateFieldTextArea { textarea: { format: 'html' | 'text'; content: string }; @@ -34,13 +180,24 @@ export interface UpdateFieldTextArea { interface UpdateField { field: { name: string }; - old_value: UpdateFieldText | UpdateFieldTextArea; - new_value: UpdateFieldText | UpdateFieldTextArea; + old_value: UpdateFieldText | UpdateFieldTextArea | UpdateIdsField | UpdateIdField; + new_value: UpdateFieldText | UpdateFieldTextArea | UpdateIdsField | UpdateIdField; } -export type CreateIncidentRequest = CreateIncidentRequestArgs; -export type CreateCommentRequest = Comment; - export interface UpdateIncidentRequest { changes: UpdateField[]; } + +export type GetValueTextContentResponse = + | UpdateFieldText + | UpdateFieldTextArea + | UpdateIdsField + | UpdateIdField; + +export interface CreateIncidentData { + name: string; + discovered_date: number; + description?: { format: string; content: string }; + incident_type_ids?: Array<{ id: number }>; + severity_code?: { id: number }; +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/validators.ts index 7226071392bc..a50e868cdda3 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/validators.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/validators.ts @@ -4,8 +4,38 @@ * you may not use this file except in compliance with the Elastic License. */ -import { validateCommonConfig, validateCommonSecrets } from '../case/validators'; -import { ExternalServiceValidation } from '../case/types'; +import { isEmpty } from 'lodash'; +import { ActionsConfigurationUtilities } from '../../actions_config'; +import { + ResilientPublicConfigurationType, + ResilientSecretConfigurationType, + ExternalServiceValidation, +} from './types'; + +import * as i18n from './translations'; + +export const validateCommonConfig = ( + configurationUtilities: ActionsConfigurationUtilities, + configObject: ResilientPublicConfigurationType +) => { + if ( + configObject.incidentConfiguration !== null && + isEmpty(configObject.incidentConfiguration.mapping) + ) { + return i18n.MAPPING_EMPTY; + } + + try { + configurationUtilities.ensureUriAllowed(configObject.apiUrl); + } catch (allowedListError) { + return i18n.ALLOWED_HOSTS_ERROR(allowedListError.message); + } +}; + +export const validateCommonSecrets = ( + configurationUtilities: ActionsConfigurationUtilities, + secrets: ResilientSecretConfigurationType +) => {}; export const validate: ExternalServiceValidation = { config: validateCommonConfig, diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts index c8e6147ecef4..455a71517fb4 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { flow } from 'lodash'; import { ExternalServiceParams, PushToServiceApiHandlerArgs, @@ -12,12 +11,11 @@ import { ExternalServiceApi, PushToServiceApiParams, PushToServiceResponse, + Incident, } from './types'; // TODO: to remove, need to support Case -import { transformers } from '../case/transformers'; -import { TransformFieldsArgs, Comment, EntityInformation } from '../case/common_types'; -import { prepareFieldsForTransformation } from '../case/utils'; +import { transformFields, transformComments, prepareFieldsForTransformation } from '../case/utils'; const handshakeHandler = async ({ externalService, @@ -62,7 +60,7 @@ const pushToServiceHandler = async ({ defaultPipes, }); - incident = transformFields({ + incident = transformFields({ params, fields, currentIncident, @@ -117,47 +115,6 @@ const pushToServiceHandler = async ({ return res; }; -export const transformFields = ({ - params, - fields, - currentIncident, -}: TransformFieldsArgs): Record => { - return fields.reduce((prev, cur) => { - const transform = flow(...cur.pipes.map((p) => transformers[p])); - return { - ...prev, - [cur.key]: transform({ - value: cur.value, - date: params.updatedAt ?? params.createdAt, - user: getEntity(params), - previousValue: currentIncident ? currentIncident[cur.key] : '', - }).value, - }; - }, {}); -}; - -export const transformComments = (comments: Comment[], pipes: string[]): Comment[] => { - return comments.map((c) => ({ - ...c, - comment: flow(...pipes.map((p) => transformers[p]))({ - value: c.comment, - date: c.updatedAt ?? c.createdAt, - user: getEntity(c), - }).value, - })); -}; - -export const getEntity = (entity: EntityInformation): string => - (entity.updatedBy != null - ? entity.updatedBy.fullName - ? entity.updatedBy.fullName - : entity.updatedBy.username - : entity.createdBy != null - ? entity.createdBy.fullName - ? entity.createdBy.fullName - : entity.createdBy.username - : '') ?? ''; - export const api: ExternalServiceApi = { handshake: handshakeHandler, pushToService: pushToServiceHandler, diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts index 55a14e4528ac..f34e9714b22c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts @@ -5,7 +5,7 @@ */ import { ExternalService, PushToServiceApiParams, ExecutorSubActionPushParams } from './types'; -import { MapRecord } from '../case/common_types'; +import { MapRecord } from '../case/types'; const createMock = (): jest.Mocked => { const service = { diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts index 921de42adfca..9896d4175954 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts @@ -5,11 +5,7 @@ */ import { schema } from '@kbn/config-schema'; -import { - CommentSchema, - EntityInformation, - IncidentConfigurationSchema, -} from '../case/common_schema'; +import { CommentSchema, EntityInformation, IncidentConfigurationSchema } from '../case/schema'; export const ExternalIncidentServiceConfiguration = { apiUrl: schema.string(), diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts index e8fcfac45d78..a6a0ac946fe9 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts @@ -16,8 +16,8 @@ import { ExecutorSubActionHandshakeParamsSchema, } from './schema'; import { ActionsConfigurationUtilities } from '../../actions_config'; -import { ExternalServiceCommentResponse } from '../case/common_types'; -import { IncidentConfigurationSchema } from '../case/common_schema'; +import { ExternalServiceCommentResponse } from '../case/types'; +import { IncidentConfigurationSchema } from '../case/schema'; import { Logger } from '../../../../../../src/core/server'; export type ServiceNowPublicConfigurationType = TypeOf< @@ -82,6 +82,13 @@ export type ExecutorSubActionHandshakeParams = TypeOf< typeof ExecutorSubActionHandshakeParamsSchema >; +export type Incident = Pick< + ExecutorSubActionPushParams, + 'description' | 'severity' | 'urgency' | 'impact' +> & { + short_description: string; +}; + export interface PushToServiceApiHandlerArgs extends ExternalServiceApiHandlerArgs { params: PushToServiceApiParams; secrets: Record; diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts index a22d7ae5cea2..545ccf82c3d7 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts @@ -13,6 +13,7 @@ import { SUPPORTED_CONNECTORS, SERVICENOW_ACTION_TYPE_ID, JIRA_ACTION_TYPE_ID, + RESILIENT_ACTION_TYPE_ID, } from '../../../../../common/constants'; /* @@ -37,8 +38,12 @@ export function initCaseConfigureGetActionConnector({ caseService, router }: Rou (action) => SUPPORTED_CONNECTORS.includes(action.actionTypeId) && // Need this filtering temporary to display only Case owned ServiceNow connectors - (![SERVICENOW_ACTION_TYPE_ID, JIRA_ACTION_TYPE_ID].includes(action.actionTypeId) || - ([SERVICENOW_ACTION_TYPE_ID, JIRA_ACTION_TYPE_ID].includes(action.actionTypeId) && + (![SERVICENOW_ACTION_TYPE_ID, JIRA_ACTION_TYPE_ID, RESILIENT_ACTION_TYPE_ID].includes( + action.actionTypeId + ) || + ([SERVICENOW_ACTION_TYPE_ID, JIRA_ACTION_TYPE_ID, RESILIENT_ACTION_TYPE_ID].includes( + action.actionTypeId + ) && action.config?.isCaseOwned === true)) ); return response.ok({ body: results }); diff --git a/x-pack/plugins/security_solution/public/cases/containers/configure/mock.ts b/x-pack/plugins/security_solution/public/cases/containers/configure/mock.ts index 9b9e978ffca4..2fc761f4dc42 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/configure/mock.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/configure/mock.ts @@ -77,7 +77,7 @@ export const connectorsMock: Connector[] = [ name: 'Jira', config: { apiUrl: 'https://instance.atlassian.ne', - casesConfiguration: { + incidentConfiguration: { mapping: [ { source: 'title', diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/components/connector_flyout/index.tsx b/x-pack/plugins/security_solution/public/common/lib/connectors/components/connector_flyout/index.tsx deleted file mode 100644 index 30e2c650a70c..000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/components/connector_flyout/index.tsx +++ /dev/null @@ -1,157 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useCallback, useEffect } from 'react'; -import { EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSpacer } from '@elastic/eui'; - -import { isEmpty, get } from 'lodash/fp'; - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ActionConnectorFieldsProps } from '../../../../../../../triggers_actions_ui/public/types'; -import { FieldMapping } from '../../../../../cases/components/configure_cases/field_mapping'; - -import { CasesConfigurationMapping } from '../../../../../cases/containers/configure/types'; - -import * as i18n from '../../translations'; -import { ActionConnector, ConnectorFlyoutHOCProps } from '../../types'; -import { createDefaultMapping } from '../../utils'; -import { connectorsConfiguration } from '../../config'; - -export const withConnectorFlyout = ({ - ConnectorFormComponent, - connectorActionTypeId, - secretKeys = [], - configKeys = [], -}: ConnectorFlyoutHOCProps) => { - const ConnectorFlyout: React.FC> = ({ - action, - editActionConfig, - editActionSecrets, - errors, - }) => { - /* We do not provide defaults values to the fields (like empty string for apiUrl) intentionally. - * If we do, errors will be shown the first time the flyout is open even though the user did not - * interact with the form. Also, we would like to show errors for empty fields provided by the user. - /*/ - const { apiUrl, casesConfiguration: { mapping = [] } = {} } = action.config; - const configKeysWithDefault = [...configKeys, 'apiUrl']; - - const isApiUrlInvalid: boolean = errors.apiUrl.length > 0 && apiUrl != null; - - /** - * We need to distinguish between the add flyout and the edit flyout. - * useEffect will run only once on component mount. - * This guarantees that the function below will run only once. - * On the first render of the component the apiUrl can be either undefined or filled. - * If it is filled then we are on the edit flyout. Otherwise we are on the add flyout. - */ - - useEffect(() => { - if (!isEmpty(apiUrl)) { - secretKeys.forEach((key: string) => editActionSecrets(key, '')); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - if (isEmpty(mapping)) { - editActionConfig('casesConfiguration', { - ...action.config.casesConfiguration, - mapping: createDefaultMapping(connectorsConfiguration[connectorActionTypeId].fields), - }); - } - - const handleOnChangeActionConfig = useCallback( - (key: string, value: string) => editActionConfig(key, value), - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - - const handleOnBlurActionConfig = useCallback( - (key: string) => { - if (configKeysWithDefault.includes(key) && get(key, action.config) == null) { - editActionConfig(key, ''); - } - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [action.config] - ); - - const handleOnChangeSecretConfig = useCallback( - (key: string, value: string) => editActionSecrets(key, value), - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - - const handleOnBlurSecretConfig = useCallback( - (key: string) => { - if (secretKeys.includes(key) && get(key, action.secrets) == null) { - editActionSecrets(key, ''); - } - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [action.secrets] - ); - - const handleOnChangeMappingConfig = useCallback( - (newMapping: CasesConfigurationMapping[]) => - editActionConfig('casesConfiguration', { - ...action.config.casesConfiguration, - mapping: newMapping, - }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [action.config] - ); - - return ( - <> - - - - handleOnChangeActionConfig('apiUrl', evt.target.value)} - onBlur={handleOnBlurActionConfig.bind(null, 'apiUrl')} - /> - - - - - - - - - - - - - ); - }; - - return ConnectorFlyout; -}; diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/config.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/config.ts index 9e6982ea2030..3aca18637882 100644 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/config.ts +++ b/x-pack/plugins/security_solution/public/common/lib/connectors/config.ts @@ -9,12 +9,12 @@ import { ServiceNowConnectorConfiguration, JiraConnectorConfiguration, + ResilientConnectorConfiguration, } from '../../../../../triggers_actions_ui/public/common'; -import { connector as resilientConnectorConfig } from './resilient/config'; import { ConnectorConfiguration } from './types'; export const connectorsConfiguration: Record = { '.servicenow': ServiceNowConnectorConfiguration as ConnectorConfiguration, '.jira': JiraConnectorConfiguration as ConnectorConfiguration, - '.resilient': resilientConnectorConfig, + '.resilient': ResilientConnectorConfiguration as ConnectorConfiguration, }; diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/flyout.tsx b/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/flyout.tsx deleted file mode 100644 index 31bf0a4dfc34..000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/flyout.tsx +++ /dev/null @@ -1,114 +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; - * you may not use this file except in compliance with the Elastic License. - */ -import React from 'react'; -import { - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiFieldPassword, - EuiSpacer, -} from '@elastic/eui'; - -import * as i18n from './translations'; -import { ConnectorFlyoutFormProps } from '../types'; -import { ResilientActionConnector } from './types'; -import { withConnectorFlyout } from '../components/connector_flyout'; - -const resilientConnectorForm: React.FC> = ({ - errors, - action, - onChangeSecret, - onBlurSecret, - onChangeConfig, - onBlurConfig, -}) => { - const { orgId } = action.config; - const { apiKeyId, apiKeySecret } = action.secrets; - const isOrgIdInvalid: boolean = errors.orgId.length > 0 && orgId != null; - const isApiKeyIdInvalid: boolean = errors.apiKeyId.length > 0 && apiKeyId != null; - const isApiKeySecretInvalid: boolean = errors.apiKeySecret.length > 0 && apiKeySecret != null; - - return ( - <> - - - - onChangeConfig('orgId', evt.target.value)} - onBlur={() => onBlurConfig('orgId')} - /> - - - - - - - - onChangeSecret('apiKeyId', evt.target.value)} - onBlur={() => onBlurSecret('apiKeyId')} - /> - - - - - - - - onChangeSecret('apiKeySecret', evt.target.value)} - onBlur={() => onBlurSecret('apiKeySecret')} - /> - - - - - ); -}; - -export const resilientConnectorFlyout = withConnectorFlyout({ - ConnectorFormComponent: resilientConnectorForm, - secretKeys: ['apiKeyId', 'apiKeySecret'], - configKeys: ['orgId'], - connectorActionTypeId: '.resilient', -}); - -// eslint-disable-next-line import/no-default-export -export { resilientConnectorFlyout as default }; diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/index.tsx b/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/index.tsx deleted file mode 100644 index ba4879e87a1f..000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/index.tsx +++ /dev/null @@ -1,54 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { lazy } from 'react'; -import { - ValidationResult, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../triggers_actions_ui/public/types'; - -import { connector } from './config'; -import { createActionType } from '../utils'; -import logo from './logo.svg'; -import { ResilientActionConnector } from './types'; -import * as i18n from './translations'; - -interface Errors { - orgId: string[]; - apiKeyId: string[]; - apiKeySecret: string[]; -} - -const validateConnector = (action: ResilientActionConnector): ValidationResult => { - const errors: Errors = { - orgId: [], - apiKeyId: [], - apiKeySecret: [], - }; - - if (!action.config.orgId) { - errors.orgId = [...errors.orgId, i18n.RESILIENT_PROJECT_KEY_REQUIRED]; - } - - if (!action.secrets.apiKeyId) { - errors.apiKeyId = [...errors.apiKeyId, i18n.RESILIENT_API_KEY_ID_REQUIRED]; - } - - if (!action.secrets.apiKeySecret) { - errors.apiKeySecret = [...errors.apiKeySecret, i18n.RESILIENT_API_KEY_SECRET_REQUIRED]; - } - - return { errors }; -}; - -export const getActionType = createActionType({ - id: connector.id, - iconClass: logo, - selectMessage: i18n.RESILIENT_DESC, - actionTypeTitle: connector.name, - validateConnector, - actionConnectorFields: lazy(() => import('./flyout')), -}); diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/translations.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/translations.ts deleted file mode 100644 index 2ff97ad35409..000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/translations.ts +++ /dev/null @@ -1,72 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -export * from '../translations'; - -export const RESILIENT_DESC = i18n.translate( - 'xpack.securitySolution.case.connectors.resilient.selectMessageText', - { - defaultMessage: 'Push or update Security case data to a new issue in Resilient', - } -); - -export const RESILIENT_TITLE = i18n.translate( - 'xpack.securitySolution.case.connectors.resilient.actionTypeTitle', - { - defaultMessage: 'IBM Resilient', - } -); - -export const RESILIENT_PROJECT_KEY_LABEL = i18n.translate( - 'xpack.securitySolution.case.connectors.resilient.orgId', - { - defaultMessage: 'Organization ID', - } -); - -export const RESILIENT_PROJECT_KEY_REQUIRED = i18n.translate( - 'xpack.securitySolution.case.connectors.resilient.requiredOrgIdTextField', - { - defaultMessage: 'Organization ID is required', - } -); - -export const RESILIENT_API_KEY_ID_LABEL = i18n.translate( - 'xpack.securitySolution.case.connectors.resilient.apiKeyId', - { - defaultMessage: 'API key ID', - } -); - -export const RESILIENT_API_KEY_ID_REQUIRED = i18n.translate( - 'xpack.securitySolution.case.connectors.resilient.requiredApiKeyIdTextField', - { - defaultMessage: 'API key ID is required', - } -); - -export const RESILIENT_API_KEY_SECRET_LABEL = i18n.translate( - 'xpack.securitySolution.case.connectors.resilient.apiKeySecret', - { - defaultMessage: 'API key secret', - } -); - -export const RESILIENT_API_KEY_SECRET_REQUIRED = i18n.translate( - 'xpack.securitySolution.case.connectors.resilient.requiredApiKeySecretTextField', - { - defaultMessage: 'API key secret is required', - } -); - -export const MAPPING_FIELD_NAME = i18n.translate( - 'xpack.securitySolution.case.configureCases.mappingFieldName', - { - defaultMessage: 'Name', - } -); diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/types.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/types.ts deleted file mode 100644 index fe6dbb2b3674..000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/types.ts +++ /dev/null @@ -1,22 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -/* eslint-disable no-restricted-imports */ -/* eslint-disable @kbn/eslint/no-restricted-paths */ - -import { - ResilientPublicConfigurationType, - ResilientSecretConfigurationType, -} from '../../../../../../actions/server/builtin_action_types/resilient/types'; - -export { ResilientFieldsType } from '../../../../../../case/common/api/connectors'; - -export * from '../types'; - -export interface ResilientActionConnector { - config: ResilientPublicConfigurationType; - secrets: ResilientSecretConfigurationType; -} diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/translations.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/translations.ts deleted file mode 100644 index 6dd1247d40fc..000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/translations.ts +++ /dev/null @@ -1,98 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -export const API_URL_LABEL = i18n.translate( - 'xpack.securitySolution.case.connectors.common.apiUrlTextFieldLabel', - { - defaultMessage: 'URL', - } -); - -export const API_URL_REQUIRED = i18n.translate( - 'xpack.securitySolution.case.connectors.common.requiredApiUrlTextField', - { - defaultMessage: 'URL is required', - } -); - -export const API_URL_INVALID = i18n.translate( - 'xpack.securitySolution.case.connectors.common.invalidApiUrlTextField', - { - defaultMessage: 'URL is invalid', - } -); - -export const USERNAME_LABEL = i18n.translate( - 'xpack.securitySolution.case.connectors.common.usernameTextFieldLabel', - { - defaultMessage: 'Username', - } -); - -export const USERNAME_REQUIRED = i18n.translate( - 'xpack.securitySolution.case.connectors.common.requiredUsernameTextField', - { - defaultMessage: 'Username is required', - } -); - -export const PASSWORD_LABEL = i18n.translate( - 'xpack.securitySolution.case.connectors.common.passwordTextFieldLabel', - { - defaultMessage: 'Password', - } -); - -export const PASSWORD_REQUIRED = i18n.translate( - 'xpack.securitySolution.case.connectors.common.requiredPasswordTextField', - { - defaultMessage: 'Password is required', - } -); - -export const API_TOKEN_LABEL = i18n.translate( - 'xpack.securitySolution.case.connectors.common.apiTokenTextFieldLabel', - { - defaultMessage: 'API token', - } -); - -export const API_TOKEN_REQUIRED = i18n.translate( - 'xpack.securitySolution.case.connectors.common.requiredApiTokenTextField', - { - defaultMessage: 'API token is required', - } -); - -export const EMAIL_LABEL = i18n.translate( - 'xpack.securitySolution.case.connectors.common.emailTextFieldLabel', - { - defaultMessage: 'Email', - } -); - -export const EMAIL_REQUIRED = i18n.translate( - 'xpack.securitySolution.case.connectors.common.requiredEmailTextField', - { - defaultMessage: 'Email is required', - } -); - -export const MAPPING_FIELD_DESC = i18n.translate( - 'xpack.securitySolution.case.configureCases.mappingFieldDescription', - { - defaultMessage: 'Description', - } -); - -export const MAPPING_FIELD_COMMENTS = i18n.translate( - 'xpack.securitySolution.case.configureCases.mappingFieldComments', - { - defaultMessage: 'Comments', - } -); diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/types.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/types.ts index 1d688ad9b1d6..5d83c226bfec 100644 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/types.ts +++ b/x-pack/plugins/security_solution/public/common/lib/connectors/types.ts @@ -4,12 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -/* eslint-disable no-restricted-imports */ -/* eslint-disable @kbn/eslint/no-restricted-paths */ - import { ActionType } from '../../../../../triggers_actions_ui/public'; -import { IErrorObject } from '../../../../../triggers_actions_ui/public/types'; -import { ExternalIncidentServiceConfiguration } from '../../../../../actions/server/builtin_action_types/case/types'; import { ActionType as ThirdPartySupportedActions, @@ -29,34 +24,3 @@ export interface ConnectorConfiguration extends ActionType { logo: string; fields: Record; } - -export interface ActionConnector { - config: ExternalIncidentServiceConfiguration; - secrets: {}; -} - -export interface ActionConnectorParams { - message: string; -} - -export interface ActionConnectorValidationErrors { - apiUrl: string[]; -} - -export type Optional = Omit & Partial; - -export interface ConnectorFlyoutFormProps { - errors: IErrorObject; - action: T; - onChangeSecret: (key: string, value: string) => void; - onBlurSecret: (key: string) => void; - onChangeConfig: (key: string, value: string) => void; - onBlurConfig: (key: string) => void; -} - -export interface ConnectorFlyoutHOCProps { - ConnectorFormComponent: React.FC>; - connectorActionTypeId: string; - configKeys?: string[]; - secretKeys?: string[]; -} diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/utils.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/utils.ts index 6e72205c145a..0a6dd37d9f9e 100644 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/utils.ts +++ b/x-pack/plugins/security_solution/public/common/lib/connectors/utils.ts @@ -4,63 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - ActionTypeModel, - ValidationResult, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../triggers_actions_ui/public/types'; - -import { - ActionConnector, - ActionConnectorParams, - ActionConnectorValidationErrors, - Optional, - ThirdPartyField, -} from './types'; -import { isUrlInvalid } from './validators'; - -import * as i18n from './translations'; import { CasesConfigurationMapping } from '../../../cases/containers/configure/types'; -export const createActionType = ({ - id, - actionTypeTitle, - selectMessage, - iconClass, - validateConnector, - validateParams = connectorParamsValidator, - actionConnectorFields, - actionParamsFields = null, -}: Optional) => (): ActionTypeModel => { - return { - id, - iconClass, - selectMessage, - actionTypeTitle, - validateConnector: (action: ActionConnector): ValidationResult => { - const errors: ActionConnectorValidationErrors = { - apiUrl: [], - }; - - if (!action.config.apiUrl) { - errors.apiUrl = [...errors.apiUrl, i18n.API_URL_REQUIRED]; - } - - if (isUrlInvalid(action.config.apiUrl)) { - errors.apiUrl = [...errors.apiUrl, i18n.API_URL_INVALID]; - } - - return { errors: { ...errors, ...validateConnector(action).errors } }; - }, - validateParams, - actionConnectorFields, - actionParamsFields, - }; -}; - -const connectorParamsValidator = (actionParams: ActionConnectorParams): ValidationResult => { - return { errors: {} }; -}; +import { ThirdPartyField } from './types'; export const createDefaultMapping = ( fields: Record diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/validators.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/validators.ts deleted file mode 100644 index 2989cf4d98f8..000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/validators.ts +++ /dev/null @@ -1,7 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -export { isUrlInvalid } from '../../utils/validators'; diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 10bbbbfa7271..9b4eb6a25d1e 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -21,7 +21,6 @@ import { import { Storage } from '../../../../src/plugins/kibana_utils/public'; import { initTelemetry } from './common/lib/telemetry'; import { KibanaServices } from './common/lib/kibana/services'; -import { resilientActionType } from './common/lib/connectors'; import { PluginSetup, PluginStart, @@ -96,8 +95,6 @@ export class Plugin implements IPlugin { const storage = new Storage(localStorage); const [coreStart, startPlugins] = await core.getStartServices(); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 54c92d323fcf..9c7799f11ee0 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -15107,9 +15107,6 @@ "xpack.securitySolution.case.configureCases.incidentManagementSystemDesc": "オプションとして、セキュリティケースを選択した外部のインシデント管理システムに接続できます。そうすると、選択したサードパーティシステム内でケースデータをインシデントとしてプッシュできます。", "xpack.securitySolution.case.configureCases.incidentManagementSystemLabel": "インシデント管理システム", "xpack.securitySolution.case.configureCases.incidentManagementSystemTitle": "外部のインシデント管理システムに接続", - "xpack.securitySolution.case.configureCases.mappingFieldComments": "コメント", - "xpack.securitySolution.case.configureCases.mappingFieldDescription": "説明", - "xpack.securitySolution.case.configureCases.mappingFieldName": "名前", "xpack.securitySolution.case.configureCases.mappingFieldNotMapped": "マップされません", "xpack.securitySolution.case.configureCases.noConnector": "コネクターを選択していません", "xpack.securitySolution.case.configureCases.updateConnector": "コネクターを更新", @@ -15123,25 +15120,6 @@ "xpack.securitySolution.case.confirmDeleteCase.deleteCases": "ケースを削除", "xpack.securitySolution.case.confirmDeleteCase.deleteTitle": "「{caseTitle}」を削除", "xpack.securitySolution.case.confirmDeleteCase.selectedCases": "選択したケースを削除", - "xpack.securitySolution.case.connectors.common.apiTokenTextFieldLabel": "APIトークン", - "xpack.securitySolution.case.connectors.common.apiUrlTextFieldLabel": "URL", - "xpack.securitySolution.case.connectors.common.emailTextFieldLabel": "メール", - "xpack.securitySolution.case.connectors.common.invalidApiUrlTextField": "URLが無効です", - "xpack.securitySolution.case.connectors.common.passwordTextFieldLabel": "パスワード", - "xpack.securitySolution.case.connectors.common.requiredApiTokenTextField": "APIトークンが必要です", - "xpack.securitySolution.case.connectors.common.requiredApiUrlTextField": "URLが必要です", - "xpack.securitySolution.case.connectors.common.requiredEmailTextField": "電子メールが必要です", - "xpack.securitySolution.case.connectors.common.requiredPasswordTextField": "パスワードが必要です", - "xpack.securitySolution.case.connectors.common.requiredUsernameTextField": "ユーザー名が必要です", - "xpack.securitySolution.case.connectors.common.usernameTextFieldLabel": "ユーザー名", - "xpack.securitySolution.case.connectors.resilient.actionTypeTitle": "IBM Resilient", - "xpack.securitySolution.case.connectors.resilient.apiKeyId": "APIキーID", - "xpack.securitySolution.case.connectors.resilient.apiKeySecret": "APIキーシークレット", - "xpack.securitySolution.case.connectors.resilient.orgId": "組織ID", - "xpack.securitySolution.case.connectors.resilient.requiredApiKeyIdTextField": "APIキーIDが必要です", - "xpack.securitySolution.case.connectors.resilient.requiredApiKeySecretTextField": "APIキーシークレットが必要です", - "xpack.securitySolution.case.connectors.resilient.requiredOrgIdTextField": "組織IDが必要です", - "xpack.securitySolution.case.connectors.resilient.selectMessageText": "Resilientでセキュリティケースデータを更新するか、新しいインシデントにプッシュ", "xpack.securitySolution.case.createCase.descriptionFieldRequiredError": "説明が必要です。", "xpack.securitySolution.case.createCase.fieldTagsHelpText": "このケースの1つ以上のカスタム識別タグを入力します。新しいタグを開始するには、各タグの後でEnterを押します。", "xpack.securitySolution.case.createCase.titleFieldRequiredError": "タイトルが必要です。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index df721cb62466..8a6bc996d9bd 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -15116,9 +15116,6 @@ "xpack.securitySolution.case.configureCases.incidentManagementSystemDesc": "您可能会根据需要将 Security 案例连接到选择的外部事件管理系统。这将允许您将案例数据作为事件推送到所选第三方系统。", "xpack.securitySolution.case.configureCases.incidentManagementSystemLabel": "事件管理系统", "xpack.securitySolution.case.configureCases.incidentManagementSystemTitle": "连接到外部事件管理系统", - "xpack.securitySolution.case.configureCases.mappingFieldComments": "注释", - "xpack.securitySolution.case.configureCases.mappingFieldDescription": "描述", - "xpack.securitySolution.case.configureCases.mappingFieldName": "名称", "xpack.securitySolution.case.configureCases.mappingFieldNotMapped": "未映射", "xpack.securitySolution.case.configureCases.noConnector": "未选择连接器", "xpack.securitySolution.case.configureCases.updateConnector": "更新连接器", @@ -15132,25 +15129,6 @@ "xpack.securitySolution.case.confirmDeleteCase.deleteCases": "删除案例", "xpack.securitySolution.case.confirmDeleteCase.deleteTitle": "删除“{caseTitle}”", "xpack.securitySolution.case.confirmDeleteCase.selectedCases": "删除选定案例", - "xpack.securitySolution.case.connectors.common.apiTokenTextFieldLabel": "API 令牌", - "xpack.securitySolution.case.connectors.common.apiUrlTextFieldLabel": "URL", - "xpack.securitySolution.case.connectors.common.emailTextFieldLabel": "电子邮件", - "xpack.securitySolution.case.connectors.common.invalidApiUrlTextField": "URL 无效", - "xpack.securitySolution.case.connectors.common.passwordTextFieldLabel": "密码", - "xpack.securitySolution.case.connectors.common.requiredApiTokenTextField": "“API 令牌”必填", - "xpack.securitySolution.case.connectors.common.requiredApiUrlTextField": "“URL”必填", - "xpack.securitySolution.case.connectors.common.requiredEmailTextField": "“电子邮件”必填", - "xpack.securitySolution.case.connectors.common.requiredPasswordTextField": "“密码”必填", - "xpack.securitySolution.case.connectors.common.requiredUsernameTextField": "“用户名”必填", - "xpack.securitySolution.case.connectors.common.usernameTextFieldLabel": "用户名", - "xpack.securitySolution.case.connectors.resilient.actionTypeTitle": "IBM Resilient", - "xpack.securitySolution.case.connectors.resilient.apiKeyId": "API 密钥 ID", - "xpack.securitySolution.case.connectors.resilient.apiKeySecret": "API 密钥密码", - "xpack.securitySolution.case.connectors.resilient.orgId": "组织 ID", - "xpack.securitySolution.case.connectors.resilient.requiredApiKeyIdTextField": "“API 密钥 ID”必填", - "xpack.securitySolution.case.connectors.resilient.requiredApiKeySecretTextField": "“API 密钥密码”必填", - "xpack.securitySolution.case.connectors.resilient.requiredOrgIdTextField": "“组织 ID”必填", - "xpack.securitySolution.case.connectors.resilient.selectMessageText": "将 Security 案例数据推送或更新到 Resilient 中的新问题", "xpack.securitySolution.case.createCase.descriptionFieldRequiredError": "描述必填。", "xpack.securitySolution.case.createCase.fieldTagsHelpText": "为此案例键入一个或多个定制识别标记。在每个标记后按 Enter 键可开始新的标记。", "xpack.securitySolution.case.createCase.titleFieldRequiredError": "标题必填。", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.test.tsx index be3e8a31820c..8c37dc940a23 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.test.tsx @@ -27,6 +27,7 @@ describe('EmailParamsFields renders', () => { docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} /> ); + expect(wrapper.find('[data-test-subj="toEmailAddressInput"]').length > 0).toBeTruthy(); expect( wrapper.find('[data-test-subj="toEmailAddressInput"]').first().prop('selectedOptions') diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx index 25c04bda3f53..a882e3bc43f3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx @@ -13,6 +13,7 @@ describe('IndexParamsFields renders', () => { const actionParams = { documents: [{ test: 123 }], }; + const wrapper = mountWithIntl( { group: 'group', class: 'test class', }; + const wrapper = mountWithIntl( > { + return await http.post(`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, { + body: JSON.stringify({ + params: { subAction: 'incidentTypes', subActionParams: {} }, + }), + signal, + }); +} + +export async function getSeverity({ + http, + signal, + connectorId, +}: { + http: HttpSetup; + signal: AbortSignal; + connectorId: string; +}): Promise> { + return await http.post(`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, { + body: JSON.stringify({ + params: { subAction: 'severity', subActionParams: {} }, + }), + signal, + }); +} diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/config.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/config.ts similarity index 88% rename from x-pack/plugins/security_solution/public/common/lib/connectors/resilient/config.ts rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/config.ts index 7d4edbf62487..a2054585c19b 100644 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/config.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/config.ts @@ -4,14 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ConnectorConfiguration } from './types'; - import * as i18n from './translations'; import logo from './logo.svg'; -export const connector: ConnectorConfiguration = { +export const connectorConfiguration = { id: '.resilient', - name: i18n.RESILIENT_TITLE, + name: i18n.TITLE, logo, enabled: true, enabledInConfig: true, diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/index.ts similarity index 77% rename from x-pack/plugins/security_solution/public/common/lib/connectors/index.ts rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/index.ts index 33afa82c84f3..0905bd29493e 100644 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { getActionType as resilientActionType } from './resilient'; +export { getActionType as getResilientActionType } from './resilient'; diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/logo.svg b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/logo.svg similarity index 100% rename from x-pack/plugins/security_solution/public/common/lib/connectors/resilient/logo.svg rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/logo.svg diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.test.tsx new file mode 100644 index 000000000000..b73eb72f137c --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.test.tsx @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { TypeRegistry } from '../../../type_registry'; +import { registerBuiltInActionTypes } from '.././index'; +import { ActionTypeModel } from '../../../../types'; +import { ResilientActionConnector } from './types'; + +const ACTION_TYPE_ID = '.resilient'; +let actionTypeModel: ActionTypeModel; + +beforeAll(() => { + const actionTypeRegistry = new TypeRegistry(); + registerBuiltInActionTypes({ actionTypeRegistry }); + const getResult = actionTypeRegistry.get(ACTION_TYPE_ID); + if (getResult !== null) { + actionTypeModel = getResult; + } +}); + +describe('actionTypeRegistry.get() works', () => { + test('action type static data is as expected', () => { + expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID); + }); +}); + +describe('resilient connector validation', () => { + test('connector validation succeeds when connector config is valid', () => { + const actionConnector = { + secrets: { + apiKeyId: 'email', + apiKeySecret: 'token', + }, + id: 'test', + actionTypeId: '.resilient', + isPreconfigured: false, + name: 'resilient', + config: { + apiUrl: 'https://test/', + orgId: '201', + }, + } as ResilientActionConnector; + + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + errors: { + apiUrl: [], + apiKeyId: [], + apiKeySecret: [], + orgId: [], + }, + }); + }); + + test('connector validation fails when connector config is not valid', () => { + const actionConnector = ({ + secrets: { + apiKeyId: 'user', + }, + id: '.jira', + actionTypeId: '.jira', + name: 'jira', + config: {}, + } as unknown) as ResilientActionConnector; + + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + errors: { + apiUrl: ['URL is required.'], + apiKeyId: [], + apiKeySecret: ['API key secret is required'], + orgId: ['Organization ID is required'], + }, + }); + }); +}); + +describe('resilient action params validation', () => { + test('action params validation succeeds when action params is valid', () => { + const actionParams = { + subActionParams: { title: 'some title {{test}}' }, + }; + + expect(actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { title: [] }, + }); + }); + + test('params validation fails when body is not valid', () => { + const actionParams = { + subActionParams: { title: '' }, + }; + + expect(actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { + title: ['Title is required.'], + }, + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.tsx new file mode 100644 index 000000000000..cda6935f3b73 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { lazy } from 'react'; +import { ValidationResult, ActionTypeModel } from '../../../../types'; +import { connectorConfiguration } from './config'; +import logo from './logo.svg'; +import { ResilientActionConnector, ResilientActionParams } from './types'; +import * as i18n from './translations'; +import { isValidUrl } from '../../../lib/value_validators'; + +const validateConnector = (action: ResilientActionConnector): ValidationResult => { + const validationResult = { errors: {} }; + const errors = { + apiUrl: new Array(), + orgId: new Array(), + apiKeyId: new Array(), + apiKeySecret: new Array(), + }; + validationResult.errors = errors; + + if (!action.config.apiUrl) { + errors.apiUrl = [...errors.apiUrl, i18n.API_URL_REQUIRED]; + } + + if (action.config.apiUrl && !isValidUrl(action.config.apiUrl, 'https:')) { + errors.apiUrl = [...errors.apiUrl, i18n.API_URL_INVALID]; + } + + if (!action.config.orgId) { + errors.orgId = [...errors.orgId, i18n.ORG_ID_REQUIRED]; + } + + if (!action.secrets.apiKeyId) { + errors.apiKeyId = [...errors.apiKeyId, i18n.API_KEY_ID_REQUIRED]; + } + + if (!action.secrets.apiKeySecret) { + errors.apiKeySecret = [...errors.apiKeySecret, i18n.API_KEY_SECRET_REQUIRED]; + } + + return validationResult; +}; + +export function getActionType(): ActionTypeModel { + return { + id: connectorConfiguration.id, + iconClass: logo, + selectMessage: i18n.DESC, + actionTypeTitle: connectorConfiguration.name, + validateConnector, + actionConnectorFields: lazy(() => import('./resilient_connectors')), + validateParams: (actionParams: ResilientActionParams): ValidationResult => { + const validationResult = { errors: {} }; + const errors = { + title: new Array(), + }; + validationResult.errors = errors; + if (actionParams.subActionParams && !actionParams.subActionParams.title?.length) { + errors.title.push(i18n.TITLE_REQUIRED); + } + return validationResult; + }, + actionParamsFields: lazy(() => import('./resilient_params')), + }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.test.tsx new file mode 100644 index 000000000000..7e242f1f501d --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.test.tsx @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { DocLinksStart } from 'kibana/public'; +import ResilientConnectorFields from './resilient_connectors'; +import { ResilientActionConnector } from './types'; + +describe('ResilientActionConnectorFields renders', () => { + test('alerting Resilient connector fields is rendered', () => { + const actionConnector = { + secrets: { + apiKeyId: 'key', + apiKeySecret: 'secret', + }, + id: 'test', + actionTypeId: '.resilient', + isPreconfigured: false, + name: 'resilient', + config: { + apiUrl: 'https://test/', + orgId: '201', + }, + } as ResilientActionConnector; + const deps = { + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart, + }; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={deps!.docLinks} + readOnly={false} + /> + ); + + expect(wrapper.find('[data-test-subj="apiUrlFromInput"]').length > 0).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="connector-resilient-orgId-form-input"]').length > 0 + ).toBeTruthy(); + + expect( + wrapper.find('[data-test-subj="connector-resilient-apiKeySecret-form-input"]').length > 0 + ).toBeTruthy(); + + expect( + wrapper.find('[data-test-subj="connector-resilient-apiKeySecret-form-input"]').length > 0 + ).toBeTruthy(); + }); + + test('case specific Resilient connector fields is rendered', () => { + const actionConnector = { + secrets: { + apiKeyId: 'email', + apiKeySecret: 'token', + }, + id: 'test', + actionTypeId: '.resilient', + isPreconfigured: false, + name: 'resilient', + config: { + apiUrl: 'https://test/', + orgId: '201', + }, + } as ResilientActionConnector; + const deps = { + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart, + }; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={deps!.docLinks} + readOnly={false} + consumer={'case'} + /> + ); + + expect(wrapper.find('[data-test-subj="case-resilient-mappings"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="apiUrlFromInput"]').length > 0).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="connector-resilient-orgId-form-input"]').length > 0 + ).toBeTruthy(); + + expect( + wrapper.find('[data-test-subj="connector-resilient-apiKeySecret-form-input"]').length > 0 + ).toBeTruthy(); + + expect( + wrapper.find('[data-test-subj="connector-resilient-apiKeySecret-form-input"]').length > 0 + ).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.tsx new file mode 100644 index 000000000000..7965e216f1d6 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.tsx @@ -0,0 +1,209 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useCallback } from 'react'; + +import { + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiFieldPassword, + EuiSpacer, +} from '@elastic/eui'; + +import { isEmpty } from 'lodash'; +import { ActionConnectorFieldsProps } from '../../../../types'; +import * as i18n from './translations'; +import { ResilientActionConnector } from './types'; +import { connectorConfiguration } from './config'; +import { FieldMapping, CasesConfigurationMapping, createDefaultMapping } from '../case_mappings'; + +const ResilientConnectorFields: React.FC> = ({ + action, + editActionSecrets, + editActionConfig, + errors, + consumer, + readOnly, + docLinks, +}) => { + // TODO: remove incidentConfiguration later, when Case Resilient will move their fields to the level of action execution + const { apiUrl, orgId, incidentConfiguration, isCaseOwned } = action.config; + const mapping = incidentConfiguration ? incidentConfiguration.mapping : []; + + const isApiUrlInvalid: boolean = errors.apiUrl.length > 0 && apiUrl != null; + + const { apiKeyId, apiKeySecret } = action.secrets; + + const isOrgIdInvalid: boolean = errors.orgId.length > 0 && orgId != null; + const isApiKeyInvalid: boolean = errors.apiKeyId.length > 0 && apiKeyId != null; + const isApiKeySecretInvalid: boolean = errors.apiKeySecret.length > 0 && apiKeySecret != null; + + // TODO: remove this block later, when Case ServiceNow will move their fields to the level of action execution + if (consumer === 'case') { + if (isEmpty(mapping)) { + editActionConfig('incidentConfiguration', { + mapping: createDefaultMapping(connectorConfiguration.fields as any), + }); + } + + if (!isCaseOwned) { + editActionConfig('isCaseOwned', true); + } + } + + const handleOnChangeActionConfig = useCallback( + (key: string, value: string) => editActionConfig(key, value), + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + + const handleOnChangeSecretConfig = useCallback( + (key: string, value: string) => editActionSecrets(key, value), + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + + const handleOnChangeMappingConfig = useCallback( + (newMapping: CasesConfigurationMapping[]) => + editActionConfig('incidentConfiguration', { + ...action.config.incidentConfiguration, + mapping: newMapping, + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [action.config] + ); + + return ( + <> + + + + handleOnChangeActionConfig('apiUrl', evt.target.value)} + onBlur={() => { + if (!apiUrl) { + editActionConfig('apiUrl', ''); + } + }} + /> + + + + + + + + handleOnChangeActionConfig('orgId', evt.target.value)} + onBlur={() => { + if (!orgId) { + editActionConfig('orgId', ''); + } + }} + /> + + + + + + + + handleOnChangeSecretConfig('apiKeyId', evt.target.value)} + onBlur={() => { + if (!apiKeyId) { + editActionSecrets('apiKeyId', ''); + } + }} + /> + + + + + + + + handleOnChangeSecretConfig('apiKeySecret', evt.target.value)} + onBlur={() => { + if (!apiKeySecret) { + editActionSecrets('apiKeySecret', ''); + } + }} + /> + + + + {consumer === 'case' && ( // TODO: remove this block later, when Case Resilient will move their fields to the level of action execution + <> + + + + + + + + )} + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { ResilientConnectorFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.test.tsx new file mode 100644 index 000000000000..17020805757f --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.test.tsx @@ -0,0 +1,189 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import ResilientParamsFields from './resilient_params'; +import { DocLinksStart } from 'kibana/public'; + +import { useGetIncidentTypes } from './use_get_incident_types'; +import { useGetSeverity } from './use_get_severity'; + +jest.mock('../../../app_context', () => { + const post = jest.fn(); + return { + useAppDependencies: jest.fn(() => ({ http: { post } })), + }; +}); + +jest.mock('./use_get_incident_types'); +jest.mock('./use_get_severity'); + +const useGetIncidentTypesMock = useGetIncidentTypes as jest.Mock; +const useGetSeverityMock = useGetSeverity as jest.Mock; + +const actionParams = { + subAction: 'pushToService', + subActionParams: { + title: 'title', + description: 'some description', + comments: [{ commentId: '1', comment: 'comment for resilient' }], + incidentTypes: [1001], + severityCode: 6, + savedObjectId: '123', + externalId: null, + }, +}; +const connector = { + secrets: {}, + config: {}, + id: 'test', + actionTypeId: '.test', + name: 'Test', + isPreconfigured: false, +}; + +describe('ResilientParamsFields renders', () => { + const useGetIncidentTypesResponse = { + isLoading: false, + incidentTypes: [ + { + id: 19, + name: 'Malware', + }, + { + id: 21, + name: 'Denial of Service', + }, + ], + }; + + const useGetSeverityResponse = { + isLoading: false, + severity: [ + { + id: 4, + name: 'Low', + }, + { + id: 5, + name: 'Medium', + }, + { + id: 6, + name: 'High', + }, + ], + }; + + beforeEach(() => { + useGetIncidentTypesMock.mockReturnValue(useGetIncidentTypesResponse); + useGetSeverityMock.mockReturnValue(useGetSeverityResponse); + }); + + test('all params fields are rendered', () => { + const wrapper = mountWithIntl( + {}} + index={0} + messageVariables={[]} + docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + actionConnector={connector} + /> + ); + expect(wrapper.find('[data-test-subj="incidentTypeComboBox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="severitySelect"]').first().prop('value')).toStrictEqual( + 6 + ); + expect(wrapper.find('[data-test-subj="titleInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="descriptionTextArea"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="commentsTextArea"]').length > 0).toBeTruthy(); + }); + + test('it shows loading when loading incident types', () => { + useGetIncidentTypesMock.mockReturnValue({ ...useGetIncidentTypesResponse, isLoading: true }); + const wrapper = mountWithIntl( + {}} + index={0} + messageVariables={[]} + docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + actionConnector={connector} + /> + ); + + expect( + wrapper.find('[data-test-subj="incidentTypeComboBox"]').first().prop('isLoading') + ).toBeTruthy(); + }); + + test('it shows loading when loading severity', () => { + useGetSeverityMock.mockReturnValue({ + ...useGetSeverityResponse, + isLoading: true, + }); + + const wrapper = mountWithIntl( + {}} + index={0} + messageVariables={[]} + docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + actionConnector={connector} + /> + ); + + expect( + wrapper.find('[data-test-subj="severitySelect"]').first().prop('isLoading') + ).toBeTruthy(); + }); + + test('it disabled the fields when loading issue types', () => { + useGetIncidentTypesMock.mockReturnValue({ ...useGetIncidentTypesResponse, isLoading: true }); + + const wrapper = mountWithIntl( + {}} + index={0} + messageVariables={[]} + docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + actionConnector={connector} + /> + ); + + expect( + wrapper.find('[data-test-subj="incidentTypeComboBox"]').first().prop('isDisabled') + ).toBeTruthy(); + }); + + test('it disabled the fields when loading severity', () => { + useGetSeverityMock.mockReturnValue({ + ...useGetSeverityResponse, + isLoading: true, + }); + + const wrapper = mountWithIntl( + {}} + index={0} + messageVariables={[]} + docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + actionConnector={connector} + /> + ); + + expect(wrapper.find('[data-test-subj="severitySelect"]').first().prop('disabled')).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.tsx new file mode 100644 index 000000000000..4b157c699998 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.tsx @@ -0,0 +1,256 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, useEffect, useState } from 'react'; +import { + EuiFormRow, + EuiComboBox, + EuiSelect, + EuiSpacer, + EuiTitle, + EuiComboBoxOptionOption, + EuiSelectOption, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { ActionParamsProps } from '../../../../types'; +import { useAppDependencies } from '../../../app_context'; +import { ResilientActionParams } from './types'; +import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables'; +import { TextFieldWithMessageVariables } from '../../text_field_with_message_variables'; + +import { useGetIncidentTypes } from './use_get_incident_types'; +import { useGetSeverity } from './use_get_severity'; + +const ResilientParamsFields: React.FunctionComponent> = ({ + actionParams, + editAction, + index, + errors, + messageVariables, + actionConnector, +}) => { + const [firstLoad, setFirstLoad] = useState(false); + const { http, toastNotifications } = useAppDependencies(); + const { title, description, comments, incidentTypes, severityCode, savedObjectId } = + actionParams.subActionParams || {}; + + const [incidentTypesComboBoxOptions, setIncidentTypesComboBoxOptions] = useState< + Array> + >([]); + + const [selectedIncidentTypesComboBoxOptions, setSelectedIncidentTypesComboBoxOptions] = useState< + Array> + >([]); + + const [severitySelectOptions, setSeveritySelectOptions] = useState([]); + + useEffect(() => { + setFirstLoad(true); + }, []); + + const { + isLoading: isLoadingIncidentTypes, + incidentTypes: allIncidentTypes, + } = useGetIncidentTypes({ + http, + toastNotifications, + actionConnector, + }); + + const { isLoading: isLoadingSeverity, severity } = useGetSeverity({ + http, + toastNotifications, + actionConnector, + }); + + const editSubActionProperty = (key: string, value: {}) => { + const newProps = { ...actionParams.subActionParams, [key]: value }; + editAction('subActionParams', newProps, index); + }; + + useEffect(() => { + const options = severity.map((s) => ({ + value: s.id.toString(), + text: s.name, + })); + + setSeveritySelectOptions(options); + }, [actionConnector, severity]); + + // Reset parameters when changing connector + useEffect(() => { + if (!firstLoad) { + return; + } + + setIncidentTypesComboBoxOptions([]); + setSelectedIncidentTypesComboBoxOptions([]); + setSeveritySelectOptions([]); + editAction('subActionParams', { title, comments, description: '', savedObjectId }, index); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [actionConnector]); + + useEffect(() => { + if (!actionParams.subAction) { + editAction('subAction', 'pushToService', index); + } + if (!savedObjectId && messageVariables?.find((variable) => variable.name === 'alertId')) { + editSubActionProperty('savedObjectId', '{{alertId}}'); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [actionConnector, savedObjectId]); + + useEffect(() => { + setIncidentTypesComboBoxOptions( + allIncidentTypes + ? allIncidentTypes.map((type: { id: number; name: string }) => ({ + label: type.name, + value: type.id.toString(), + })) + : [] + ); + + const allIncidentTypesAsObject = allIncidentTypes.reduce( + (acc, type) => ({ ...acc, [type.id.toString()]: type.name }), + {} as Record + ); + + setSelectedIncidentTypesComboBoxOptions( + incidentTypes + ? incidentTypes + .map((type) => ({ + label: allIncidentTypesAsObject[type.toString()], + value: type.toString(), + })) + .filter((type) => type.label != null) + : [] + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [actionConnector, allIncidentTypes]); + + return ( + + +

Incident

+
+ + + ) => { + setSelectedIncidentTypesComboBoxOptions( + selectedOptions.map((selectedOption) => ({ + label: selectedOption.label, + value: selectedOption.value, + })) + ); + + editSubActionProperty( + 'incidentTypes', + selectedOptions.map((selectedOption) => selectedOption.value ?? selectedOption.label) + ); + }} + onBlur={() => { + if (!incidentTypes) { + editSubActionProperty('incidentTypes', []); + } + }} + isClearable={true} + /> + + + + { + editSubActionProperty('severityCode', e.target.value); + }} + /> + + + 0 && title !== undefined} + label={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.titleFieldLabel', + { + defaultMessage: 'Name', + } + )} + > + + + + { + editSubActionProperty(key, [{ commentId: 'alert-comment', comment: value }]); + }} + messageVariables={messageVariables} + paramsProperty={'comments'} + inputTargetValue={comments && comments.length > 0 ? comments[0].comment : ''} + label={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.commentsTextAreaFieldLabel', + { + defaultMessage: 'Additional comments (optional)', + } + )} + errors={errors.comments as string[]} + /> +
+ ); +}; + +// eslint-disable-next-line import/no-default-export +export { ResilientParamsFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/translations.ts new file mode 100644 index 000000000000..71ad05abfdec --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/translations.ts @@ -0,0 +1,133 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const DESC = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.selectMessageText', + { + defaultMessage: 'Push or update data to a new incident in Resilient.', + } +); + +export const TITLE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.actionTypeTitle', + { + defaultMessage: 'Resilient', + } +); + +export const API_URL_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiUrlTextFieldLabel', + { + defaultMessage: 'URL', + } +); + +export const API_URL_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiUrlTextField', + { + defaultMessage: 'URL is required.', + } +); + +export const API_URL_INVALID = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.invalidApiUrlTextField', + { + defaultMessage: 'URL is invalid.', + } +); + +export const ORG_ID_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.orgId', + { + defaultMessage: 'Organization ID', + } +); + +export const ORG_ID_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredOrgIdTextField', + { + defaultMessage: 'Organization ID is required', + } +); + +export const API_KEY_ID_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiKeyId', + { + defaultMessage: 'API key ID', + } +); + +export const API_KEY_ID_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiKeyIdTextField', + { + defaultMessage: 'API key ID is required', + } +); + +export const API_KEY_SECRET_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiKeySecret', + { + defaultMessage: 'API key secret', + } +); + +export const API_KEY_SECRET_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiKeySecretTextField', + { + defaultMessage: 'API key secret is required', + } +); + +export const MAPPING_FIELD_NAME = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.mappingFieldShortDescription', + { + defaultMessage: 'Name', + } +); + +export const MAPPING_FIELD_DESC = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.mappingFieldDescription', + { + defaultMessage: 'Description', + } +); + +export const MAPPING_FIELD_COMMENTS = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.mappingFieldComments', + { + defaultMessage: 'Comments', + } +); + +export const DESCRIPTION_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.common.requiredDescriptionTextField', + { + defaultMessage: 'Description is required.', + } +); + +export const TITLE_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.common.requiredTitleTextField', + { + defaultMessage: 'Title is required.', + } +); + +export const INCIDENT_TYPES_API_ERROR = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.unableToGetIncidentTypesMessage', + { + defaultMessage: 'Unable to get incident types', + } +); + +export const SEVERITY_API_ERROR = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.unableToGetSeverityMessage', + { + defaultMessage: 'Unable to get severity', + } +); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/types.ts new file mode 100644 index 000000000000..37516f5bac37 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/types.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CasesConfigurationMapping } from '../case_mappings'; + +export interface ResilientActionConnector { + config: ResilientConfig; + secrets: ResilientSecrets; +} + +export interface ResilientActionParams { + subAction: string; + subActionParams: { + savedObjectId: string; + title: string; + description: string; + externalId: string | null; + incidentTypes: number[]; + severityCode: number; + comments: Array<{ commentId: string; comment: string }>; + }; +} + +interface IncidentConfiguration { + mapping: CasesConfigurationMapping[]; +} + +interface ResilientConfig { + apiUrl: string; + orgId: string; + incidentConfiguration?: IncidentConfiguration; + isCaseOwned?: boolean; +} + +interface ResilientSecrets { + apiKeyId: string; + apiKeySecret: string; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/use_get_incident_types.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/use_get_incident_types.tsx new file mode 100644 index 000000000000..219c6ac77d08 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/use_get_incident_types.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useState, useEffect, useRef } from 'react'; +import { HttpSetup, ToastsApi } from 'kibana/public'; +import { ActionConnector } from '../../../../types'; +import { getIncidentTypes } from './api'; +import * as i18n from './translations'; + +type IncidentTypes = Array<{ id: number; name: string }>; + +interface Props { + http: HttpSetup; + toastNotifications: Pick< + ToastsApi, + 'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError' + >; + actionConnector?: ActionConnector; +} + +export interface UseGetIncidentTypes { + incidentTypes: IncidentTypes; + isLoading: boolean; +} + +export const useGetIncidentTypes = ({ + http, + toastNotifications, + actionConnector, +}: Props): UseGetIncidentTypes => { + const [isLoading, setIsLoading] = useState(true); + const [incidentTypes, setIncidentTypes] = useState([]); + const abortCtrl = useRef(new AbortController()); + + useEffect(() => { + let didCancel = false; + const fetchData = async () => { + if (!actionConnector) { + setIsLoading(false); + return; + } + + abortCtrl.current = new AbortController(); + setIsLoading(true); + + try { + const res = await getIncidentTypes({ + http, + signal: abortCtrl.current.signal, + connectorId: actionConnector.id, + }); + + if (!didCancel) { + setIsLoading(false); + setIncidentTypes(res.data ?? []); + if (res.status && res.status === 'error') { + toastNotifications.addDanger({ + title: i18n.INCIDENT_TYPES_API_ERROR, + text: `${res.serviceMessage ?? res.message}`, + }); + } + } + } catch (error) { + if (!didCancel) { + toastNotifications.addDanger({ + title: i18n.INCIDENT_TYPES_API_ERROR, + text: error.message, + }); + } + } + }; + + abortCtrl.current.abort(); + fetchData(); + + return () => { + didCancel = true; + setIsLoading(false); + abortCtrl.current.abort(); + }; + }, [http, actionConnector, toastNotifications]); + + return { + incidentTypes, + isLoading, + }; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/use_get_severity.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/use_get_severity.tsx new file mode 100644 index 000000000000..83689254f000 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/use_get_severity.tsx @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useState, useEffect, useRef } from 'react'; +import { HttpSetup, ToastsApi } from 'kibana/public'; +import { ActionConnector } from '../../../../types'; +import { getSeverity } from './api'; +import * as i18n from './translations'; + +type Severity = Array<{ id: number; name: string }>; + +interface Props { + http: HttpSetup; + toastNotifications: Pick< + ToastsApi, + 'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError' + >; + actionConnector?: ActionConnector; +} + +export interface UseGetSeverity { + severity: Severity; + isLoading: boolean; +} + +export const useGetSeverity = ({ + http, + toastNotifications, + actionConnector, +}: Props): UseGetSeverity => { + const [isLoading, setIsLoading] = useState(true); + const [severity, setSeverity] = useState([]); + const abortCtrl = useRef(new AbortController()); + + useEffect(() => { + let didCancel = false; + const fetchData = async () => { + if (!actionConnector) { + setIsLoading(false); + return; + } + + abortCtrl.current = new AbortController(); + setIsLoading(true); + + try { + const res = await getSeverity({ + http, + signal: abortCtrl.current.signal, + connectorId: actionConnector.id, + }); + + if (!didCancel) { + setIsLoading(false); + setSeverity(res.data ?? []); + + if (res.status && res.status === 'error') { + toastNotifications.addDanger({ + title: i18n.SEVERITY_API_ERROR, + text: `${res.serviceMessage ?? res.message}`, + }); + } + } + } catch (error) { + if (!didCancel) { + toastNotifications.addDanger({ + title: i18n.SEVERITY_API_ERROR, + text: error.message, + }); + } + } + }; + + abortCtrl.current.abort(); + fetchData(); + + return () => { + didCancel = true; + setIsLoading(false); + abortCtrl.current.abort(); + }; + }, [http, actionConnector, toastNotifications]); + + return { + severity, + isLoading, + }; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.test.tsx index 1fc856b1e1ab..f4d831d7234e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.test.tsx @@ -23,6 +23,7 @@ describe('ServiceNowParamsFields renders', () => { externalId: null, }, }; + const wrapper = mountWithIntl( { const actionParams = { message: 'test message', }; + const wrapper = mountWithIntl( { const actionParams = { body: 'test message', }; + const wrapper = mountWithIntl( { + let resilientSimulatorURL: string = ''; + + // need to wait for kibanaServer to settle ... + before(() => { + resilientSimulatorURL = kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.RESILIENT) + ); + }); + + it('should return 403 when creating a resilient action', async () => { + await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A resilient action', + actionTypeId: '.resilient', + config: { + apiUrl: resilientSimulatorURL, + incidentConfiguration: { ...mockResilient.config.incidentConfiguration }, + isCaseOwned: true, + }, + secrets: mockResilient.secrets, + }) + .expect(403, { + statusCode: 403, + error: 'Forbidden', + message: + 'Action type .resilient is disabled because your basic license does not support it. Please upgrade your license.', + }); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts index 9cbc2373ef94..d1d19da423e6 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts @@ -41,9 +41,10 @@ export default function resilientTest({ getService }: FtrProviderContext) { const mockResilient = { config: { - apiUrl: 'www.jiraisinkibanaactions.com', + apiUrl: 'www.resilientisinkibanaactions.com', orgId: '201', - casesConfiguration: { mapping }, + incidentConfiguration: { mapping }, + isCaseOwned: true, }, secrets: { apiKeyId: 'key', @@ -55,6 +56,8 @@ export default function resilientTest({ getService }: FtrProviderContext) { savedObjectId: '123', title: 'a title', description: 'a description', + incidentTypes: [1001], + severityCode: 6, createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: 'Elastic User', username: 'elastic' }, updatedAt: null, @@ -108,7 +111,8 @@ export default function resilientTest({ getService }: FtrProviderContext) { config: { apiUrl: resilientSimulatorURL, orgId: mockResilient.config.orgId, - casesConfiguration: mockResilient.config.casesConfiguration, + incidentConfiguration: mockResilient.config.incidentConfiguration, + isCaseOwned: true, }, }); @@ -124,7 +128,8 @@ export default function resilientTest({ getService }: FtrProviderContext) { config: { apiUrl: resilientSimulatorURL, orgId: mockResilient.config.orgId, - casesConfiguration: mockResilient.config.casesConfiguration, + incidentConfiguration: mockResilient.config.incidentConfiguration, + isCaseOwned: true, }, }); }); @@ -179,7 +184,7 @@ export default function resilientTest({ getService }: FtrProviderContext) { config: { apiUrl: 'http://resilient.mynonexistent.com', orgId: mockResilient.config.orgId, - casesConfiguration: mockResilient.config.casesConfiguration, + incidentConfiguration: mockResilient.config.incidentConfiguration, }, secrets: mockResilient.secrets, }) @@ -204,7 +209,7 @@ export default function resilientTest({ getService }: FtrProviderContext) { config: { apiUrl: resilientSimulatorURL, orgId: mockResilient.config.orgId, - casesConfiguration: mockResilient.config.casesConfiguration, + incidentConfiguration: mockResilient.config.incidentConfiguration, }, }) .expect(400) @@ -218,30 +223,6 @@ export default function resilientTest({ getService }: FtrProviderContext) { }); }); - it('should respond with a 400 Bad Request when creating a ibm resilient action without casesConfiguration', async () => { - await supertest - .post('/api/actions/action') - .set('kbn-xsrf', 'foo') - .send({ - name: 'An IBM Resilient', - actionTypeId: '.resilient', - config: { - apiUrl: resilientSimulatorURL, - orgId: mockResilient.config.orgId, - }, - secrets: mockResilient.secrets, - }) - .expect(400) - .then((resp: any) => { - expect(resp.body).to.eql({ - statusCode: 400, - error: 'Bad Request', - message: - 'error validating action type config: [casesConfiguration.mapping]: expected value of type [array] but got [undefined]', - }); - }); - }); - it('should respond with a 400 Bad Request when creating a ibm resilient action with empty mapping', async () => { await supertest .post('/api/actions/action') @@ -252,7 +233,7 @@ export default function resilientTest({ getService }: FtrProviderContext) { config: { apiUrl: resilientSimulatorURL, orgId: mockResilient.config.orgId, - casesConfiguration: { mapping: [] }, + incidentConfiguration: { mapping: [] }, }, secrets: mockResilient.secrets, }) @@ -262,7 +243,7 @@ export default function resilientTest({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - 'error validating action type config: [casesConfiguration.mapping]: expected non-empty but got empty', + 'error validating action type config: [incidentConfiguration.mapping]: expected non-empty but got empty', }); }); }); @@ -277,7 +258,7 @@ export default function resilientTest({ getService }: FtrProviderContext) { config: { apiUrl: resilientSimulatorURL, orgId: mockResilient.config.orgId, - casesConfiguration: { + incidentConfiguration: { mapping: [ { source: 'title', @@ -307,7 +288,7 @@ export default function resilientTest({ getService }: FtrProviderContext) { config: { apiUrl: resilientSimulatorURL, orgId: mockResilient.config.orgId, - casesConfiguration: mockResilient.config.casesConfiguration, + incidentConfiguration: mockResilient.config.incidentConfiguration, }, secrets: mockResilient.secrets, }); @@ -353,7 +334,7 @@ export default function resilientTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subAction]: expected value to equal [pushToService]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subAction]: expected value to equal [pushToService]\n- [3.subAction]: expected value to equal [incidentTypes]\n- [4.subAction]: expected value to equal [severity]', }); }); }); @@ -371,7 +352,7 @@ export default function resilientTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.savedObjectId]: expected value of type [string] but got [undefined]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.savedObjectId]: expected value of type [string] but got [undefined]\n- [3.subAction]: expected value to equal [incidentTypes]\n- [4.subAction]: expected value to equal [severity]', }); }); }); @@ -389,7 +370,7 @@ export default function resilientTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.savedObjectId]: expected value of type [string] but got [undefined]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.savedObjectId]: expected value of type [string] but got [undefined]\n- [3.subAction]: expected value to equal [incidentTypes]\n- [4.subAction]: expected value to equal [severity]', }); }); }); @@ -412,31 +393,7 @@ export default function resilientTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.title]: expected value of type [string] but got [undefined]', - }); - }); - }); - - it('should handle failing with a simulated success without createdAt', async () => { - await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - ...mockResilient.params, - subActionParams: { - savedObjectId: 'success', - title: 'success', - }, - }, - }) - .then((resp: any) => { - expect(resp.body).to.eql({ - actionId: simulatedActionId, - status: 'error', - retry: false, - message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.createdAt]: expected value of type [string] but got [undefined]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.title]: expected value of type [string] but got [undefined]\n- [3.subAction]: expected value to equal [incidentTypes]\n- [4.subAction]: expected value to equal [severity]', }); }); }); @@ -464,7 +421,7 @@ export default function resilientTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.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]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.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- [3.subAction]: expected value to equal [incidentTypes]\n- [4.subAction]: expected value to equal [severity]', }); }); }); @@ -492,35 +449,7 @@ export default function resilientTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.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]', - }); - }); - }); - - it('should handle failing with a simulated success without comment.createdAt', async () => { - await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - ...mockResilient.params, - subActionParams: { - ...mockResilient.params.subActionParams, - savedObjectId: 'success', - title: 'success', - createdAt: 'success', - createdBy: { username: 'elastic' }, - comments: [{ commentId: 'success', comment: 'success' }], - }, - }, - }) - .then((resp: any) => { - expect(resp.body).to.eql({ - actionId: simulatedActionId, - status: 'error', - retry: false, - message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.createdAt]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.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- [3.subAction]: expected value to equal [incidentTypes]\n- [4.subAction]: expected value to equal [severity]', }); }); }); @@ -536,7 +465,7 @@ export default function resilientTest({ getService }: FtrProviderContext) { ...mockResilient.params, subActionParams: { ...mockResilient.params.subActionParams, - comments: [], + comments: null, }, }, }) diff --git a/x-pack/test/case_api_integration/common/lib/utils.ts b/x-pack/test/case_api_integration/common/lib/utils.ts index c23df53c4fee..41f92d022f06 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -99,7 +99,7 @@ export const getResilientConnector = () => ({ config: { apiUrl: 'http://some.non.existent.com', orgId: 'pkey', - casesConfiguration: { + incidentConfiguration: { mapping: [ { source: 'title', @@ -118,6 +118,7 @@ export const getResilientConnector = () => ({ }, ], }, + isCaseOwned: true, }, });