From e42a3383d322ea27d6961b4460aa28daf17e4912 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 14 Apr 2020 11:25:15 +0300 Subject: [PATCH 01/33] Refactor connectors --- .../connectors/constants.ts | 7 + .../builtin_action_types/connectors/index.ts | 7 + .../builtin_action_types/connectors/schema.ts | 81 ++++++ .../connectors/servicenow/api.ts | 100 ++++++++ .../connectors/servicenow/config.ts | 13 + .../connectors/servicenow/index.ts | 19 ++ .../connectors/servicenow/service.ts | 125 ++++++++++ .../connectors/servicenow/translations.ts | 23 ++ .../connectors/servicenow/validators.ts | 41 ++++ .../connectors/transformers.ts | 43 ++++ .../connectors/translations.ts | 43 ++++ .../builtin_action_types/connectors/types.ts | 139 +++++++++++ .../builtin_action_types/connectors/utils.ts | 230 ++++++++++++++++++ .../server/builtin_action_types/index.ts | 8 +- 14 files changed, 877 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/connectors/constants.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/connectors/index.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/connectors/schema.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/api.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/config.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/index.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/translations.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/validators.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/connectors/transformers.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/connectors/translations.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/connectors/types.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/connectors/utils.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/constants.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/constants.ts new file mode 100644 index 00000000000000..1f2bc7f5e8e531 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/constants.ts @@ -0,0 +1,7 @@ +/* + * 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 const SUPPORTED_SOURCE_FIELDS = ['title', 'comments', 'description']; diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/index.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/index.ts new file mode 100644 index 00000000000000..9ecf39b8922e42 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { connector as getServiceNowConnector } from './servicenow'; diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/schema.ts new file mode 100644 index 00000000000000..0527f40e207f84 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/schema.ts @@ -0,0 +1,81 @@ +/* + * 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 CaseConfigurationSchema = schema.object({ + mapping: schema.arrayOf(MapRecordSchema), +}); + +export const ConnectorPublicConfiguration = { + apiUrl: schema.string(), + casesConfiguration: CaseConfigurationSchema, +}; + +export const ConnectorPublicConfigurationSchema = schema.object(ConnectorPublicConfiguration); + +export const ConnectorSecretConfiguration = { + password: schema.string(), + username: schema.string(), +}; + +export const ConnectorSecretConfigurationSchema = schema.object(ConnectorSecretConfiguration); + +export const UserSchema = schema.object({ + fullName: schema.oneOf([schema.nullable(schema.string()), schema.maybe(schema.string())]), + username: schema.oneOf([schema.nullable(schema.string()), schema.maybe(schema.string())]), + email: schema.oneOf([schema.nullable(schema.string()), schema.maybe(schema.string())]), +}); + +const EntityInformation = { + createdAt: schema.string(), + createdBy: 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(), + version: schema.maybe(schema.string()), + ...EntityInformation, +}); + +export const ExecutorActionSchema = schema.oneOf([ + schema.literal('getIncident'), + schema.literal('pushToService'), + schema.literal('handshake'), +]); + +export const ExecutorActionParams = { + caseId: schema.string(), + title: schema.string(), + description: schema.maybe(schema.string()), + comments: schema.maybe(schema.arrayOf(CommentSchema)), + externalCaseId: schema.nullable(schema.string()), + ...EntityInformation, +}; + +export const ExecutorActionParamsSchema = schema.object(ExecutorActionParams); + +export const ExecutorParamsSchema = schema.object({ + action: ExecutorActionSchema, + actionParams: ExecutorActionParamsSchema, +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/api.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/api.ts new file mode 100644 index 00000000000000..3e82609ddf5bfa --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/api.ts @@ -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 { zipWith } from 'lodash'; + +import { + ConnectorApi, + ExternalServiceParams, + ExternalServiceCommentResponse, + Comment, + PushToServiceResponse, + ConnectorApiHandlerArgs, +} from '../types'; +import { prepareFieldsForTransformation, transformFields, transformComments } from '../utils'; + +const handshakeHandler = async ({ + externalService, + mapping, + params, +}: ConnectorApiHandlerArgs) => {}; +const getIncidentHandler = async ({ + externalService, + mapping, + params, +}: ConnectorApiHandlerArgs) => {}; + +const pushToServiceHandler = async ({ + externalService, + mapping, + params, +}: ConnectorApiHandlerArgs): Promise => { + const { externalCaseId, comments } = params; + const updateIncident = externalCaseId ? true : false; + const defaultPipes = updateIncident ? ['informationUpdated'] : ['informationCreated']; + let currentIncident: ExternalServiceParams | undefined; + let res: PushToServiceResponse; + + if (externalCaseId) { + currentIncident = await externalService.getIncident(externalCaseId); + } + + const fields = prepareFieldsForTransformation({ + params, + mapping, + defaultPipes, + }); + + const incident = transformFields({ + params, + fields, + currentIncident, + }); + + if (updateIncident) { + res = await externalService.updateIncident({ incidentId: externalCaseId, 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']); + + const createdComments = await Promise.all( + commentsTransformed.map(comment => + externalService.createComment({ + incidentId: res.id, + comment, + field: mapping.get('comments').target, + }) + ) + ); + + const zippedComments: ExternalServiceCommentResponse[] = zipWith( + commentsTransformed, + createdComments, + (a: Comment, b: ExternalServiceCommentResponse) => ({ + commentId: a.commentId, + pushedDate: b.pushedDate, + }) + ); + + res.comments = [...zippedComments]; + } + + return res; +}; + +export const api: ConnectorApi = { + handshake: handshakeHandler, + pushToService: pushToServiceHandler, + getIncident: getIncidentHandler, +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/config.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/config.ts new file mode 100644 index 00000000000000..3f303f00be1d05 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/config.ts @@ -0,0 +1,13 @@ +/* + * 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 { ConnectorConfiguration } from '../types'; +import * as i18n from './translations'; + +export const config: ConnectorConfiguration = { + id: '.servicenow', + name: i18n.NAME, +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/index.ts new file mode 100644 index 00000000000000..3b4704a77afb1f --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/index.ts @@ -0,0 +1,19 @@ +/* + * 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 { createConnector } from '../utils'; + +import { api } from './api'; +import { config } from './config'; +import { validate } from './validators'; +import { createExternalService } from './service'; + +export const connector = createConnector({ + api, + config, + validate, + createExternalService, +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.ts new file mode 100644 index 00000000000000..b1bfbaf711fcc5 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.ts @@ -0,0 +1,125 @@ +/* + * 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 axios from 'axios'; + +import { ExternalServiceCredential, ExternalService, ExternalServiceParams } from '../types'; +import { addTimeZoneToDate, patch, request } from '../utils'; + +const API_VERSION = 'v2'; +const INCIDENT_URL = `api/now/${API_VERSION}/table/incident`; +const COMMENT_URL = `api/now/${API_VERSION}/table/incident`; + +// Based on: https://docs.servicenow.com/bundle/orlando-platform-user-interface/page/use/navigation/reference/r_NavigatingByURLExamples.html +const VIEW_INCIDENT_URL = `nav_to.do?uri=incident.do?sys_id=`; + +const getErrorMessage = (msg: string) => { + return `[Action][ServiceNow]: ${msg}`; +}; + +export const createExternalService = ({ + url, + username, + password, +}: ExternalServiceCredential): ExternalService => { + if (!url || !username || !password) { + throw Error('[Action][ServiceNow]: Wrong configuration.'); + } + + const incidentUrl = `${url}/${INCIDENT_URL}`; + const commentUrl = `${url}/${COMMENT_URL}`; + const axiosInstance = axios.create({ + auth: { username, password }, + }); + + const getIncidentViewURL = (id: string) => { + return `${url}/${VIEW_INCIDENT_URL}${id}`; + }; + + const getIncident = async (id: string) => { + try { + const res = await request({ + axios: axiosInstance, + url: `${incidentUrl}/${id}`, + }); + + return { ...res.data.result }; + } catch (error) { + throw new Error( + getErrorMessage(`Unable to get incident with id ${id}. Error: ${error.message}`) + ); + } + }; + + const createIncident = async ({ incident }: ExternalServiceParams) => { + try { + const res = await request({ + axios: axiosInstance, + url: `${incidentUrl}`, + method: 'post', + data: { ...incident }, + }); + + return { + title: res.data.result.number, + id: res.data.result.sys_id, + pushedDate: new Date(addTimeZoneToDate(res.data.result.sys_created_on)).toISOString(), + url: getIncidentViewURL(res.data.result.sys_id), + }; + } catch (error) { + throw new Error(getErrorMessage(`Unable to create incident. Error: ${error.message}`)); + } + }; + + const updateIncident = async ({ incidentId, incident }: ExternalServiceParams) => { + try { + const res = await patch({ + axios: axiosInstance, + url: `${incidentUrl}/${incidentId}`, + data: { ...incident }, + }); + + return { + title: res.data.result.number, + id: res.data.result.sys_id, + pushedDate: new Date(addTimeZoneToDate(res.data.result.sys_updated_on)).toISOString(), + url: getIncidentViewURL(res.data.result.sys_id), + }; + } catch (error) { + throw new Error( + getErrorMessage(`Unable to update incident with id ${incidentId}. Error: ${error.message}`) + ); + } + }; + + const createComment = async ({ incidentId, comment, field }: ExternalServiceParams) => { + try { + const res = await patch({ + axios: axiosInstance, + url: `${commentUrl}/${incidentId}`, + data: { [field]: comment.comment }, + }); + + return { + commentId: comment.commentId, + pushedDate: new Date(addTimeZoneToDate(res.data.result.sys_updated_on)).toISOString(), + }; + } catch (error) { + throw new Error( + getErrorMessage( + `Unable to create comment at incident with id ${incidentId}. Error: ${error.message}` + ) + ); + } + }; + + return { + getIncident, + createIncident, + updateIncident, + createComment, + }; +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/translations.ts new file mode 100644 index 00000000000000..349b2e2988d623 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/translations.ts @@ -0,0 +1,23 @@ +/* + * 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 NAME = i18n.translate('xpack.actions.builtin.servicenowTitle', { + defaultMessage: 'ServiceNow', +}); + +export const MAPPING_EMPTY = i18n.translate('xpack.actions.builtin.servicenow.emptyMapping', { + defaultMessage: '[casesConfiguration.mapping]: expected non-empty but got empty', +}); + +export const WHITE_LISTED_ERROR = (message: string) => + i18n.translate('xpack.actions.builtin.servicenow.servicenowApiWhitelistError', { + defaultMessage: 'error configuring servicenow action: {message}', + values: { + message, + }, + }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/validators.ts new file mode 100644 index 00000000000000..9773ec3d61a174 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/validators.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 { isEmpty } from 'lodash'; + +import { ActionsConfigurationUtilities } from '../../../actions_config'; +import { + ConnectorSecretConfigurationType, + ConnectorPublicConfigurationType, + ConnectorValidation, +} from '../types'; + +import * as i18n from './translations'; + +const validateConfig = ( + configurationUtilities: ActionsConfigurationUtilities, + configObject: ConnectorPublicConfigurationType +) => { + try { + if (isEmpty(configObject.casesConfiguration.mapping)) { + return i18n.MAPPING_EMPTY; + } + + configurationUtilities.ensureWhitelistedUri(configObject.apiUrl); + } catch (whitelistError) { + return i18n.WHITE_LISTED_ERROR(whitelistError.message); + } +}; + +const validateSecrets = ( + configurationUtilities: ActionsConfigurationUtilities, + secrets: ConnectorSecretConfigurationType +) => {}; + +export const validate: ConnectorValidation = { + config: validateConfig, + secrets: validateSecrets, +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/transformers.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/transformers.ts new file mode 100644 index 00000000000000..dc0a03fab8c715 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/transformers.ts @@ -0,0 +1,43 @@ +/* + * 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 { TransformerArgs } from './types'; +import * as i18n from './translations'; + +export const informationCreated = ({ + value, + date, + user, + ...rest +}: TransformerArgs): TransformerArgs => ({ + value: `${value} ${i18n.FIELD_INFORMATION('create', date, user)}`, + ...rest, +}); + +export const informationUpdated = ({ + value, + date, + user, + ...rest +}: TransformerArgs): TransformerArgs => ({ + value: `${value} ${i18n.FIELD_INFORMATION('update', date, user)}`, + ...rest, +}); + +export const informationAdded = ({ + value, + date, + user, + ...rest +}: TransformerArgs): TransformerArgs => ({ + value: `${value} ${i18n.FIELD_INFORMATION('add', date, user)}`, + ...rest, +}); + +export const append = ({ value, previousValue, ...rest }: TransformerArgs): TransformerArgs => ({ + value: previousValue ? `${previousValue} \r\n${value}` : `${value}`, + ...rest, +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/translations.ts new file mode 100644 index 00000000000000..5e1dee44b0764f --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/translations.ts @@ -0,0 +1,43 @@ +/* + * 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_REQUIRED = i18n.translate( + 'xpack.actions.builtin.connector.connectorApiNullError', + { + defaultMessage: 'connector [apiUrl] is required', + } +); + +export const FIELD_INFORMATION = ( + mode: string, + date: string | undefined, + user: string | undefined +) => { + switch (mode) { + case 'create': + return i18n.translate('xpack.actions.builtin.connector.informationCreated', { + values: { date, user }, + defaultMessage: '(created at {date} by {user})', + }); + case 'update': + return i18n.translate('xpack.actions.builtin.connector.informationUpdated', { + values: { date, user }, + defaultMessage: '(updated at {date} by {user})', + }); + case 'add': + return i18n.translate('xpack.actions.builtin.connector.informationAdded', { + values: { date, user }, + defaultMessage: '(added at {date} by {user})', + }); + default: + return i18n.translate('xpack.actions.builtin.connector.informationDefault', { + values: { date, user }, + defaultMessage: '(created at {date} by {user})', + }); + } +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/types.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/types.ts new file mode 100644 index 00000000000000..57ef97cb869a3f --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/types.ts @@ -0,0 +1,139 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TypeOf } from '@kbn/config-schema'; + +import { + ConnectorPublicConfigurationSchema, + ConnectorSecretConfigurationSchema, + ExecutorParamsSchema, + CaseConfigurationSchema, + MapRecordSchema, + CommentSchema, + ExecutorActionParamsSchema, +} from './schema'; +import { ActionsConfigurationUtilities } from '../../actions_config'; +import { ExecutorType } from '../../types'; + +export interface AnyParams { + [index: string]: string; +} + +export type ConnectorPublicConfigurationType = TypeOf; +export type ConnectorSecretConfigurationType = TypeOf; + +export type ExecutorParams = TypeOf; +export type ExecutorActionParams = TypeOf; + +export type CaseConfiguration = TypeOf; +export type MapRecord = TypeOf; +export type Comment = TypeOf; + +export interface ApiParams extends ExecutorActionParams { + externalCase: Record; +} + +export interface ConnectorConfiguration { + id: string; + name: string; +} + +export interface ExternalServiceCredential { + url: string; + username: string; + password: string; +} + +export interface ConnectorValidation { + config: ( + configurationUtilities: ActionsConfigurationUtilities, + configObject: ConnectorPublicConfigurationType + ) => void; + secrets: ( + configurationUtilities: ActionsConfigurationUtilities, + secrets: ConnectorSecretConfigurationType + ) => void; +} + +export interface ExternalServiceCaseResponse { + id: string; + title: string; + url: string; + pushedDate: string; +} + +export interface ExternalServiceCommentResponse { + commentId: string; + pushedDate: 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 ConnectorApiHandlerArgs { + externalService: ExternalService; + mapping: Map; + params: ApiParams; +} + +export interface PushToServiceResponse extends ExternalServiceCaseResponse { + comments?: ExternalServiceCommentResponse[]; +} + +export interface ConnectorApi { + handshake: (args: ConnectorApiHandlerArgs) => Promise; + pushToService: (args: ConnectorApiHandlerArgs) => Promise; + getIncident: (args: ConnectorApiHandlerArgs) => Promise; +} + +export interface CreateConnectorBasicArgs { + api: ConnectorApi; + createExternalService: (credentials: ExternalServiceCredential) => ExternalService; +} + +export interface CreateConnectorArgs extends CreateConnectorBasicArgs { + config: ConnectorConfiguration; + validate: ConnectorValidation; +} + +export interface CreateActionTypeArgs { + configurationUtilities: ActionsConfigurationUtilities; + executor?: ExecutorType; +} + +export interface PipedField { + key: string; + value: string; + actionType: string; + pipes: string[]; +} + +export interface PrepareFieldsForTransformArgs { + params: ApiParams; + mapping: Map; + defaultPipes?: string[]; +} + +export interface TransformFieldsArgs { + params: ExecutorActionParams; + fields: PipedField[]; + currentIncident?: ExternalServiceParams; +} + +export interface TransformerArgs { + value: string; + date?: string; + user?: string; + previousValue?: string; +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/utils.ts new file mode 100644 index 00000000000000..88fa8c0fc9dacf --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/utils.ts @@ -0,0 +1,230 @@ +/* + * 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 { curry, flow } from 'lodash'; +import { schema } from '@kbn/config-schema'; +import { AxiosInstance, Method, AxiosResponse } from 'axios'; + +import { ActionTypeExecutorOptions, ActionTypeExecutorResult, ActionType } from '../../types'; + +import { + ConnectorPublicConfiguration, + ConnectorSecretConfiguration, + ExecutorParamsSchema, +} from './schema'; + +import { + CreateConnectorArgs, + ConnectorPublicConfigurationType, + ConnectorSecretConfigurationType, + CreateActionTypeArgs, + ExecutorParams, + MapRecord, + AnyParams, + CreateConnectorBasicArgs, + PrepareFieldsForTransformArgs, + PipedField, + TransformFieldsArgs, + Comment, +} from './types'; + +import * as transformers from './transformers'; + +import { SUPPORTED_SOURCE_FIELDS } from './constants'; + +export const normalizeMapping = (supportedFields: string[], mapping: MapRecord[]): MapRecord[] => { + // Prevent prototype pollution and remove unsupported fields + return mapping.filter( + m => m.source !== '__proto__' && m.target !== '__proto__' && supportedFields.includes(m.source) + ); +}; + +export const buildMap = (mapping: MapRecord[]): Map => { + return normalizeMapping(SUPPORTED_SOURCE_FIELDS, mapping).reduce((fieldsMap, field) => { + const { source, target, actionType } = field; + fieldsMap.set(source, { target, actionType }); + fieldsMap.set(target, { target: source, actionType }); + return fieldsMap; + }, new Map()); +}; + +export const mapParams = (params: any, mapping: Map) => { + return Object.keys(params).reduce((prev: AnyParams, curr: string): AnyParams => { + const field = mapping.get(curr); + if (field) { + prev[field.target] = params[curr]; + } + return prev; + }, {}); +}; + +export const createConnectorExecutor = ({ + api, + createExternalService, +}: CreateConnectorBasicArgs) => async ( + execOptions: ActionTypeExecutorOptions +): Promise => { + const actionId = execOptions.actionId; + const { + apiUrl, + casesConfiguration: { mapping: configurationMapping }, + } = execOptions.config as ConnectorPublicConfigurationType; + + const { username, password } = execOptions.secrets as ConnectorSecretConfigurationType; + const params = execOptions.params as ExecutorParams; + const { action, actionParams } = params; + const { comments, externalCaseId, ...restParams } = actionParams; + + const mapping = buildMap(configurationMapping); + const externalCase = mapParams(restParams, mapping); + + const res: Pick & + Pick = { + status: 'ok', + actionId, + }; + + const externalService = createExternalService({ url: apiUrl, username, password }); + + if (!api[action]) { + throw new Error('[Action][Connector] Unsupported action type'); + } + + const data = await api[action]({ + externalService, + mapping, + params: { ...actionParams, externalCase }, + }); + + return { + ...res, + data, + }; +}; + +export const createConnector = ({ + api, + config, + validate, + createExternalService, +}: CreateConnectorArgs) => { + return ({ + configurationUtilities, + executor = createConnectorExecutor({ api, createExternalService }), + }: CreateActionTypeArgs): ActionType => ({ + id: config.id, + name: config.name, + minimumLicenseRequired: 'platinum', + validate: { + config: schema.object(ConnectorPublicConfiguration, { + validate: curry(validate.config)(configurationUtilities), + }), + secrets: schema.object(ConnectorSecretConfiguration, { + validate: curry(validate.secrets)(configurationUtilities), + }), + params: ExecutorParamsSchema, + }, + executor, + }); +}; + +export const throwIfNotAlive = ( + status: number, + contentType: string, + validStatusCodes: number[] = [200, 201] +) => { + if (!validStatusCodes.includes(status) || !contentType.includes('application/json')) { + throw new Error('Instance is not alive.'); + } +}; + +export const request = async ({ + axios, + url, + method = 'get', + data = {}, +}: { + axios: AxiosInstance; + url: string; + method?: Method; + data?: any; +}): Promise => { + const res = await axios(url, { method, data }); + throwIfNotAlive(res.status, res.headers['content-type']); + return res; +}; + +export const patch = ({ + axios, + url, + data, +}: { + axios: AxiosInstance; + url: string; + data: any; +}): Promise => { + return request({ + axios, + url, + method: 'patch', + data, + }); +}; + +export const addTimeZoneToDate = (date: string, timezone = 'GMT'): string => { + return `${date} ${timezone}`; +}; + +export const prepareFieldsForTransformation = ({ + params, + mapping, + defaultPipes = ['informationCreated'], +}: PrepareFieldsForTransformArgs): PipedField[] => { + return Object.keys(params.externalCase) + .filter(p => mapping.get(p).actionType !== 'nothing') + .map(p => ({ + key: p, + value: params.externalCase[p], + actionType: mapping.get(p).actionType, + pipes: [...defaultPipes], + })) + .map(p => ({ + ...p, + pipes: p.actionType === 'append' ? [...p.pipes, 'append'] : p.pipes, + })); +}; + +const t = { ...transformers } as { [index: string]: Function }; // TODO: Find a better solution if exists. + +export const transformFields = ({ params, fields, currentIncident }: TransformFieldsArgs) => { + return fields.reduce((prev, cur) => { + const transform = flow(...cur.pipes.map(p => t[p])); + prev[cur.key] = transform({ + value: cur.value, + date: params.updatedAt ?? params.createdAt, + user: + params.updatedBy != null + ? params.updatedBy.fullName ?? params.updatedBy.username + : params.createdBy.fullName ?? params.createdBy.username, + previousValue: currentIncident ? currentIncident[cur.key] : '', + }).value; + return prev; + }, {} as any); +}; + +export const transformComments = (comments: Comment[], pipes: string[]): Comment[] => { + return comments.map(c => ({ + ...c, + comment: flow(...pipes.map(p => t[p]))({ + value: c.comment, + date: c.updatedAt ?? c.createdAt, + user: + c.updatedBy != null + ? c.updatedBy.fullName ?? c.updatedBy.username + : c.createdBy.fullName ?? c.createdBy.username, + }).value, + })); +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/index.ts b/x-pack/plugins/actions/server/builtin_action_types/index.ts index a92a279d084399..5e092efdef8f7d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/index.ts @@ -12,10 +12,12 @@ import { getActionType as getEmailActionType } from './email'; import { getActionType as getIndexActionType } from './es_index'; import { getActionType as getPagerDutyActionType } from './pagerduty'; import { getActionType as getServerLogActionType } from './server_log'; -import { getActionType as getServiceNowActionType } from './servicenow'; import { getActionType as getSlackActionType } from './slack'; import { getActionType as getWebhookActionType } from './webhook'; +// Connectors +import { getServiceNowConnector } from './connectors'; + export function registerBuiltInActionTypes({ actionsConfigUtils: configurationUtilities, actionTypeRegistry, @@ -29,7 +31,9 @@ export function registerBuiltInActionTypes({ actionTypeRegistry.register(getIndexActionType({ logger })); actionTypeRegistry.register(getPagerDutyActionType({ logger, configurationUtilities })); actionTypeRegistry.register(getServerLogActionType({ logger })); - actionTypeRegistry.register(getServiceNowActionType({ configurationUtilities })); actionTypeRegistry.register(getSlackActionType({ configurationUtilities })); actionTypeRegistry.register(getWebhookActionType({ logger, configurationUtilities })); + + // Connectors + actionTypeRegistry.register(getServiceNowConnector({ configurationUtilities })); } From 812b4b260e49c18763ea025038956cdedcb6232a Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 14 Apr 2020 19:07:41 +0300 Subject: [PATCH 02/33] Add tests --- .../builtin_action_types/connectors/schema.ts | 3 +- .../connectors/servicenow/api.test.ts | 526 ++++++++++++++++ .../connectors/servicenow/api.ts | 10 +- .../connectors/servicenow/mocks.ts | 102 +++ .../connectors/servicenow/service.test.ts | 255 ++++++++ .../connectors/servicenow/service.ts | 2 +- .../connectors/transformers.test.ts | 129 ++++ .../connectors/utils.test.ts | 581 ++++++++++++++++++ .../builtin_action_types/connectors/utils.ts | 18 +- x-pack/plugins/case/common/api/cases/case.ts | 6 +- 10 files changed, 1616 insertions(+), 16 deletions(-) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/api.test.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/mocks.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.test.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/connectors/transformers.test.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/connectors/utils.test.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/schema.ts index 0527f40e207f84..5a2d4f8f323e3d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/schema.ts @@ -39,7 +39,6 @@ export const ConnectorSecretConfigurationSchema = schema.object(ConnectorSecretC export const UserSchema = schema.object({ fullName: schema.oneOf([schema.nullable(schema.string()), schema.maybe(schema.string())]), username: schema.oneOf([schema.nullable(schema.string()), schema.maybe(schema.string())]), - email: schema.oneOf([schema.nullable(schema.string()), schema.maybe(schema.string())]), }); const EntityInformation = { @@ -69,7 +68,7 @@ export const ExecutorActionParams = { title: schema.string(), description: schema.maybe(schema.string()), comments: schema.maybe(schema.arrayOf(CommentSchema)), - externalCaseId: schema.nullable(schema.string()), + externalId: schema.nullable(schema.string()), ...EntityInformation, }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/api.test.ts new file mode 100644 index 00000000000000..0ce7d2dfac9e4f --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/api.test.ts @@ -0,0 +1,526 @@ +/* + * 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 { api } from './api'; +import { externalServiceMock, mapping, apiParams } from './mocks'; +import { ExternalService } from '../types'; + +describe('api', () => { + let externalService: jest.Mocked; + + beforeAll(() => { + externalService = externalServiceMock.create(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('pushToService', () => { + describe('create incident', () => { + test('it creates an incident', async () => { + const params = { ...apiParams, externalId: null }; + const res = await api.pushToService({ externalService, mapping, params }); + + expect(res).toEqual({ + id: 'incident-1', + title: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', + comments: [ + { + commentId: 'case-comment-1', + pushedDate: '2020-03-10T12:24:20.000Z', + }, + { + commentId: 'case-comment-2', + pushedDate: '2020-03-10T12:24:20.000Z', + }, + ], + }); + }); + + test('it creates an incident without comments', async () => { + const params = { ...apiParams, externalId: null, comments: [] }; + const res = await api.pushToService({ externalService, mapping, params }); + + expect(res).toEqual({ + id: 'incident-1', + title: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', + }); + }); + + test('it calls createIncident correctly', async () => { + const params = { ...apiParams, externalId: null }; + await api.pushToService({ externalService, mapping, params }); + + expect(externalService.createIncident).toHaveBeenCalledWith({ + incident: { + description: + 'Incident description (created at 2020-03-13T08:34:53.450Z by Elastic User)', + short_description: + 'Incident title (created at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + expect(externalService.updateIncident).not.toHaveBeenCalled(); + }); + + test('it calls createComment correctly', async () => { + const params = { ...apiParams, externalId: null }; + await api.pushToService({ externalService, mapping, params }); + expect(externalService.createComment).toHaveBeenCalledTimes(2); + expect(externalService.createComment).toHaveBeenNthCalledWith(1, { + incidentId: 'incident-1', + comment: { + commentId: 'case-comment-1', + version: 'WzU3LDFd', + comment: 'A comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + updatedAt: '2020-03-13T08:34:53.450Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + }, + field: 'comments', + }); + + expect(externalService.createComment).toHaveBeenNthCalledWith(2, { + incidentId: 'incident-1', + comment: { + commentId: 'case-comment-2', + version: 'WlK3LDFd', + comment: 'Another comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + updatedAt: '2020-03-13T08:34:53.450Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + }, + field: 'comments', + }); + }); + }); + + describe('update incident', () => { + test('it updates an incident', async () => { + const res = await api.pushToService({ externalService, mapping, params: apiParams }); + + expect(res).toEqual({ + id: 'incident-2', + title: 'INC02', + pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', + comments: [ + { + commentId: 'case-comment-1', + pushedDate: '2020-03-10T12:24:20.000Z', + }, + { + commentId: 'case-comment-2', + pushedDate: '2020-03-10T12:24:20.000Z', + }, + ], + }); + }); + + test('it updates an incident without comments', async () => { + const params = { ...apiParams, comments: [] }; + const res = await api.pushToService({ externalService, mapping, params }); + + expect(res).toEqual({ + id: 'incident-2', + title: 'INC02', + pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', + }); + }); + + test('it calls updateIncident correctly', async () => { + const params = { ...apiParams }; + await api.pushToService({ externalService, mapping, params }); + + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + description: + 'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + short_description: + 'Incident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + expect(externalService.createIncident).not.toHaveBeenCalled(); + }); + + test('it calls createComment correctly', async () => { + const params = { ...apiParams }; + await api.pushToService({ externalService, mapping, params }); + expect(externalService.createComment).toHaveBeenCalledTimes(2); + expect(externalService.createComment).toHaveBeenNthCalledWith(1, { + incidentId: 'incident-2', + comment: { + commentId: 'case-comment-1', + version: 'WzU3LDFd', + comment: 'A comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + updatedAt: '2020-03-13T08:34:53.450Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + }, + field: 'comments', + }); + + expect(externalService.createComment).toHaveBeenNthCalledWith(2, { + incidentId: 'incident-2', + comment: { + commentId: 'case-comment-2', + version: 'WlK3LDFd', + comment: 'Another comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + updatedAt: '2020-03-13T08:34:53.450Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + }, + field: 'comments', + }); + }); + }); + + describe('mapping variations', () => { + test('overwrite & append', async () => { + mapping.set('title', { + target: 'short_description', + actionType: 'overwrite', + }); + + mapping.set('description', { + target: 'description', + actionType: 'append', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('short_description', { + target: 'title', + actionType: 'overwrite', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + short_description: + 'Incident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + description: + 'description from servicenow \r\nIncident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + }); + + test('nothing & append', async () => { + mapping.set('title', { + target: 'short_description', + actionType: 'nothing', + }); + + mapping.set('description', { + target: 'description', + actionType: 'append', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('short_description', { + target: 'title', + actionType: 'nothing', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + description: + 'description from servicenow \r\nIncident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + }); + + test('append & append', async () => { + mapping.set('title', { + target: 'short_description', + actionType: 'append', + }); + + mapping.set('description', { + target: 'description', + actionType: 'append', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('short_description', { + target: 'title', + actionType: 'append', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + short_description: + 'title from servicenow \r\nIncident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + description: + 'description from servicenow \r\nIncident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + }); + + test('nothing & nothing', async () => { + mapping.set('title', { + target: 'short_description', + actionType: 'nothing', + }); + + mapping.set('description', { + target: 'description', + actionType: 'nothing', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('short_description', { + target: 'title', + actionType: 'nothing', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: {}, + }); + }); + + test('overwrite & nothing', async () => { + mapping.set('title', { + target: 'short_description', + actionType: 'overwrite', + }); + + mapping.set('description', { + target: 'description', + actionType: 'nothing', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('short_description', { + target: 'title', + actionType: 'overwrite', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + short_description: + 'Incident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + }); + + test('overwrite & overwrite', async () => { + mapping.set('title', { + target: 'short_description', + actionType: 'overwrite', + }); + + mapping.set('description', { + target: 'description', + actionType: 'overwrite', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('short_description', { + target: 'title', + actionType: 'overwrite', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + short_description: + 'Incident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + description: + 'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + }); + + test('nothing & overwrite', async () => { + mapping.set('title', { + target: 'short_description', + actionType: 'nothing', + }); + + mapping.set('description', { + target: 'description', + actionType: 'overwrite', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('short_description', { + target: 'title', + actionType: 'nothing', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + description: + 'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + }); + + test('append & overwrite', async () => { + mapping.set('title', { + target: 'short_description', + actionType: 'append', + }); + + mapping.set('description', { + target: 'description', + actionType: 'overwrite', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('short_description', { + target: 'title', + actionType: 'append', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + short_description: + 'title from servicenow \r\nIncident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + description: + 'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + }); + + test('append & nothing', async () => { + mapping.set('title', { + target: 'short_description', + actionType: 'append', + }); + + mapping.set('description', { + target: 'description', + actionType: 'nothing', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('short_description', { + target: 'title', + actionType: 'append', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + short_description: + 'title from servicenow \r\nIncident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + }); + + test('comment nothing', async () => { + mapping.set('title', { + target: 'short_description', + actionType: 'overwrite', + }); + + mapping.set('description', { + target: 'description', + actionType: 'nothing', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'nothing', + }); + + mapping.set('short_description', { + target: 'title', + actionType: 'overwrite', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.createComment).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/api.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/api.ts index 3e82609ddf5bfa..687b853234ed7e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/api.ts @@ -32,14 +32,14 @@ const pushToServiceHandler = async ({ mapping, params, }: ConnectorApiHandlerArgs): Promise => { - const { externalCaseId, comments } = params; - const updateIncident = externalCaseId ? true : false; + const { externalId, comments } = params; + const updateIncident = externalId ? true : false; const defaultPipes = updateIncident ? ['informationUpdated'] : ['informationCreated']; let currentIncident: ExternalServiceParams | undefined; let res: PushToServiceResponse; - if (externalCaseId) { - currentIncident = await externalService.getIncident(externalCaseId); + if (externalId) { + currentIncident = await externalService.getIncident(externalId); } const fields = prepareFieldsForTransformation({ @@ -55,7 +55,7 @@ const pushToServiceHandler = async ({ }); if (updateIncident) { - res = await externalService.updateIncident({ incidentId: externalCaseId, incident }); + res = await externalService.updateIncident({ incidentId: externalId, incident }); } else { res = await externalService.createIncident({ incident }); } diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/mocks.ts new file mode 100644 index 00000000000000..52472d05633a28 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/mocks.ts @@ -0,0 +1,102 @@ +/* + * 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 { ExternalService, ApiParams, ExecutorActionParams } from '../types'; + +const createMock = (): jest.Mocked => ({ + getIncident: jest.fn().mockImplementation(() => + Promise.resolve({ + short_description: 'title from servicenow', + description: 'description from servicenow', + }) + ), + createIncident: jest.fn().mockImplementation(() => + Promise.resolve({ + id: 'incident-1', + title: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', + }) + ), + updateIncident: jest.fn().mockImplementation(() => + Promise.resolve({ + id: 'incident-2', + title: 'INC02', + pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', + }) + ), + createComment: jest.fn().mockImplementation(() => + Promise.resolve({ + commentId: 'comment-1', + pushedDate: '2020-03-10T12:24:20.000Z', + }) + ), +}); + +const externalServiceMock = { + create: createMock, +}; + +const mapping: Map = new Map(); + +mapping.set('title', { + target: 'short_description', + actionType: 'overwrite', +}); + +mapping.set('description', { + target: 'description', + actionType: 'overwrite', +}); + +mapping.set('comments', { + target: 'comments', + actionType: 'append', +}); + +mapping.set('short_description', { + target: 'title', + actionType: 'overwrite', +}); + +const executorParams: ExecutorActionParams = { + caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', + externalId: 'incident-3', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: '2020-03-13T08:34:53.450Z', + updatedBy: { fullName: 'Elastic User', username: 'elastic' }, + title: 'Incident title', + description: 'Incident description', + comments: [ + { + commentId: 'case-comment-1', + version: 'WzU3LDFd', + comment: 'A comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: '2020-03-13T08:34:53.450Z', + updatedBy: { fullName: 'Elastic User', username: 'elastic' }, + }, + { + commentId: 'case-comment-2', + version: 'WlK3LDFd', + comment: 'Another comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: '2020-03-13T08:34:53.450Z', + updatedBy: { fullName: 'Elastic User', username: 'elastic' }, + }, + ], +}; + +const apiParams: ApiParams = { + ...executorParams, + externalCase: { short_description: 'Incident title', description: 'Incident description' }, +}; + +export { externalServiceMock, mapping, executorParams, apiParams }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.test.ts new file mode 100644 index 00000000000000..da409903d8ea5d --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.test.ts @@ -0,0 +1,255 @@ +/* + * 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 axios from 'axios'; + +import { getErrorMessage, createExternalService } from './service'; +import * as utils from '../utils'; +import { ExternalService } from '../types'; +import { object } from 'joi'; + +jest.mock('axios'); +jest.mock('../utils', () => { + const originalUtils = jest.requireActual('../utils'); + return { + ...originalUtils, + request: jest.fn(), + patch: jest.fn(), + }; +}); + +axios.create = jest.fn(() => axios); +const requestMock = utils.request as jest.Mock; +const patchMock = utils.patch as jest.Mock; + +describe('ServiceNow service', () => { + let service: ExternalService; + + beforeAll(() => { + service = createExternalService({ + url: 'https://dev102283.service-now.com', + username: 'admin', + password: 'admin', + }); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('createExternalService', () => { + test('throws without url', () => { + expect(() => + createExternalService({ url: null, username: 'admin', password: 'admin' }) + ).toThrow(); + }); + + test('throws without username', () => { + expect(() => + createExternalService({ url: 'test.com', username: '', password: 'admin' }) + ).toThrow(); + }); + + test('throws without password', () => { + expect(() => + createExternalService({ url: 'test.com', username: '', password: undefined }) + ).toThrow(); + }); + }); + + describe('getErrorMessage', () => { + test('return correct message', () => { + const msg = getErrorMessage('An error has occurred'); + expect(msg).toBe('[Action][ServiceNow]: An error has occurred'); + }); + }); + + describe('getIncident', () => { + test('it returns the incident correctly', async () => { + requestMock.mockImplementation(() => ({ + data: { result: { sys_id: '1', number: 'INC01' } }, + })); + const res = await service.getIncident('1'); + expect(res).toEqual({ sys_id: '1', number: 'INC01' }); + }); + + test('it should call request with correct arguments', async () => { + requestMock.mockImplementation(() => ({ + data: { result: { sys_id: '1', number: 'INC01' } }, + })); + + await service.getIncident('1'); + expect(requestMock).toHaveBeenCalledWith({ + axios, + url: 'https://dev102283.service-now.com/api/now/v2/table/incident/1', + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementation(() => { + throw new Error('An error has occurred'); + }); + expect(service.getIncident('1')).rejects.toThrow( + 'Unable to get incident with id 1. Error: An error has occurred' + ); + }); + }); + + describe('createIncident', () => { + test('it creates the incident correctly', async () => { + requestMock.mockImplementation(() => ({ + data: { result: { sys_id: '1', number: 'INC01', sys_created_on: '2020-03-10 12:24:20' } }, + })); + + const res = await service.createIncident({ + incident: { short_description: 'title', description: 'desc' }, + }); + + expect(res).toEqual({ + title: 'INC01', + id: '1', + pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://dev102283.service-now.com/nav_to.do?uri=incident.do?sys_id=1', + }); + }); + + test('it should call request with correct arguments', async () => { + requestMock.mockImplementation(() => ({ + data: { result: { sys_id: '1', number: 'INC01', sys_created_on: '2020-03-10 12:24:20' } }, + })); + + await service.createIncident({ + incident: { short_description: 'title', description: 'desc' }, + }); + + expect(requestMock).toHaveBeenCalledWith({ + axios, + url: 'https://dev102283.service-now.com/api/now/v2/table/incident', + method: 'post', + data: { short_description: 'title', description: 'desc' }, + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementation(() => { + throw new Error('An error has occurred'); + }); + + expect( + service.createIncident({ + incident: { short_description: 'title', description: 'desc' }, + }) + ).rejects.toThrow( + '[Action][ServiceNow]: Unable to create incident. Error: An error has occurred' + ); + }); + }); + + describe('updateIncident', () => { + test('it updates the incident correctly', async () => { + patchMock.mockImplementation(() => ({ + data: { result: { sys_id: '1', number: 'INC01', sys_updated_on: '2020-03-10 12:24:20' } }, + })); + + const res = await service.updateIncident({ + incidentId: '1', + incident: { short_description: 'title', description: 'desc' }, + }); + + expect(res).toEqual({ + title: 'INC01', + id: '1', + pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://dev102283.service-now.com/nav_to.do?uri=incident.do?sys_id=1', + }); + }); + + test('it should call request with correct arguments', async () => { + patchMock.mockImplementation(() => ({ + data: { result: { sys_id: '1', number: 'INC01', sys_updated_on: '2020-03-10 12:24:20' } }, + })); + + await service.updateIncident({ + incidentId: '1', + incident: { short_description: 'title', description: 'desc' }, + }); + + expect(patchMock).toHaveBeenCalledWith({ + axios, + url: 'https://dev102283.service-now.com/api/now/v2/table/incident/1', + data: { short_description: 'title', description: 'desc' }, + }); + }); + + test('it should throw an error', async () => { + patchMock.mockImplementation(() => { + throw new Error('An error has occurred'); + }); + + expect( + service.updateIncident({ + incidentId: '1', + incident: { short_description: 'title', description: 'desc' }, + }) + ).rejects.toThrow( + '[Action][ServiceNow]: Unable to update incident with id 1. Error: An error has occurred' + ); + }); + }); + + describe('createComment', () => { + test('it creates the comment correctly', async () => { + patchMock.mockImplementation(() => ({ + data: { result: { sys_id: '1', number: 'INC01', sys_updated_on: '2020-03-10 12:24:20' } }, + })); + + const res = await service.createComment({ + incidentId: '1', + comment: { comment: 'comment', commentId: 'comment-1' }, + field: 'comments', + }); + + expect(res).toEqual({ + commentId: 'comment-1', + pushedDate: '2020-03-10T12:24:20.000Z', + }); + }); + + test('it should call request with correct arguments', async () => { + patchMock.mockImplementation(() => ({ + data: { result: { sys_id: '1', number: 'INC01', sys_updated_on: '2020-03-10 12:24:20' } }, + })); + + await service.createComment({ + incidentId: '1', + comment: { comment: 'comment', commentId: 'comment-1' }, + field: 'my_field', + }); + + expect(patchMock).toHaveBeenCalledWith({ + axios, + url: 'https://dev102283.service-now.com/api/now/v2/table/incident/1', + data: { my_field: 'comment' }, + }); + }); + + test('it should throw an error', async () => { + patchMock.mockImplementation(() => { + throw new Error('An error has occurred'); + }); + + expect( + service.createComment({ + incidentId: '1', + comment: { comment: 'comment', commentId: 'comment-1' }, + field: 'comments', + }) + ).rejects.toThrow( + '[Action][ServiceNow]: Unable to create comment at incident with id 1. Error: An error has occurred' + ); + }); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.ts index b1bfbaf711fcc5..92748aa7f3929a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.ts @@ -16,7 +16,7 @@ const COMMENT_URL = `api/now/${API_VERSION}/table/incident`; // Based on: https://docs.servicenow.com/bundle/orlando-platform-user-interface/page/use/navigation/reference/r_NavigatingByURLExamples.html const VIEW_INCIDENT_URL = `nav_to.do?uri=incident.do?sys_id=`; -const getErrorMessage = (msg: string) => { +export const getErrorMessage = (msg: string) => { return `[Action][ServiceNow]: ${msg}`; }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/transformers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/transformers.test.ts new file mode 100644 index 00000000000000..5254a22237e57a --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/transformers.test.ts @@ -0,0 +1,129 @@ +/* + * 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 { informationCreated, informationUpdated, informationAdded, append } from './transformers'; + +describe('informationCreated', () => { + test('transforms correctly', () => { + const res = informationCreated({ + value: 'a value', + date: '2020-04-15T08:19:27.400Z', + user: 'elastic', + }); + expect(res).toEqual({ value: 'a value (created at 2020-04-15T08:19:27.400Z by elastic)' }); + }); + + test('transforms correctly without optional fields', () => { + const res = informationCreated({ + value: 'a value', + }); + expect(res).toEqual({ value: 'a value (created at by )' }); + }); + + test('returns correctly rest fields', () => { + const res = informationCreated({ + value: 'a value', + date: '2020-04-15T08:19:27.400Z', + user: 'elastic', + previousValue: 'previous value', + }); + expect(res).toEqual({ + value: 'a value (created at 2020-04-15T08:19:27.400Z by elastic)', + previousValue: 'previous value', + }); + }); +}); + +describe('informationUpdated', () => { + test('transforms correctly', () => { + const res = informationUpdated({ + value: 'a value', + date: '2020-04-15T08:19:27.400Z', + user: 'elastic', + }); + expect(res).toEqual({ value: 'a value (updated at 2020-04-15T08:19:27.400Z by elastic)' }); + }); + + test('transforms correctly without optional fields', () => { + const res = informationUpdated({ + value: 'a value', + }); + expect(res).toEqual({ value: 'a value (updated at by )' }); + }); + + test('returns correctly rest fields', () => { + const res = informationUpdated({ + value: 'a value', + date: '2020-04-15T08:19:27.400Z', + user: 'elastic', + previousValue: 'previous value', + }); + expect(res).toEqual({ + value: 'a value (updated at 2020-04-15T08:19:27.400Z by elastic)', + previousValue: 'previous value', + }); + }); +}); + +describe('informationAdded', () => { + test('transforms correctly', () => { + const res = informationAdded({ + value: 'a value', + date: '2020-04-15T08:19:27.400Z', + user: 'elastic', + }); + expect(res).toEqual({ value: 'a value (added at 2020-04-15T08:19:27.400Z by elastic)' }); + }); + + test('transforms correctly without optional fields', () => { + const res = informationAdded({ + value: 'a value', + }); + expect(res).toEqual({ value: 'a value (added at by )' }); + }); + + test('returns correctly rest fields', () => { + const res = informationAdded({ + value: 'a value', + date: '2020-04-15T08:19:27.400Z', + user: 'elastic', + previousValue: 'previous value', + }); + expect(res).toEqual({ + value: 'a value (added at 2020-04-15T08:19:27.400Z by elastic)', + previousValue: 'previous value', + }); + }); +}); + +describe('append', () => { + test('transforms correctly', () => { + const res = append({ + value: 'a value', + previousValue: 'previous value', + }); + expect(res).toEqual({ value: 'previous value \r\na value' }); + }); + + test('transforms correctly without optional fields', () => { + const res = append({ + value: 'a value', + }); + expect(res).toEqual({ value: 'a value' }); + }); + + test('returns correctly rest fields', () => { + const res = append({ + value: 'a value', + user: 'elastic', + previousValue: 'previous value', + }); + expect(res).toEqual({ + value: 'previous value \r\na value', + user: 'elastic', + }); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/utils.test.ts new file mode 100644 index 00000000000000..fc0838f5c85c39 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/utils.test.ts @@ -0,0 +1,581 @@ +/* + * 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 axios from 'axios'; + +import { + normalizeMapping, + buildMap, + mapParams, + prepareFieldsForTransformation, + transformFields, + transformComments, + addTimeZoneToDate, + throwIfNotAlive, + request, + patch, +} from './utils'; + +import { SUPPORTED_SOURCE_FIELDS } from './constants'; +import { Comment, MapRecord, ApiParams } from './types'; + +jest.mock('axios'); +const axiosMock = (axios as unknown) as jest.Mock; + +const mapping: MapRecord[] = [ + { source: 'title', target: 'short_description', actionType: 'overwrite' }, + { source: 'description', target: 'description', actionType: 'append' }, + { source: 'comments', target: 'comments', actionType: 'append' }, +]; + +const finalMapping: Map = new Map(); + +finalMapping.set('title', { + target: 'short_description', + actionType: 'overwrite', +}); + +finalMapping.set('description', { + target: 'description', + actionType: 'append', +}); + +finalMapping.set('comments', { + target: 'comments', + actionType: 'append', +}); + +finalMapping.set('short_description', { + target: 'title', + actionType: 'overwrite', +}); + +const maliciousMapping: MapRecord[] = [ + { source: '__proto__', target: 'short_description', actionType: 'nothing' }, + { source: 'description', target: '__proto__', actionType: 'nothing' }, + { source: 'comments', target: 'comments', actionType: 'nothing' }, + { source: 'unsupportedSource', target: 'comments', actionType: 'nothing' }, +]; + +const fullParams: ApiParams = { + caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', + title: 'a title', + description: 'a description', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + externalId: null, + externalCase: { + short_description: 'a title', + description: 'a description', + }, + comments: [ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + version: 'WzU3LDFd', + comment: 'first comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + }, + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + version: 'WzU3LDFd', + comment: 'second comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + }, + ], +}; + +describe('normalizeMapping', () => { + test('remove malicious fields', () => { + const sanitizedMapping = normalizeMapping(SUPPORTED_SOURCE_FIELDS, maliciousMapping); + expect(sanitizedMapping.every(m => m.source !== '__proto__' && m.target !== '__proto__')).toBe( + true + ); + }); + + test('remove unsuppported source fields', () => { + const normalizedMapping = normalizeMapping(SUPPORTED_SOURCE_FIELDS, maliciousMapping); + expect(normalizedMapping).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + source: 'unsupportedSource', + target: 'comments', + actionType: 'nothing', + }), + ]) + ); + }); +}); + +describe('buildMap', () => { + test('builds sanitized Map', () => { + const finalMap = buildMap(maliciousMapping); + expect(finalMap.get('__proto__')).not.toBeDefined(); + }); + + test('builds Map correct', () => { + const final = buildMap(mapping); + expect(final).toEqual(finalMapping); + }); +}); + +describe('mapParams', () => { + test('maps params correctly', () => { + const params = { + caseId: '123', + incidentId: '456', + title: 'Incident title', + description: 'Incident description', + }; + + const fields = mapParams(params, finalMapping); + + expect(fields).toEqual({ + short_description: 'Incident title', + description: 'Incident description', + }); + }); + + test('do not add fields not in mapping', () => { + const params = { + caseId: '123', + incidentId: '456', + title: 'Incident title', + description: 'Incident description', + }; + const fields = mapParams(params, finalMapping); + + const { title, description, ...unexpectedFields } = params; + + expect(fields).not.toEqual(expect.objectContaining(unexpectedFields)); + }); +}); + +describe('prepareFieldsForTransformation', () => { + test('prepare fields with defaults', () => { + const res = prepareFieldsForTransformation({ + params: fullParams, + mapping: finalMapping, + }); + expect(res).toEqual([ + { + key: 'short_description', + value: 'a title', + actionType: 'overwrite', + pipes: ['informationCreated'], + }, + { + key: 'description', + value: 'a description', + actionType: 'append', + pipes: ['informationCreated', 'append'], + }, + ]); + }); + + test('prepare fields with default pipes', () => { + const res = prepareFieldsForTransformation({ + params: fullParams, + mapping: finalMapping, + defaultPipes: ['myTestPipe'], + }); + expect(res).toEqual([ + { + key: 'short_description', + value: 'a title', + actionType: 'overwrite', + pipes: ['myTestPipe'], + }, + { + key: 'description', + value: 'a description', + actionType: 'append', + pipes: ['myTestPipe', 'append'], + }, + ]); + }); +}); + +describe('transformFields', () => { + test('transform fields for creation correctly', () => { + const fields = prepareFieldsForTransformation({ + params: fullParams, + mapping: finalMapping, + }); + + const res = transformFields({ + params: fullParams, + fields, + }); + + expect(res).toEqual({ + short_description: 'a title (created at 2020-03-13T08:34:53.450Z by Elastic User)', + description: 'a description (created at 2020-03-13T08:34:53.450Z by Elastic User)', + }); + }); + + test('transform fields for update correctly', () => { + const fields = prepareFieldsForTransformation({ + params: { + ...fullParams, + updatedAt: '2020-03-15T08:34:53.450Z', + updatedBy: { + username: 'anotherUser', + fullName: 'Another User', + }, + }, + mapping: finalMapping, + defaultPipes: ['informationUpdated'], + }); + + const res = transformFields({ + params: { + ...fullParams, + updatedAt: '2020-03-15T08:34:53.450Z', + updatedBy: { + username: 'anotherUser', + fullName: 'Another User', + }, + }, + fields, + currentIncident: { + short_description: 'first title (created at 2020-03-13T08:34:53.450Z by Elastic User)', + description: 'first description (created at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + expect(res).toEqual({ + short_description: 'a title (updated at 2020-03-15T08:34:53.450Z by Another User)', + description: + 'first description (created at 2020-03-13T08:34:53.450Z by Elastic User) \r\na description (updated at 2020-03-15T08:34:53.450Z by Another User)', + }); + }); + + test('add newline character to descripton', () => { + const fields = prepareFieldsForTransformation({ + params: fullParams, + mapping: finalMapping, + defaultPipes: ['informationUpdated'], + }); + + const res = transformFields({ + params: fullParams, + fields, + currentIncident: { + short_description: 'first title', + description: 'first description', + }, + }); + expect(res.description?.includes('\r\n')).toBe(true); + }); + + test('append username if fullname is undefined when create', () => { + const fields = prepareFieldsForTransformation({ + params: fullParams, + mapping: finalMapping, + }); + + const res = transformFields({ + params: { + ...fullParams, + createdBy: { fullName: '', username: 'elastic' }, + }, + fields, + }); + + expect(res).toEqual({ + short_description: 'a title (created at 2020-03-13T08:34:53.450Z by elastic)', + description: 'a description (created at 2020-03-13T08:34:53.450Z by elastic)', + }); + }); + + test('append username if fullname is undefined when update', () => { + const fields = prepareFieldsForTransformation({ + params: { + ...fullParams, + updatedAt: '2020-03-15T08:34:53.450Z', + updatedBy: { + username: 'anotherUser', + fullName: 'Another User', + }, + }, + mapping: finalMapping, + defaultPipes: ['informationUpdated'], + }); + + const res = transformFields({ + params: { + ...fullParams, + updatedAt: '2020-03-15T08:34:53.450Z', + updatedBy: { username: 'anotherUser', fullName: '' }, + }, + fields, + }); + + expect(res).toEqual({ + short_description: 'a title (updated at 2020-03-15T08:34:53.450Z by anotherUser)', + description: 'a description (updated at 2020-03-15T08:34:53.450Z by anotherUser)', + }); + }); +}); + +describe('transformComments', () => { + test('transform creation comments', () => { + const comments: Comment[] = [ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + version: 'WzU3LDFd', + comment: 'first comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + }, + ]; + const res = transformComments(comments, ['informationCreated']); + expect(res).toEqual([ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + version: 'WzU3LDFd', + comment: 'first comment (created at 2020-03-13T08:34:53.450Z by Elastic User)', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + }, + ]); + }); + + test('transform update comments', () => { + const comments: Comment[] = [ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + version: 'WzU3LDFd', + comment: 'first comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: '2020-03-15T08:34:53.450Z', + updatedBy: { + fullName: 'Another User', + username: 'anotherUser', + }, + }, + ]; + const res = transformComments(comments, ['informationUpdated']); + expect(res).toEqual([ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + version: 'WzU3LDFd', + comment: 'first comment (updated at 2020-03-15T08:34:53.450Z by Another User)', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: '2020-03-15T08:34:53.450Z', + updatedBy: { + fullName: 'Another User', + username: 'anotherUser', + }, + }, + ]); + }); + + test('transform added comments', () => { + const comments: Comment[] = [ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + version: 'WzU3LDFd', + comment: 'first comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + }, + ]; + const res = transformComments(comments, ['informationAdded']); + expect(res).toEqual([ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + version: 'WzU3LDFd', + comment: 'first comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + }, + ]); + }); + + test('transform comments without fullname', () => { + const comments: Comment[] = [ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + version: 'WzU3LDFd', + comment: 'first comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: '', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + }, + ]; + const res = transformComments(comments, ['informationAdded']); + expect(res).toEqual([ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + version: 'WzU3LDFd', + comment: 'first comment (added at 2020-03-13T08:34:53.450Z by elastic)', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: '', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + }, + ]); + }); + + test('adds update user correctly', () => { + const comments: Comment[] = [ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + version: 'WzU3LDFd', + comment: 'first comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic', username: 'elastic' }, + updatedAt: '2020-04-13T08:34:53.450Z', + updatedBy: { fullName: 'Elastic2', username: 'elastic' }, + }, + ]; + const res = transformComments(comments, ['informationAdded']); + expect(res).toEqual([ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + version: 'WzU3LDFd', + comment: 'first comment (added at 2020-04-13T08:34:53.450Z by Elastic2)', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic', username: 'elastic' }, + updatedAt: '2020-04-13T08:34:53.450Z', + updatedBy: { fullName: 'Elastic2', username: 'elastic' }, + }, + ]); + }); + + test('adds update user with empty fullname correctly', () => { + const comments: Comment[] = [ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + version: 'WzU3LDFd', + comment: 'first comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic', username: 'elastic' }, + updatedAt: '2020-04-13T08:34:53.450Z', + updatedBy: { fullName: '', username: 'elastic2' }, + }, + ]; + const res = transformComments(comments, ['informationAdded']); + expect(res).toEqual([ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + version: 'WzU3LDFd', + comment: 'first comment (added at 2020-04-13T08:34:53.450Z by elastic2)', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic', username: 'elastic' }, + updatedAt: '2020-04-13T08:34:53.450Z', + updatedBy: { fullName: '', username: 'elastic2' }, + }, + ]); + }); +}); + +describe('addTimeZoneToDate', () => { + test('adds timezone with default', () => { + const date = addTimeZoneToDate('2020-04-14T15:01:55.456Z'); + expect(date).toBe('2020-04-14T15:01:55.456Z GMT'); + }); + + test('adds timezone correctly', () => { + const date = addTimeZoneToDate('2020-04-14T15:01:55.456Z', 'PST'); + expect(date).toBe('2020-04-14T15:01:55.456Z PST'); + }); +}); + +describe('throwIfNotAlive ', () => { + test('throws correctly when status is invalid', async () => { + expect(() => { + throwIfNotAlive(404, 'application/json'); + }).toThrow('Instance is not alive.'); + }); + + test('throws correctly when content is invalid', () => { + expect(() => { + throwIfNotAlive(200, 'application/html'); + }).toThrow('Instance is not alive.'); + }); + + test('do NOT throws with custom validStatusCodes', async () => { + expect(() => { + throwIfNotAlive(404, 'application/json', [404]); + }).not.toThrow('Instance is not alive.'); + }); +}); + +describe('request', () => { + beforeEach(() => { + axiosMock.mockImplementation(() => ({ + status: 200, + headers: { 'content-type': 'application/json' }, + data: { incidentId: '123' }, + })); + }); + + test('it fetch correctly with defaults', async () => { + const res = await request({ axios, url: '/test' }); + + expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'get', data: {} }); + expect(res).toEqual({ + status: 200, + headers: { 'content-type': 'application/json' }, + data: { incidentId: '123' }, + }); + }); + + test('it fetch correctly', async () => { + const res = await request({ axios, url: '/test', method: 'post', data: { id: '123' } }); + + expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'post', data: { id: '123' } }); + expect(res).toEqual({ + status: 200, + headers: { 'content-type': 'application/json' }, + data: { incidentId: '123' }, + }); + }); + + test('it throws correctly', async () => { + axiosMock.mockImplementation(() => ({ + status: 404, + headers: { 'content-type': 'application/json' }, + data: { incidentId: '123' }, + })); + + await expect(request({ axios, url: '/test' })).rejects.toThrow(); + }); +}); + +describe('patch', () => { + beforeEach(() => { + axiosMock.mockImplementation(() => ({ + status: 200, + headers: { 'content-type': 'application/json' }, + })); + }); + + test('it fetch correctly', async () => { + await patch({ axios, url: '/test', data: { id: '123' } }); + expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'patch', data: { id: '123' } }); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/utils.ts index 88fa8c0fc9dacf..2df31f6437bf7b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/utils.ts @@ -76,7 +76,7 @@ export const createConnectorExecutor = ({ const { username, password } = execOptions.secrets as ConnectorSecretConfigurationType; const params = execOptions.params as ExecutorParams; const { action, actionParams } = params; - const { comments, externalCaseId, ...restParams } = actionParams; + const { comments, externalId, ...restParams } = actionParams; const mapping = buildMap(configurationMapping); const externalCase = mapParams(restParams, mapping); @@ -207,8 +207,12 @@ export const transformFields = ({ params, fields, currentIncident }: TransformFi date: params.updatedAt ?? params.createdAt, user: params.updatedBy != null - ? params.updatedBy.fullName ?? params.updatedBy.username - : params.createdBy.fullName ?? params.createdBy.username, + ? params.updatedBy.fullName + ? params.updatedBy.fullName + : params.updatedBy.username + : params.createdBy.fullName + ? params.createdBy.fullName + : params.createdBy.username, previousValue: currentIncident ? currentIncident[cur.key] : '', }).value; return prev; @@ -223,8 +227,12 @@ export const transformComments = (comments: Comment[], pipes: string[]): Comment date: c.updatedAt ?? c.createdAt, user: c.updatedBy != null - ? c.updatedBy.fullName ?? c.updatedBy.username - : c.createdBy.fullName ?? c.createdBy.username, + ? c.updatedBy.fullName + ? c.updatedBy.fullName + : c.updatedBy.username + : c.createdBy.fullName + ? c.createdBy.fullName + : c.createdBy.username, }).value, })); }; diff --git a/x-pack/plugins/case/common/api/cases/case.ts b/x-pack/plugins/case/common/api/cases/case.ts index 1f08a410249057..47a00f5c96c65d 100644 --- a/x-pack/plugins/case/common/api/cases/case.ts +++ b/x-pack/plugins/case/common/api/cases/case.ts @@ -132,7 +132,7 @@ export const ServiceConnectorCaseParamsRt = rt.intersection([ caseId: rt.string, createdAt: rt.string, createdBy: ServiceConnectorUserParams, - incidentId: rt.union([rt.string, rt.null]), + externalId: rt.union([rt.string, rt.null]), title: rt.string, updatedAt: rt.union([rt.string, rt.null]), updatedBy: rt.union([ServiceConnectorUserParams, rt.null]), @@ -145,8 +145,8 @@ export const ServiceConnectorCaseParamsRt = rt.intersection([ export const ServiceConnectorCaseResponseRt = rt.intersection([ rt.type({ - number: rt.string, - incidentId: rt.string, + title: rt.string, + id: rt.string, pushedDate: rt.string, url: rt.string, }), From f6708aceb820ae0ad48740d04cf1c8aa8dda6cf9 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 15 Apr 2020 15:58:25 +0300 Subject: [PATCH 03/33] Delete servicenow --- .../servicenow/action_handlers.test.ts | 850 ------------------ .../servicenow/action_handlers.ts | 129 --- .../servicenow/constants.ts | 8 - .../servicenow/helpers.test.ts | 409 --------- .../servicenow/helpers.ts | 125 --- .../builtin_action_types/servicenow/index.ts | 111 --- .../servicenow/lib/constants.ts | 13 - .../servicenow/lib/index.test.ts | 334 ------- .../servicenow/lib/index.ts | 186 ---- .../servicenow/lib/types.ts | 32 - .../builtin_action_types/servicenow/mock.ts | 115 --- .../builtin_action_types/servicenow/schema.ts | 70 -- .../servicenow/transformers.ts | 43 - .../servicenow/translations.ts | 82 -- .../builtin_action_types/servicenow/types.ts | 103 --- 15 files changed, 2610 deletions(-) delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/constants.ts delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/constants.ts delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/types.ts delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/transformers.ts delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts deleted file mode 100644 index aa9b1dcfcf239b..00000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts +++ /dev/null @@ -1,850 +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 { - handleCreateIncident, - handleUpdateIncident, - handleIncident, - createComments, -} from './action_handlers'; -import { ServiceNow } from './lib'; -import { Mapping } from './types'; - -jest.mock('./lib'); - -const ServiceNowMock = ServiceNow as jest.Mock; - -const finalMapping: Mapping = new Map(); - -finalMapping.set('title', { - target: 'short_description', - actionType: 'overwrite', -}); - -finalMapping.set('description', { - target: 'description', - actionType: 'overwrite', -}); - -finalMapping.set('comments', { - target: 'comments', - actionType: 'append', -}); - -finalMapping.set('short_description', { - target: 'title', - actionType: 'overwrite', -}); - -const params = { - caseId: '123', - title: 'a title', - description: 'a description', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - incidentId: null, - incident: { - short_description: 'a title', - description: 'a description', - }, - comments: [ - { - commentId: '456', - version: 'WzU3LDFd', - comment: 'first comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - }, - ], -}; - -beforeAll(() => { - ServiceNowMock.mockImplementation(() => { - return { - serviceNow: { - getUserID: jest.fn().mockResolvedValue('1234'), - getIncident: jest.fn().mockResolvedValue({ - short_description: 'servicenow title', - description: 'servicenow desc', - }), - createIncident: jest.fn().mockResolvedValue({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }), - updateIncident: jest.fn().mockResolvedValue({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }), - batchCreateComments: jest - .fn() - .mockResolvedValue([{ commentId: '456', pushedDate: '2020-03-10T12:24:20.000Z' }]), - }, - }; - }); -}); - -describe('handleIncident', () => { - test('create an incident', async () => { - const { serviceNow } = new ServiceNowMock(); - - const res = await handleIncident({ - incidentId: null, - serviceNow, - params, - comments: params.comments, - mapping: finalMapping, - }); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - comments: [ - { - commentId: '456', - pushedDate: '2020-03-10T12:24:20.000Z', - }, - ], - }); - }); - test('update an incident', async () => { - const { serviceNow } = new ServiceNowMock(); - - const res = await handleIncident({ - incidentId: '123', - serviceNow, - params, - comments: params.comments, - mapping: finalMapping, - }); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - comments: [ - { - commentId: '456', - pushedDate: '2020-03-10T12:24:20.000Z', - }, - ], - }); - }); -}); - -describe('handleCreateIncident', () => { - test('create an incident without comments', async () => { - const { serviceNow } = new ServiceNowMock(); - - const res = await handleCreateIncident({ - serviceNow, - params, - comments: [], - mapping: finalMapping, - }); - - expect(serviceNow.createIncident).toHaveBeenCalled(); - expect(serviceNow.createIncident).toHaveBeenCalledWith({ - short_description: 'a title (created at 2020-03-13T08:34:53.450Z by Elastic User)', - description: 'a description (created at 2020-03-13T08:34:53.450Z by Elastic User)', - }); - expect(serviceNow.createIncident).toHaveReturned(); - expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }); - }); - - test('create an incident with comments', async () => { - const { serviceNow } = new ServiceNowMock(); - - const res = await handleCreateIncident({ - serviceNow, - params, - comments: params.comments, - mapping: finalMapping, - }); - - expect(serviceNow.createIncident).toHaveBeenCalled(); - expect(serviceNow.createIncident).toHaveBeenCalledWith({ - description: 'a description (created at 2020-03-13T08:34:53.450Z by Elastic User)', - short_description: 'a title (created at 2020-03-13T08:34:53.450Z by Elastic User)', - }); - expect(serviceNow.createIncident).toHaveReturned(); - expect(serviceNow.batchCreateComments).toHaveBeenCalled(); - expect(serviceNow.batchCreateComments).toHaveBeenCalledWith( - '123', - [ - { - comment: 'first comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', - commentId: '456', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: null, - updatedBy: null, - version: 'WzU3LDFd', - }, - ], - 'comments' - ); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - comments: [ - { - commentId: '456', - pushedDate: '2020-03-10T12:24:20.000Z', - }, - ], - }); - }); -}); - -describe('handleUpdateIncident', () => { - test('update an incident without comments', async () => { - const { serviceNow } = new ServiceNowMock(); - - const res = await handleUpdateIncident({ - incidentId: '123', - serviceNow, - params: { - ...params, - updatedAt: '2020-03-15T08:34:53.450Z', - updatedBy: { fullName: 'Another User', username: 'anotherUser' }, - }, - comments: [], - mapping: finalMapping, - }); - - expect(serviceNow.updateIncident).toHaveBeenCalled(); - expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { - short_description: 'a title (updated at 2020-03-15T08:34:53.450Z by Another User)', - description: 'a description (updated at 2020-03-15T08:34:53.450Z by Another User)', - }); - expect(serviceNow.updateIncident).toHaveReturned(); - expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }); - }); - - test('update an incident with comments', async () => { - const { serviceNow } = new ServiceNowMock(); - serviceNow.batchCreateComments.mockResolvedValue([ - { commentId: '456', pushedDate: '2020-03-10T12:24:20.000Z' }, - { commentId: '789', pushedDate: '2020-03-10T12:24:20.000Z' }, - ]); - - const res = await handleUpdateIncident({ - incidentId: '123', - serviceNow, - params: { - ...params, - updatedAt: '2020-03-15T08:34:53.450Z', - updatedBy: { fullName: 'Another User', username: 'anotherUser' }, - }, - comments: [ - { - comment: 'first comment', - commentId: '456', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: null, - updatedBy: null, - version: 'WzU3LDFd', - }, - { - comment: 'second comment', - commentId: '789', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: '2020-03-16T08:34:53.450Z', - updatedBy: { - fullName: 'Another User', - username: 'anotherUser', - }, - version: 'WzU3LDFd', - }, - ], - mapping: finalMapping, - }); - - expect(serviceNow.updateIncident).toHaveBeenCalled(); - expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { - description: 'a description (updated at 2020-03-15T08:34:53.450Z by Another User)', - short_description: 'a title (updated at 2020-03-15T08:34:53.450Z by Another User)', - }); - expect(serviceNow.updateIncident).toHaveReturned(); - expect(serviceNow.batchCreateComments).toHaveBeenCalled(); - expect(serviceNow.batchCreateComments).toHaveBeenCalledWith( - '123', - [ - { - comment: 'first comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', - commentId: '456', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: null, - updatedBy: null, - version: 'WzU3LDFd', - }, - { - comment: 'second comment (added at 2020-03-16T08:34:53.450Z by Another User)', - commentId: '789', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: '2020-03-16T08:34:53.450Z', - updatedBy: { - fullName: 'Another User', - username: 'anotherUser', - }, - version: 'WzU3LDFd', - }, - ], - 'comments' - ); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - comments: [ - { - commentId: '456', - pushedDate: '2020-03-10T12:24:20.000Z', - }, - { - commentId: '789', - pushedDate: '2020-03-10T12:24:20.000Z', - }, - ], - }); - }); -}); - -describe('handleUpdateIncident: different action types', () => { - test('overwrite & append', async () => { - const { serviceNow } = new ServiceNowMock(); - finalMapping.set('title', { - target: 'short_description', - actionType: 'overwrite', - }); - - finalMapping.set('description', { - target: 'description', - actionType: 'append', - }); - - finalMapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - finalMapping.set('short_description', { - target: 'title', - actionType: 'overwrite', - }); - - const res = await handleUpdateIncident({ - incidentId: '123', - serviceNow, - params, - comments: [], - mapping: finalMapping, - }); - - expect(serviceNow.updateIncident).toHaveBeenCalled(); - expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { - short_description: 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - description: - 'servicenow desc \r\na description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - }); - expect(serviceNow.updateIncident).toHaveReturned(); - expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }); - }); - - test('nothing & append', async () => { - const { serviceNow } = new ServiceNowMock(); - finalMapping.set('title', { - target: 'short_description', - actionType: 'nothing', - }); - - finalMapping.set('description', { - target: 'description', - actionType: 'append', - }); - - finalMapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - finalMapping.set('short_description', { - target: 'title', - actionType: 'nothing', - }); - - const res = await handleUpdateIncident({ - incidentId: '123', - serviceNow, - params, - comments: [], - mapping: finalMapping, - }); - - expect(serviceNow.updateIncident).toHaveBeenCalled(); - expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { - description: - 'servicenow desc \r\na description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - }); - expect(serviceNow.updateIncident).toHaveReturned(); - expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }); - }); - - test('append & append', async () => { - const { serviceNow } = new ServiceNowMock(); - finalMapping.set('title', { - target: 'short_description', - actionType: 'append', - }); - - finalMapping.set('description', { - target: 'description', - actionType: 'append', - }); - - finalMapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - finalMapping.set('short_description', { - target: 'title', - actionType: 'append', - }); - - const res = await handleUpdateIncident({ - incidentId: '123', - serviceNow, - params, - comments: [], - mapping: finalMapping, - }); - - expect(serviceNow.updateIncident).toHaveBeenCalled(); - expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { - short_description: - 'servicenow title \r\na title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - description: - 'servicenow desc \r\na description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - }); - expect(serviceNow.updateIncident).toHaveReturned(); - expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }); - }); - - test('nothing & nothing', async () => { - const { serviceNow } = new ServiceNowMock(); - finalMapping.set('title', { - target: 'short_description', - actionType: 'nothing', - }); - - finalMapping.set('description', { - target: 'description', - actionType: 'nothing', - }); - - finalMapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - finalMapping.set('short_description', { - target: 'title', - actionType: 'nothing', - }); - - const res = await handleUpdateIncident({ - incidentId: '123', - serviceNow, - params, - comments: [], - mapping: finalMapping, - }); - - expect(serviceNow.updateIncident).toHaveBeenCalled(); - expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', {}); - expect(serviceNow.updateIncident).toHaveReturned(); - expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }); - }); - - test('overwrite & nothing', async () => { - const { serviceNow } = new ServiceNowMock(); - finalMapping.set('title', { - target: 'short_description', - actionType: 'overwrite', - }); - - finalMapping.set('description', { - target: 'description', - actionType: 'nothing', - }); - - finalMapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - finalMapping.set('short_description', { - target: 'title', - actionType: 'overwrite', - }); - - const res = await handleUpdateIncident({ - incidentId: '123', - serviceNow, - params, - comments: [], - mapping: finalMapping, - }); - - expect(serviceNow.updateIncident).toHaveBeenCalled(); - expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { - short_description: 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - }); - expect(serviceNow.updateIncident).toHaveReturned(); - expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }); - }); - - test('overwrite & overwrite', async () => { - const { serviceNow } = new ServiceNowMock(); - finalMapping.set('title', { - target: 'short_description', - actionType: 'overwrite', - }); - - finalMapping.set('description', { - target: 'description', - actionType: 'overwrite', - }); - - finalMapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - finalMapping.set('short_description', { - target: 'title', - actionType: 'overwrite', - }); - - const res = await handleUpdateIncident({ - incidentId: '123', - serviceNow, - params, - comments: [], - mapping: finalMapping, - }); - - expect(serviceNow.updateIncident).toHaveBeenCalled(); - expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { - short_description: 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - description: 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - }); - expect(serviceNow.updateIncident).toHaveReturned(); - expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }); - }); - - test('nothing & overwrite', async () => { - const { serviceNow } = new ServiceNowMock(); - finalMapping.set('title', { - target: 'short_description', - actionType: 'nothing', - }); - - finalMapping.set('description', { - target: 'description', - actionType: 'overwrite', - }); - - finalMapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - finalMapping.set('short_description', { - target: 'title', - actionType: 'nothing', - }); - - const res = await handleUpdateIncident({ - incidentId: '123', - serviceNow, - params, - comments: [], - mapping: finalMapping, - }); - - expect(serviceNow.updateIncident).toHaveBeenCalled(); - expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { - description: 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - }); - expect(serviceNow.updateIncident).toHaveReturned(); - expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }); - }); - - test('append & overwrite', async () => { - const { serviceNow } = new ServiceNowMock(); - finalMapping.set('title', { - target: 'short_description', - actionType: 'append', - }); - - finalMapping.set('description', { - target: 'description', - actionType: 'overwrite', - }); - - finalMapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - finalMapping.set('short_description', { - target: 'title', - actionType: 'append', - }); - - const res = await handleUpdateIncident({ - incidentId: '123', - serviceNow, - params, - comments: [], - mapping: finalMapping, - }); - - expect(serviceNow.updateIncident).toHaveBeenCalled(); - expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { - short_description: - 'servicenow title \r\na title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - description: 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - }); - expect(serviceNow.updateIncident).toHaveReturned(); - expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }); - }); - - test('append & nothing', async () => { - const { serviceNow } = new ServiceNowMock(); - finalMapping.set('title', { - target: 'short_description', - actionType: 'append', - }); - - finalMapping.set('description', { - target: 'description', - actionType: 'nothing', - }); - - finalMapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - finalMapping.set('short_description', { - target: 'title', - actionType: 'append', - }); - - const res = await handleUpdateIncident({ - incidentId: '123', - serviceNow, - params, - comments: [], - mapping: finalMapping, - }); - - expect(serviceNow.updateIncident).toHaveBeenCalled(); - expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { - short_description: - 'servicenow title \r\na title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - }); - expect(serviceNow.updateIncident).toHaveReturned(); - expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }); - }); -}); - -describe('createComments', () => { - test('create comments correctly', async () => { - const { serviceNow } = new ServiceNowMock(); - serviceNow.batchCreateComments.mockResolvedValue([ - { commentId: '456', pushedDate: '2020-03-10T12:24:20.000Z' }, - { commentId: '789', pushedDate: '2020-03-10T12:24:20.000Z' }, - ]); - - const comments = [ - { - comment: 'first comment', - commentId: '456', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: null, - updatedBy: null, - version: 'WzU3LDFd', - }, - { - comment: 'second comment', - commentId: '789', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: '2020-03-13T08:34:53.450Z', - updatedBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - version: 'WzU3LDFd', - }, - ]; - - const res = await createComments(serviceNow, '123', 'comments', comments); - - expect(serviceNow.batchCreateComments).toHaveBeenCalled(); - expect(serviceNow.batchCreateComments).toHaveBeenCalledWith( - '123', - [ - { - comment: 'first comment', - commentId: '456', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: null, - updatedBy: null, - version: 'WzU3LDFd', - }, - { - comment: 'second comment', - commentId: '789', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: '2020-03-13T08:34:53.450Z', - updatedBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - version: 'WzU3LDFd', - }, - ], - 'comments' - ); - expect(res).toEqual([ - { - commentId: '456', - pushedDate: '2020-03-10T12:24:20.000Z', - }, - { - commentId: '789', - pushedDate: '2020-03-10T12:24:20.000Z', - }, - ]); - }); -}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts deleted file mode 100644 index 9166f53cf757e4..00000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts +++ /dev/null @@ -1,129 +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 { zipWith } from 'lodash'; -import { CommentResponse } from './lib/types'; -import { - HandlerResponse, - Comment, - SimpleComment, - CreateHandlerArguments, - UpdateHandlerArguments, - IncidentHandlerArguments, -} from './types'; -import { ServiceNow } from './lib'; -import { transformFields, prepareFieldsForTransformation, transformComments } from './helpers'; - -export const createComments = async ( - serviceNow: ServiceNow, - incidentId: string, - key: string, - comments: Comment[] -): Promise => { - const createdComments = await serviceNow.batchCreateComments(incidentId, comments, key); - - return zipWith(comments, createdComments, (a: Comment, b: CommentResponse) => ({ - commentId: a.commentId, - pushedDate: b.pushedDate, - })); -}; - -export const handleCreateIncident = async ({ - serviceNow, - params, - comments, - mapping, -}: CreateHandlerArguments): Promise => { - const fields = prepareFieldsForTransformation({ - params, - mapping, - }); - - const incident = transformFields({ - params, - fields, - }); - - const createdIncident = await serviceNow.createIncident({ - ...incident, - }); - - const res: HandlerResponse = { ...createdIncident }; - - if ( - comments && - Array.isArray(comments) && - comments.length > 0 && - mapping.get('comments')?.actionType !== 'nothing' - ) { - comments = transformComments(comments, params, ['informationAdded']); - res.comments = [ - ...(await createComments( - serviceNow, - res.incidentId, - mapping.get('comments')!.target, - comments - )), - ]; - } - - return { ...res }; -}; - -export const handleUpdateIncident = async ({ - incidentId, - serviceNow, - params, - comments, - mapping, -}: UpdateHandlerArguments): Promise => { - const currentIncident = await serviceNow.getIncident(incidentId); - const fields = prepareFieldsForTransformation({ - params, - mapping, - defaultPipes: ['informationUpdated'], - }); - - const incident = transformFields({ - params, - fields, - currentIncident, - }); - - const updatedIncident = await serviceNow.updateIncident(incidentId, { - ...incident, - }); - - const res: HandlerResponse = { ...updatedIncident }; - - if ( - comments && - Array.isArray(comments) && - comments.length > 0 && - mapping.get('comments')?.actionType !== 'nothing' - ) { - comments = transformComments(comments, params, ['informationAdded']); - res.comments = [ - ...(await createComments(serviceNow, incidentId, mapping.get('comments')!.target, comments)), - ]; - } - - return { ...res }; -}; - -export const handleIncident = async ({ - incidentId, - serviceNow, - params, - comments, - mapping, -}: IncidentHandlerArguments): Promise => { - if (!incidentId) { - return await handleCreateIncident({ serviceNow, params, comments, mapping }); - } else { - return await handleUpdateIncident({ incidentId, serviceNow, params, comments, mapping }); - } -}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/constants.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/constants.ts deleted file mode 100644 index a0ffd859e14caa..00000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/constants.ts +++ /dev/null @@ -1,8 +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 const ACTION_TYPE_ID = '.servicenow'; -export const SUPPORTED_SOURCE_FIELDS = ['title', 'comments', 'description']; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts deleted file mode 100644 index cbcefe6364e8f2..00000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts +++ /dev/null @@ -1,409 +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 { - normalizeMapping, - buildMap, - mapParams, - appendField, - appendInformationToField, - prepareFieldsForTransformation, - transformFields, - transformComments, -} from './helpers'; -import { mapping, finalMapping } from './mock'; -import { SUPPORTED_SOURCE_FIELDS } from './constants'; -import { MapEntry, Params, Comment } from './types'; - -const maliciousMapping: MapEntry[] = [ - { source: '__proto__', target: 'short_description', actionType: 'nothing' }, - { source: 'description', target: '__proto__', actionType: 'nothing' }, - { source: 'comments', target: 'comments', actionType: 'nothing' }, - { source: 'unsupportedSource', target: 'comments', actionType: 'nothing' }, -]; - -const fullParams: Params = { - caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', - title: 'a title', - description: 'a description', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - incidentId: null, - incident: { - short_description: 'a title', - description: 'a description', - }, - comments: [ - { - commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', - comment: 'first comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - }, - { - commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', - comment: 'second comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - }, - ], -}; - -describe('sanitizeMapping', () => { - test('remove malicious fields', () => { - const sanitizedMapping = normalizeMapping(SUPPORTED_SOURCE_FIELDS, maliciousMapping); - expect(sanitizedMapping.every(m => m.source !== '__proto__' && m.target !== '__proto__')).toBe( - true - ); - }); - - test('remove unsuppported source fields', () => { - const normalizedMapping = normalizeMapping(SUPPORTED_SOURCE_FIELDS, maliciousMapping); - expect(normalizedMapping).not.toEqual( - expect.arrayContaining([ - expect.objectContaining({ - source: 'unsupportedSource', - target: 'comments', - actionType: 'nothing', - }), - ]) - ); - }); -}); - -describe('buildMap', () => { - test('builds sanitized Map', () => { - const finalMap = buildMap(maliciousMapping); - expect(finalMap.get('__proto__')).not.toBeDefined(); - }); - - test('builds Map correct', () => { - const final = buildMap(mapping); - expect(final).toEqual(finalMapping); - }); -}); - -describe('mapParams', () => { - test('maps params correctly', () => { - const params = { - caseId: '123', - incidentId: '456', - title: 'Incident title', - description: 'Incident description', - }; - - const fields = mapParams(params, finalMapping); - - expect(fields).toEqual({ - short_description: 'Incident title', - description: 'Incident description', - }); - }); - - test('do not add fields not in mapping', () => { - const params = { - caseId: '123', - incidentId: '456', - title: 'Incident title', - description: 'Incident description', - }; - const fields = mapParams(params, finalMapping); - - const { title, description, ...unexpectedFields } = params; - - expect(fields).not.toEqual(expect.objectContaining(unexpectedFields)); - }); -}); - -describe('prepareFieldsForTransformation', () => { - test('prepare fields with defaults', () => { - const res = prepareFieldsForTransformation({ - params: fullParams, - mapping: finalMapping, - }); - expect(res).toEqual([ - { - key: 'short_description', - value: 'a title', - actionType: 'overwrite', - pipes: ['informationCreated'], - }, - { - key: 'description', - value: 'a description', - actionType: 'append', - pipes: ['informationCreated', 'append'], - }, - ]); - }); - - test('prepare fields with default pipes', () => { - const res = prepareFieldsForTransformation({ - params: fullParams, - mapping: finalMapping, - defaultPipes: ['myTestPipe'], - }); - expect(res).toEqual([ - { - key: 'short_description', - value: 'a title', - actionType: 'overwrite', - pipes: ['myTestPipe'], - }, - { - key: 'description', - value: 'a description', - actionType: 'append', - pipes: ['myTestPipe', 'append'], - }, - ]); - }); -}); - -describe('transformFields', () => { - test('transform fields for creation correctly', () => { - const fields = prepareFieldsForTransformation({ - params: fullParams, - mapping: finalMapping, - }); - - const res = transformFields({ - params: fullParams, - fields, - }); - - expect(res).toEqual({ - short_description: 'a title (created at 2020-03-13T08:34:53.450Z by Elastic User)', - description: 'a description (created at 2020-03-13T08:34:53.450Z by Elastic User)', - }); - }); - - test('transform fields for update correctly', () => { - const fields = prepareFieldsForTransformation({ - params: { - ...fullParams, - updatedAt: '2020-03-15T08:34:53.450Z', - updatedBy: { username: 'anotherUser', fullName: 'Another User' }, - }, - mapping: finalMapping, - defaultPipes: ['informationUpdated'], - }); - - const res = transformFields({ - params: { - ...fullParams, - updatedAt: '2020-03-15T08:34:53.450Z', - updatedBy: { username: 'anotherUser', fullName: 'Another User' }, - }, - fields, - currentIncident: { - short_description: 'first title (created at 2020-03-13T08:34:53.450Z by Elastic User)', - description: 'first description (created at 2020-03-13T08:34:53.450Z by Elastic User)', - }, - }); - expect(res).toEqual({ - short_description: 'a title (updated at 2020-03-15T08:34:53.450Z by Another User)', - description: - 'first description (created at 2020-03-13T08:34:53.450Z by Elastic User) \r\na description (updated at 2020-03-15T08:34:53.450Z by Another User)', - }); - }); - - test('add newline character to descripton', () => { - const fields = prepareFieldsForTransformation({ - params: fullParams, - mapping: finalMapping, - defaultPipes: ['informationUpdated'], - }); - - const res = transformFields({ - params: fullParams, - fields, - currentIncident: { - short_description: 'first title', - description: 'first description', - }, - }); - expect(res.description?.includes('\r\n')).toBe(true); - }); - - test('append username if fullname is undefined when create', () => { - const fields = prepareFieldsForTransformation({ - params: fullParams, - mapping: finalMapping, - }); - - const res = transformFields({ - params: { ...fullParams, createdBy: { fullName: null, username: 'elastic' } }, - fields, - }); - - expect(res).toEqual({ - short_description: 'a title (created at 2020-03-13T08:34:53.450Z by elastic)', - description: 'a description (created at 2020-03-13T08:34:53.450Z by elastic)', - }); - }); - - test('append username if fullname is undefined when update', () => { - const fields = prepareFieldsForTransformation({ - params: { - ...fullParams, - updatedAt: '2020-03-15T08:34:53.450Z', - updatedBy: { username: 'anotherUser', fullName: 'Another User' }, - }, - mapping: finalMapping, - defaultPipes: ['informationUpdated'], - }); - - const res = transformFields({ - params: { - ...fullParams, - updatedAt: '2020-03-15T08:34:53.450Z', - updatedBy: { username: 'anotherUser', fullName: null }, - }, - fields, - }); - - expect(res).toEqual({ - short_description: 'a title (updated at 2020-03-15T08:34:53.450Z by anotherUser)', - description: 'a description (updated at 2020-03-15T08:34:53.450Z by anotherUser)', - }); - }); -}); - -describe('appendField', () => { - test('prefix correctly', () => { - expect('my_prefixmy_value ').toEqual(appendField({ value: 'my_value', prefix: 'my_prefix' })); - }); - - test('suffix correctly', () => { - expect('my_value my_suffix').toEqual(appendField({ value: 'my_value', suffix: 'my_suffix' })); - }); - - test('prefix and suffix correctly', () => { - expect('my_prefixmy_value my_suffix').toEqual( - appendField({ value: 'my_value', prefix: 'my_prefix', suffix: 'my_suffix' }) - ); - }); -}); - -describe('appendInformationToField', () => { - test('creation mode', () => { - const res = appendInformationToField({ - value: 'my value', - user: 'Elastic Test User', - date: '2020-03-13T08:34:53.450Z', - mode: 'create', - }); - expect(res).toEqual('my value (created at 2020-03-13T08:34:53.450Z by Elastic Test User)'); - }); - - test('update mode', () => { - const res = appendInformationToField({ - value: 'my value', - user: 'Elastic Test User', - date: '2020-03-13T08:34:53.450Z', - mode: 'update', - }); - expect(res).toEqual('my value (updated at 2020-03-13T08:34:53.450Z by Elastic Test User)'); - }); - - test('add mode', () => { - const res = appendInformationToField({ - value: 'my value', - user: 'Elastic Test User', - date: '2020-03-13T08:34:53.450Z', - mode: 'add', - }); - expect(res).toEqual('my value (added at 2020-03-13T08:34:53.450Z by Elastic Test User)'); - }); -}); - -describe('transformComments', () => { - test('transform creation comments', () => { - const comments: Comment[] = [ - { - commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', - comment: 'first comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - }, - ]; - const res = transformComments(comments, fullParams, ['informationCreated']); - expect(res).toEqual([ - { - commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', - comment: 'first comment (created at 2020-03-13T08:34:53.450Z by Elastic User)', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - }, - ]); - }); - - test('transform update comments', () => { - const comments: Comment[] = [ - { - commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', - comment: 'first comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: '2020-03-15T08:34:53.450Z', - updatedBy: { fullName: 'Another User', username: 'anotherUser' }, - }, - ]; - const res = transformComments(comments, fullParams, ['informationUpdated']); - expect(res).toEqual([ - { - commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', - comment: 'first comment (updated at 2020-03-15T08:34:53.450Z by Another User)', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: '2020-03-15T08:34:53.450Z', - updatedBy: { fullName: 'Another User', username: 'anotherUser' }, - }, - ]); - }); - test('transform added comments', () => { - const comments: Comment[] = [ - { - commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', - comment: 'first comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - }, - ]; - const res = transformComments(comments, fullParams, ['informationAdded']); - expect(res).toEqual([ - { - commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', - comment: 'first comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - }, - ]); - }); -}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts deleted file mode 100644 index 0a26996ea8d69a..00000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts +++ /dev/null @@ -1,125 +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 { flow } from 'lodash'; - -import { SUPPORTED_SOURCE_FIELDS } from './constants'; -import { - MapEntry, - Mapping, - AppendFieldArgs, - AppendInformationFieldArgs, - Params, - Comment, - TransformFieldsArgs, - PipedField, - PrepareFieldsForTransformArgs, - KeyAny, -} from './types'; -import { Incident } from './lib/types'; - -import * as transformers from './transformers'; -import * as i18n from './translations'; - -export const normalizeMapping = (supportedFields: string[], mapping: MapEntry[]): MapEntry[] => { - // Prevent prototype pollution and remove unsupported fields - return mapping.filter( - m => m.source !== '__proto__' && m.target !== '__proto__' && supportedFields.includes(m.source) - ); -}; - -export const buildMap = (mapping: MapEntry[]): Mapping => { - return normalizeMapping(SUPPORTED_SOURCE_FIELDS, mapping).reduce((fieldsMap, field) => { - const { source, target, actionType } = field; - fieldsMap.set(source, { target, actionType }); - fieldsMap.set(target, { target: source, actionType }); - return fieldsMap; - }, new Map()); -}; - -export const mapParams = (params: Record, mapping: Mapping) => { - return Object.keys(params).reduce((prev: KeyAny, curr: string): KeyAny => { - const field = mapping.get(curr); - if (field) { - prev[field.target] = params[curr]; - } - return prev; - }, {}); -}; - -export const appendField = ({ value, prefix = '', suffix = '' }: AppendFieldArgs): string => { - return `${prefix}${value} ${suffix}`; -}; - -const t = { ...transformers } as { [index: string]: Function }; // TODO: Find a better solution exists. - -export const prepareFieldsForTransformation = ({ - params, - mapping, - defaultPipes = ['informationCreated'], -}: PrepareFieldsForTransformArgs): PipedField[] => { - return Object.keys(params.incident) - .filter(p => mapping.get(p)!.actionType !== 'nothing') - .map(p => ({ - key: p, - value: params.incident[p] as string, - actionType: mapping.get(p)!.actionType, - pipes: [...defaultPipes], - })) - .map(p => ({ - ...p, - pipes: p.actionType === 'append' ? [...p.pipes, 'append'] : p.pipes, - })); -}; - -export const transformFields = ({ - params, - fields, - currentIncident, -}: TransformFieldsArgs): Incident => { - return fields.reduce((prev: Incident, cur) => { - const transform = flow(...cur.pipes.map(p => t[p])); - prev[cur.key] = transform({ - value: cur.value, - date: params.updatedAt ?? params.createdAt, - user: - params.updatedBy != null - ? params.updatedBy.fullName ?? params.updatedBy.username - : params.createdBy.fullName ?? params.createdBy.username, - previousValue: currentIncident ? currentIncident[cur.key] : '', - }).value; - return prev; - }, {} as Incident); -}; - -export const appendInformationToField = ({ - value, - user, - date, - mode = 'create', -}: AppendInformationFieldArgs): string => { - return appendField({ - value, - suffix: i18n.FIELD_INFORMATION(mode, date, user), - }); -}; - -export const transformComments = ( - comments: Comment[], - params: Params, - pipes: string[] -): Comment[] => { - return comments.map(c => ({ - ...c, - comment: flow(...pipes.map(p => t[p]))({ - value: c.comment, - date: c.updatedAt ?? c.createdAt, - user: - c.updatedBy != null - ? c.updatedBy.fullName ?? c.updatedBy.username - : c.createdBy.fullName ?? c.createdBy.username, - }).value, - })); -}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts deleted file mode 100644 index 5066190d4fe561..00000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts +++ /dev/null @@ -1,111 +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 { curry, isEmpty } from 'lodash'; -import { schema } from '@kbn/config-schema'; -import { - ActionType, - ActionTypeExecutorOptions, - ActionTypeExecutorResult, - ExecutorType, -} from '../../types'; -import { ActionsConfigurationUtilities } from '../../actions_config'; -import { ServiceNow } from './lib'; - -import * as i18n from './translations'; - -import { ACTION_TYPE_ID } from './constants'; -import { ConfigType, SecretsType, Comment, ExecutorParams } from './types'; - -import { ConfigSchemaProps, SecretsSchemaProps, ParamsSchema } from './schema'; - -import { buildMap, mapParams } from './helpers'; -import { handleIncident } from './action_handlers'; - -function validateConfig( - configurationUtilities: ActionsConfigurationUtilities, - configObject: ConfigType -) { - try { - if (isEmpty(configObject.casesConfiguration.mapping)) { - return i18n.MAPPING_EMPTY; - } - - configurationUtilities.ensureWhitelistedUri(configObject.apiUrl); - } catch (whitelistError) { - return i18n.WHITE_LISTED_ERROR(whitelistError.message); - } -} - -function validateSecrets( - configurationUtilities: ActionsConfigurationUtilities, - secrets: SecretsType -) {} - -// action type definition -export function getActionType({ - configurationUtilities, - executor = serviceNowExecutor, -}: { - configurationUtilities: ActionsConfigurationUtilities; - executor?: ExecutorType; -}): ActionType { - return { - id: ACTION_TYPE_ID, - name: i18n.NAME, - minimumLicenseRequired: 'platinum', - validate: { - config: schema.object(ConfigSchemaProps, { - validate: curry(validateConfig)(configurationUtilities), - }), - secrets: schema.object(SecretsSchemaProps, { - validate: curry(validateSecrets)(configurationUtilities), - }), - params: ParamsSchema, - }, - executor, - }; -} - -// action executor - -async function serviceNowExecutor( - execOptions: ActionTypeExecutorOptions -): Promise { - const actionId = execOptions.actionId; - const { - apiUrl, - casesConfiguration: { mapping: configurationMapping }, - } = execOptions.config as ConfigType; - const { username, password } = execOptions.secrets as SecretsType; - const params = execOptions.params as ExecutorParams; - const { comments, incidentId, ...restParams } = params; - - const mapping = buildMap(configurationMapping); - const incident = mapParams((restParams as unknown) as Record, mapping); - const serviceNow = new ServiceNow({ url: apiUrl, username, password }); - - const handlerInput = { - incidentId, - serviceNow, - params: { ...params, incident }, - comments: comments as Comment[], - mapping, - }; - - const res: Pick & - Pick = { - status: 'ok', - actionId, - }; - - const data = await handleIncident(handlerInput); - - return { - ...res, - data, - }; -} diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/constants.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/constants.ts deleted file mode 100644 index 3f102ae19f437c..00000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/constants.ts +++ /dev/null @@ -1,13 +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 const API_VERSION = 'v2'; -export const INCIDENT_URL = `api/now/${API_VERSION}/table/incident`; -export const USER_URL = `api/now/${API_VERSION}/table/sys_user?user_name=`; -export const COMMENT_URL = `api/now/${API_VERSION}/table/incident`; - -// Based on: https://docs.servicenow.com/bundle/orlando-platform-user-interface/page/use/navigation/reference/r_NavigatingByURLExamples.html -export const VIEW_INCIDENT_URL = `nav_to.do?uri=incident.do?sys_id=`; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts deleted file mode 100644 index 40eeb0f920f82c..00000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts +++ /dev/null @@ -1,334 +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 axios from 'axios'; -import { ServiceNow } from '.'; -import { instance, params } from '../mock'; - -jest.mock('axios'); - -axios.create = jest.fn(() => axios); -const axiosMock = (axios as unknown) as jest.Mock; - -let serviceNow: ServiceNow; - -const testMissingConfiguration = (field: string) => { - expect.assertions(1); - try { - new ServiceNow({ ...instance, [field]: '' }); - } catch (error) { - expect(error.message).toEqual('[Action][ServiceNow]: Wrong configuration.'); - } -}; - -const prependInstanceUrl = (url: string): string => `${instance.url}/${url}`; - -describe('ServiceNow lib', () => { - beforeEach(() => { - serviceNow = new ServiceNow(instance); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - test('should thrown an error if url is missing', () => { - testMissingConfiguration('url'); - }); - - test('should thrown an error if username is missing', () => { - testMissingConfiguration('username'); - }); - - test('should thrown an error if password is missing', () => { - testMissingConfiguration('password'); - }); - - test('get user id', async () => { - axiosMock.mockResolvedValue({ - status: 200, - headers: { - 'content-type': 'application/json', - }, - data: { result: [{ sys_id: '123' }] }, - }); - - const res = await serviceNow.getUserID(); - const [url, { method }] = axiosMock.mock.calls[0]; - - expect(url).toEqual(prependInstanceUrl('api/now/v2/table/sys_user?user_name=username')); - expect(method).toEqual('get'); - expect(res).toEqual('123'); - }); - - test('create incident', async () => { - axiosMock.mockResolvedValue({ - status: 200, - headers: { - 'content-type': 'application/json', - }, - data: { result: { sys_id: '123', number: 'INC01', sys_created_on: '2020-03-10 12:24:20' } }, - }); - - const res = await serviceNow.createIncident({ - short_description: 'A title', - description: 'A description', - caller_id: '123', - }); - const [url, { method, data }] = axiosMock.mock.calls[0]; - - expect(url).toEqual(prependInstanceUrl('api/now/v2/table/incident')); - expect(method).toEqual('post'); - expect(data).toEqual({ - short_description: 'A title', - description: 'A description', - caller_id: '123', - }); - - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }); - }); - - test('update incident', async () => { - axiosMock.mockResolvedValue({ - status: 200, - headers: { - 'content-type': 'application/json', - }, - data: { result: { sys_id: '123', number: 'INC01', sys_updated_on: '2020-03-10 12:24:20' } }, - }); - - const res = await serviceNow.updateIncident('123', { - short_description: params.title, - }); - const [url, { method, data }] = axiosMock.mock.calls[0]; - - expect(url).toEqual(prependInstanceUrl(`api/now/v2/table/incident/123`)); - expect(method).toEqual('patch'); - expect(data).toEqual({ short_description: params.title }); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }); - }); - - test('create comment', async () => { - axiosMock.mockResolvedValue({ - status: 200, - headers: { - 'content-type': 'application/json', - }, - data: { result: { sys_updated_on: '2020-03-10 12:24:20' } }, - }); - - const comment = { - commentId: '456', - version: 'WzU3LDFd', - comment: 'A comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - }; - - const res = await serviceNow.createComment('123', comment, 'comments'); - - const [url, { method, data }] = axiosMock.mock.calls[0]; - - expect(url).toEqual(prependInstanceUrl(`api/now/v2/table/incident/123`)); - expect(method).toEqual('patch'); - expect(data).toEqual({ - comments: 'A comment', - }); - - expect(res).toEqual({ - commentId: '456', - pushedDate: '2020-03-10T12:24:20.000Z', - }); - }); - - test('create batch comment', async () => { - axiosMock.mockReturnValueOnce({ - status: 200, - headers: { - 'content-type': 'application/json', - }, - data: { result: { sys_updated_on: '2020-03-10 12:24:20' } }, - }); - - axiosMock.mockReturnValueOnce({ - status: 200, - headers: { - 'content-type': 'application/json', - }, - data: { result: { sys_updated_on: '2020-03-10 12:25:20' } }, - }); - - const comments = [ - { - commentId: '123', - version: 'WzU3LDFd', - comment: 'A comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - }, - { - commentId: '456', - version: 'WzU3LDFd', - comment: 'A second comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - }, - ]; - const res = await serviceNow.batchCreateComments('000', comments, 'comments'); - - comments.forEach((comment, index) => { - const [url, { method, data }] = axiosMock.mock.calls[index]; - expect(url).toEqual(prependInstanceUrl('api/now/v2/table/incident/000')); - expect(method).toEqual('patch'); - expect(data).toEqual({ - comments: comment.comment, - }); - expect(res).toEqual([ - { commentId: '123', pushedDate: '2020-03-10T12:24:20.000Z' }, - { commentId: '456', pushedDate: '2020-03-10T12:25:20.000Z' }, - ]); - }); - }); - - test('throw if not status is not ok', async () => { - expect.assertions(1); - - axiosMock.mockResolvedValue({ - status: 401, - headers: { - 'content-type': 'application/json', - }, - }); - try { - await serviceNow.getUserID(); - } catch (error) { - expect(error.message).toEqual( - '[Action][ServiceNow]: Unable to get user id. Error: [ServiceNow]: Instance is not alive.' - ); - } - }); - - test('throw if not content-type is not application/json', async () => { - expect.assertions(1); - - axiosMock.mockResolvedValue({ - status: 200, - headers: { - 'content-type': 'application/html', - }, - }); - try { - await serviceNow.getUserID(); - } catch (error) { - expect(error.message).toEqual( - '[Action][ServiceNow]: Unable to get user id. Error: [ServiceNow]: Instance is not alive.' - ); - } - }); - - test('check error when getting user', async () => { - expect.assertions(1); - - axiosMock.mockImplementationOnce(() => { - throw new Error('Bad request.'); - }); - try { - await serviceNow.getUserID(); - } catch (error) { - expect(error.message).toEqual( - '[Action][ServiceNow]: Unable to get user id. Error: Bad request.' - ); - } - }); - - test('check error when getting incident', async () => { - expect.assertions(1); - - axiosMock.mockImplementationOnce(() => { - throw new Error('Bad request.'); - }); - try { - await serviceNow.getIncident('123'); - } catch (error) { - expect(error.message).toEqual( - '[Action][ServiceNow]: Unable to get incident with id 123. Error: Bad request.' - ); - } - }); - - test('check error when creating incident', async () => { - expect.assertions(1); - - axiosMock.mockImplementationOnce(() => { - throw new Error('Bad request.'); - }); - try { - await serviceNow.createIncident({ short_description: 'title' }); - } catch (error) { - expect(error.message).toEqual( - '[Action][ServiceNow]: Unable to create incident. Error: Bad request.' - ); - } - }); - - test('check error when updating incident', async () => { - expect.assertions(1); - - axiosMock.mockImplementationOnce(() => { - throw new Error('Bad request.'); - }); - try { - await serviceNow.updateIncident('123', { short_description: 'title' }); - } catch (error) { - expect(error.message).toEqual( - '[Action][ServiceNow]: Unable to update incident with id 123. Error: Bad request.' - ); - } - }); - - test('check error when creating comment', async () => { - expect.assertions(1); - - axiosMock.mockImplementationOnce(() => { - throw new Error('Bad request.'); - }); - try { - await serviceNow.createComment( - '123', - { - commentId: '456', - version: 'WzU3LDFd', - comment: 'A second comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - }, - 'comment' - ); - } catch (error) { - expect(error.message).toEqual( - '[Action][ServiceNow]: Unable to create comment at incident with id 123. Error: Bad request.' - ); - } - }); -}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts deleted file mode 100644 index ed9cfe67a19a15..00000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts +++ /dev/null @@ -1,186 +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 axios, { AxiosInstance, Method, AxiosResponse } from 'axios'; - -import { INCIDENT_URL, USER_URL, COMMENT_URL, VIEW_INCIDENT_URL } from './constants'; -import { Instance, Incident, IncidentResponse, UpdateIncident, CommentResponse } from './types'; -import { Comment } from '../types'; - -const validStatusCodes = [200, 201]; - -class ServiceNow { - private readonly incidentUrl: string; - private readonly commentUrl: string; - private readonly userUrl: string; - private readonly axios: AxiosInstance; - - constructor(private readonly instance: Instance) { - if ( - !this.instance || - !this.instance.url || - !this.instance.username || - !this.instance.password - ) { - throw Error('[Action][ServiceNow]: Wrong configuration.'); - } - - this.incidentUrl = `${this.instance.url}/${INCIDENT_URL}`; - this.commentUrl = `${this.instance.url}/${COMMENT_URL}`; - this.userUrl = `${this.instance.url}/${USER_URL}`; - this.axios = axios.create({ - auth: { username: this.instance.username, password: this.instance.password }, - }); - } - - private _throwIfNotAlive(status: number, contentType: string) { - if (!validStatusCodes.includes(status) || !contentType.includes('application/json')) { - throw new Error('[ServiceNow]: Instance is not alive.'); - } - } - - private async _request({ - url, - method = 'get', - data = {}, - }: { - url: string; - method?: Method; - data?: unknown; - }): Promise { - const res = await this.axios(url, { method, data }); - this._throwIfNotAlive(res.status, res.headers['content-type']); - return res; - } - - private _patch({ url, data }: { url: string; data: unknown }): Promise { - return this._request({ - url, - method: 'patch', - data, - }); - } - - private _addTimeZoneToDate(date: string, timezone = 'GMT'): string { - return `${date} GMT`; - } - - private _getErrorMessage(msg: string) { - return `[Action][ServiceNow]: ${msg}`; - } - - private _getIncidentViewURL(id: string) { - return `${this.instance.url}/${VIEW_INCIDENT_URL}${id}`; - } - - async getUserID(): Promise { - try { - const res = await this._request({ url: `${this.userUrl}${this.instance.username}` }); - return res.data.result[0].sys_id; - } catch (error) { - throw new Error(this._getErrorMessage(`Unable to get user id. Error: ${error.message}`)); - } - } - - async getIncident(incidentId: string) { - try { - const res = await this._request({ - url: `${this.incidentUrl}/${incidentId}`, - }); - - return { ...res.data.result }; - } catch (error) { - throw new Error( - this._getErrorMessage( - `Unable to get incident with id ${incidentId}. Error: ${error.message}` - ) - ); - } - } - - async createIncident(incident: Incident): Promise { - try { - const res = await this._request({ - url: `${this.incidentUrl}`, - method: 'post', - data: { ...incident }, - }); - - return { - number: res.data.result.number, - incidentId: res.data.result.sys_id, - pushedDate: new Date(this._addTimeZoneToDate(res.data.result.sys_created_on)).toISOString(), - url: this._getIncidentViewURL(res.data.result.sys_id), - }; - } catch (error) { - throw new Error(this._getErrorMessage(`Unable to create incident. Error: ${error.message}`)); - } - } - - async updateIncident(incidentId: string, incident: UpdateIncident): Promise { - try { - const res = await this._patch({ - url: `${this.incidentUrl}/${incidentId}`, - data: { ...incident }, - }); - - return { - number: res.data.result.number, - incidentId: res.data.result.sys_id, - pushedDate: new Date(this._addTimeZoneToDate(res.data.result.sys_updated_on)).toISOString(), - url: this._getIncidentViewURL(res.data.result.sys_id), - }; - } catch (error) { - throw new Error( - this._getErrorMessage( - `Unable to update incident with id ${incidentId}. Error: ${error.message}` - ) - ); - } - } - - async batchCreateComments( - incidentId: string, - comments: Comment[], - field: string - ): Promise { - // Create comments sequentially. - const promises = comments.reduce(async (prevPromise, currentComment) => { - const totalComments = await prevPromise; - const res = await this.createComment(incidentId, currentComment, field); - return [...totalComments, res]; - }, Promise.resolve([] as CommentResponse[])); - - const res = await promises; - return res; - } - - async createComment( - incidentId: string, - comment: Comment, - field: string - ): Promise { - try { - const res = await this._patch({ - url: `${this.commentUrl}/${incidentId}`, - data: { [field]: comment.comment }, - }); - - return { - commentId: comment.commentId, - pushedDate: new Date(this._addTimeZoneToDate(res.data.result.sys_updated_on)).toISOString(), - }; - } catch (error) { - throw new Error( - this._getErrorMessage( - `Unable to create comment at incident with id ${incidentId}. Error: ${error.message}` - ) - ); - } - } -} - -export { ServiceNow }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/types.ts deleted file mode 100644 index a65e417dbc486e..00000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/types.ts +++ /dev/null @@ -1,32 +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 interface Instance { - url: string; - username: string; - password: string; -} - -export interface Incident { - short_description: string; - description?: string; - caller_id?: string; - [index: string]: string | undefined; -} - -export interface IncidentResponse { - number: string; - incidentId: string; - pushedDate: string; - url: string; -} - -export interface CommentResponse { - commentId: string; - pushedDate: string; -} - -export type UpdateIncident = Partial; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts deleted file mode 100644 index 06c006fb378254..00000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts +++ /dev/null @@ -1,115 +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 { MapEntry, Mapping, ExecutorParams } from './types'; -import { Incident } from './lib/types'; - -const mapping: MapEntry[] = [ - { source: 'title', target: 'short_description', actionType: 'overwrite' }, - { source: 'description', target: 'description', actionType: 'append' }, - { source: 'comments', target: 'comments', actionType: 'append' }, -]; - -const finalMapping: Mapping = new Map(); - -finalMapping.set('title', { - target: 'short_description', - actionType: 'overwrite', -}); - -finalMapping.set('description', { - target: 'description', - actionType: 'append', -}); - -finalMapping.set('comments', { - target: 'comments', - actionType: 'append', -}); - -finalMapping.set('short_description', { - target: 'title', - actionType: 'overwrite', -}); - -const params: ExecutorParams = { - caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', - incidentId: 'ceb5986e079f00100e48fbbf7c1ed06d', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: '2020-03-13T08:34:53.450Z', - updatedBy: { fullName: 'Elastic User', username: 'elastic' }, - title: 'Incident title', - description: 'Incident description', - comments: [ - { - commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', - comment: 'A comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: '2020-03-13T08:34:53.450Z', - updatedBy: { fullName: 'Elastic User', username: 'elastic' }, - }, - { - commentId: 'e3db587f-ca27-4ae9-ad2e-31f2dcc9bd0d', - version: 'WlK3LDFd', - comment: 'Another comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: '2020-03-13T08:34:53.450Z', - updatedBy: { fullName: 'Elastic User', username: 'elastic' }, - }, - ], -}; - -const incidentResponse = { - incidentId: 'c816f79cc0a8016401c5a33be04be441', - number: 'INC0010001', - pushedDate: '2020-03-13T08:34:53.450Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', -}; - -const userId = '2e9a0a5e2f79001016ab51172799b670'; - -const axiosResponse = { - status: 200, - headers: { - 'content-type': 'application/json', - }, -}; -const userIdResponse = { - result: [{ sys_id: userId }], -}; - -const incidentAxiosResponse = { - result: { sys_id: incidentResponse.incidentId, number: incidentResponse.number }, -}; - -const instance = { - url: 'https://instance.service-now.com', - username: 'username', - password: 'password', -}; - -const incident: Incident = { - short_description: params.title, - description: params.description, - caller_id: userId, -}; - -export { - mapping, - finalMapping, - params, - incidentResponse, - incidentAxiosResponse, - userId, - userIdResponse, - axiosResponse, - instance, - incident, -}; 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 deleted file mode 100644 index 889b57c8e92e23..00000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts +++ /dev/null @@ -1,70 +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 MapEntrySchema = schema.object({ - source: schema.string(), - target: schema.string(), - actionType: schema.oneOf([ - schema.literal('nothing'), - schema.literal('overwrite'), - schema.literal('append'), - ]), -}); - -export const CasesConfigurationSchema = schema.object({ - mapping: schema.arrayOf(MapEntrySchema), -}); - -export const ConfigSchemaProps = { - apiUrl: schema.string(), - casesConfiguration: CasesConfigurationSchema, -}; - -export const ConfigSchema = schema.object(ConfigSchemaProps); - -export const SecretsSchemaProps = { - password: schema.string(), - username: schema.string(), -}; - -export const SecretsSchema = schema.object(SecretsSchemaProps); - -export const UserSchema = schema.object({ - fullName: schema.nullable(schema.string()), - username: schema.string(), -}); - -const EntityInformationSchemaProps = { - createdAt: schema.string(), - createdBy: UserSchema, - updatedAt: schema.nullable(schema.string()), - updatedBy: schema.nullable(UserSchema), -}; - -export const EntityInformationSchema = schema.object(EntityInformationSchemaProps); - -export const CommentSchema = schema.object({ - commentId: schema.string(), - comment: schema.string(), - version: schema.maybe(schema.string()), - ...EntityInformationSchemaProps, -}); - -export const ExecutorAction = schema.oneOf([ - schema.literal('newIncident'), - schema.literal('updateIncident'), -]); - -export const ParamsSchema = schema.object({ - caseId: schema.string(), - title: schema.string(), - comments: schema.maybe(schema.arrayOf(CommentSchema)), - description: schema.maybe(schema.string()), - incidentId: schema.nullable(schema.string()), - ...EntityInformationSchemaProps, -}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/transformers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/transformers.ts deleted file mode 100644 index dc0a03fab8c715..00000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/transformers.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 { TransformerArgs } from './types'; -import * as i18n from './translations'; - -export const informationCreated = ({ - value, - date, - user, - ...rest -}: TransformerArgs): TransformerArgs => ({ - value: `${value} ${i18n.FIELD_INFORMATION('create', date, user)}`, - ...rest, -}); - -export const informationUpdated = ({ - value, - date, - user, - ...rest -}: TransformerArgs): TransformerArgs => ({ - value: `${value} ${i18n.FIELD_INFORMATION('update', date, user)}`, - ...rest, -}); - -export const informationAdded = ({ - value, - date, - user, - ...rest -}: TransformerArgs): TransformerArgs => ({ - value: `${value} ${i18n.FIELD_INFORMATION('add', date, user)}`, - ...rest, -}); - -export const append = ({ value, previousValue, ...rest }: TransformerArgs): TransformerArgs => ({ - value: previousValue ? `${previousValue} \r\n${value}` : `${value}`, - ...rest, -}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts deleted file mode 100644 index 3b216a6c3260ac..00000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts +++ /dev/null @@ -1,82 +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_REQUIRED = i18n.translate( - 'xpack.actions.builtin.servicenow.servicenowApiNullError', - { - defaultMessage: 'ServiceNow [apiUrl] is required', - } -); - -export const WHITE_LISTED_ERROR = (message: string) => - i18n.translate('xpack.actions.builtin.servicenow.servicenowApiWhitelistError', { - defaultMessage: 'error configuring servicenow action: {message}', - values: { - message, - }, - }); - -export const NAME = i18n.translate('xpack.actions.builtin.servicenowTitle', { - defaultMessage: 'ServiceNow', -}); - -export const MAPPING_EMPTY = i18n.translate('xpack.actions.builtin.servicenow.emptyMapping', { - defaultMessage: '[casesConfiguration.mapping]: expected non-empty but got empty', -}); - -export const ERROR_POSTING = i18n.translate( - 'xpack.actions.builtin.servicenow.postingErrorMessage', - { - defaultMessage: 'error posting servicenow event', - } -); - -export const RETRY_POSTING = (status: number) => - i18n.translate('xpack.actions.builtin.servicenow.postingRetryErrorMessage', { - defaultMessage: 'error posting servicenow event: http status {status}, retry later', - values: { - status, - }, - }); - -export const UNEXPECTED_STATUS = (status: number) => - i18n.translate('xpack.actions.builtin.servicenow.postingUnexpectedErrorMessage', { - defaultMessage: 'error posting servicenow event: unexpected status {status}', - values: { - status, - }, - }); - -export const FIELD_INFORMATION = ( - mode: string, - date: string | undefined, - user: string | undefined -) => { - switch (mode) { - case 'create': - return i18n.translate('xpack.actions.builtin.servicenow.informationCreated', { - values: { date, user }, - defaultMessage: '(created at {date} by {user})', - }); - case 'update': - return i18n.translate('xpack.actions.builtin.servicenow.informationUpdated', { - values: { date, user }, - defaultMessage: '(updated at {date} by {user})', - }); - case 'add': - return i18n.translate('xpack.actions.builtin.servicenow.informationAdded', { - values: { date, user }, - defaultMessage: '(added at {date} by {user})', - }); - default: - return i18n.translate('xpack.actions.builtin.servicenow.informationDefault', { - values: { date, user }, - defaultMessage: '(created at {date} by {user})', - }); - } -}; 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 deleted file mode 100644 index c5ef282aeffa72..00000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts +++ /dev/null @@ -1,103 +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 { - ConfigSchema, - SecretsSchema, - ParamsSchema, - CasesConfigurationSchema, - MapEntrySchema, - CommentSchema, -} from './schema'; - -import { ServiceNow } from './lib'; -import { Incident, IncidentResponse } from './lib/types'; - -// config definition -export type ConfigType = TypeOf; - -// secrets definition -export type SecretsType = TypeOf; - -export type ExecutorParams = TypeOf; - -export type CasesConfigurationType = TypeOf; -export type MapEntry = TypeOf; -export type Comment = TypeOf; - -export type Mapping = Map>; - -export interface Params extends ExecutorParams { - incident: Record; -} -export interface CreateHandlerArguments { - serviceNow: ServiceNow; - params: Params; - comments: Comment[]; - mapping: Mapping; -} - -export type UpdateHandlerArguments = CreateHandlerArguments & { - incidentId: string; -}; - -export type IncidentHandlerArguments = CreateHandlerArguments & { - incidentId: string | null; -}; - -export interface HandlerResponse extends IncidentResponse { - comments?: SimpleComment[]; -} - -export interface SimpleComment { - commentId: string; - pushedDate: string; -} - -export interface AppendFieldArgs { - value: string; - prefix?: string; - suffix?: string; -} - -export interface KeyAny { - [index: string]: unknown; -} - -export interface AppendInformationFieldArgs { - value: string; - user: string; - date: string; - mode: string; -} - -export interface TransformerArgs { - value: string; - date?: string; - user?: string; - previousValue?: string; -} - -export interface PrepareFieldsForTransformArgs { - params: Params; - mapping: Mapping; - defaultPipes?: string[]; -} - -export interface PipedField { - key: string; - value: string; - actionType: string; - pipes: string[]; -} - -export interface TransformFieldsArgs { - params: Params; - fields: PipedField[]; - currentIncident?: Incident; -} From 04cc0448cef4b0393e2f5e15d8ff5be5014fbc04 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 15 Apr 2020 19:25:43 +0300 Subject: [PATCH 04/33] Change UI --- x-pack/plugins/siem/public/containers/case/api.test.tsx | 2 +- x-pack/plugins/siem/public/containers/case/api.ts | 2 +- x-pack/plugins/siem/public/containers/case/mock.ts | 6 +++--- .../containers/case/use_post_push_to_service.test.tsx | 4 ++-- .../public/containers/case/use_post_push_to_service.tsx | 6 +++--- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/siem/public/containers/case/api.test.tsx b/x-pack/plugins/siem/public/containers/case/api.test.tsx index ad61e2b46f6c5f..bf1cfc8062bf3e 100644 --- a/x-pack/plugins/siem/public/containers/case/api.test.tsx +++ b/x-pack/plugins/siem/public/containers/case/api.test.tsx @@ -418,7 +418,7 @@ describe('Case Configuration API', () => { await pushToService(connectorId, casePushParams, abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith(`/api/action/${connectorId}/_execute`, { method: 'POST', - body: JSON.stringify({ params: casePushParams }), + body: JSON.stringify({ params: { action: 'pushToService', actionParams: casePushParams } }), signal: abortCtrl.signal, }); }); diff --git a/x-pack/plugins/siem/public/containers/case/api.ts b/x-pack/plugins/siem/public/containers/case/api.ts index b97f94a5a6b597..72fbf77defab9b 100644 --- a/x-pack/plugins/siem/public/containers/case/api.ts +++ b/x-pack/plugins/siem/public/containers/case/api.ts @@ -245,7 +245,7 @@ export const pushToService = async ( `${ACTION_URL}/${connectorId}/_execute`, { method: 'POST', - body: JSON.stringify({ params: casePushParams }), + body: JSON.stringify({ params: { action: 'pushToService', actionParams: casePushParams } }), signal, } ); diff --git a/x-pack/plugins/siem/public/containers/case/mock.ts b/x-pack/plugins/siem/public/containers/case/mock.ts index 0f44b3a1594ba0..a8f7aa4b944759 100644 --- a/x-pack/plugins/siem/public/containers/case/mock.ts +++ b/x-pack/plugins/siem/public/containers/case/mock.ts @@ -103,8 +103,8 @@ export const pushedCase: Case = { }; export const serviceConnector: ServiceConnectorCaseResponse = { - number: '123', - incidentId: '444', + title: '123', + id: '444', pushedDate: basicUpdatedAt, url: 'connector.com', comments: [ @@ -129,7 +129,7 @@ export const casePushParams = { caseId: basicCaseId, createdAt: basicCreatedAt, createdBy: elasticUser, - incidentId: null, + externalId: null, title: 'what a cool value', commentId: null, updatedAt: basicCreatedAt, diff --git a/x-pack/plugins/siem/public/containers/case/use_post_push_to_service.test.tsx b/x-pack/plugins/siem/public/containers/case/use_post_push_to_service.test.tsx index b07a346a8da46f..b9698c3e864e3f 100644 --- a/x-pack/plugins/siem/public/containers/case/use_post_push_to_service.test.tsx +++ b/x-pack/plugins/siem/public/containers/case/use_post_push_to_service.test.tsx @@ -55,8 +55,8 @@ describe('usePostPushToService', () => { { connector_id: samplePush.connectorId, connector_name: samplePush.connectorName, - external_id: serviceConnector.incidentId, - external_title: serviceConnector.number, + external_id: serviceConnector.id, + external_title: serviceConnector.title, external_url: serviceConnector.url, }, abortCtrl.signal diff --git a/x-pack/plugins/siem/public/containers/case/use_post_push_to_service.tsx b/x-pack/plugins/siem/public/containers/case/use_post_push_to_service.tsx index acd4b92ee430dc..c9d1b963f411aa 100644 --- a/x-pack/plugins/siem/public/containers/case/use_post_push_to_service.tsx +++ b/x-pack/plugins/siem/public/containers/case/use_post_push_to_service.tsx @@ -98,8 +98,8 @@ export const usePostPushToService = (): UsePostPushToService => { { connector_id: connectorId, connector_name: connectorName, - external_id: responseService.incidentId, - external_title: responseService.number, + external_id: responseService.id, + external_title: responseService.title, external_url: responseService.url, }, abortCtrl.signal @@ -180,7 +180,7 @@ export const formatServiceRequestData = (myCase: Case): ServiceConnectorCasePara : null, })), description, - incidentId: externalService?.externalId ?? null, + externalId: externalService?.externalId ?? null, title, updatedAt, updatedBy: From 71324a7eb3b9d0d20a928b0521d9fbd62d38474a Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 16 Apr 2020 12:08:42 +0300 Subject: [PATCH 05/33] Presonalized configuration --- .../connectors/servicenow/service.test.ts | 20 +++++++++++++------ .../connectors/servicenow/service.ts | 16 +++++++++++---- .../builtin_action_types/connectors/types.ts | 5 ++--- .../builtin_action_types/connectors/utils.ts | 8 ++++---- 4 files changed, 32 insertions(+), 17 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.test.ts index da409903d8ea5d..44404ec571bdb2 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.test.ts @@ -30,9 +30,8 @@ describe('ServiceNow service', () => { beforeAll(() => { service = createExternalService({ - url: 'https://dev102283.service-now.com', - username: 'admin', - password: 'admin', + config: { apiUrl: 'https://dev102283.service-now.com' }, + secrets: { username: 'admin', password: 'admin' }, }); }); @@ -43,19 +42,28 @@ describe('ServiceNow service', () => { describe('createExternalService', () => { test('throws without url', () => { expect(() => - createExternalService({ url: null, username: 'admin', password: 'admin' }) + createExternalService({ + config: { apiUrl: null }, + secrets: { username: 'admin', password: 'admin' }, + }) ).toThrow(); }); test('throws without username', () => { expect(() => - createExternalService({ url: 'test.com', username: '', password: 'admin' }) + createExternalService({ + config: { apiUrl: 'test.com' }, + secrets: { username: '', password: 'admin' }, + }) ).toThrow(); }); test('throws without password', () => { expect(() => - createExternalService({ url: 'test.com', username: '', password: undefined }) + createExternalService({ + config: { apiUrl: 'test.com' }, + secrets: { username: '', password: undefined }, + }) ).toThrow(); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.ts index 92748aa7f3929a..abcfcd15fff73d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.ts @@ -6,7 +6,13 @@ import axios from 'axios'; -import { ExternalServiceCredential, ExternalService, ExternalServiceParams } from '../types'; +import { + ExternalServiceCredential, + ExternalService, + ExternalServiceParams, + ConnectorPublicConfigurationType, + ConnectorSecretConfigurationType, +} from '../types'; import { addTimeZoneToDate, patch, request } from '../utils'; const API_VERSION = 'v2'; @@ -21,10 +27,12 @@ export const getErrorMessage = (msg: string) => { }; export const createExternalService = ({ - url, - username, - password, + config, + secrets, }: ExternalServiceCredential): ExternalService => { + const { apiUrl: url } = config as ConnectorPublicConfigurationType; + const { username, password } = secrets as ConnectorSecretConfigurationType; + if (!url || !username || !password) { throw Error('[Action][ServiceNow]: Wrong configuration.'); } diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/types.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/types.ts index 57ef97cb869a3f..5465f918592b7c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/types.ts @@ -42,9 +42,8 @@ export interface ConnectorConfiguration { } export interface ExternalServiceCredential { - url: string; - username: string; - password: string; + config: Record; + secrets: Record; } export interface ConnectorValidation { diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/utils.ts index 2df31f6437bf7b..77408b34faf56b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/utils.ts @@ -19,7 +19,6 @@ import { import { CreateConnectorArgs, ConnectorPublicConfigurationType, - ConnectorSecretConfigurationType, CreateActionTypeArgs, ExecutorParams, MapRecord, @@ -69,11 +68,9 @@ export const createConnectorExecutor = ({ ): Promise => { const actionId = execOptions.actionId; const { - apiUrl, casesConfiguration: { mapping: configurationMapping }, } = execOptions.config as ConnectorPublicConfigurationType; - const { username, password } = execOptions.secrets as ConnectorSecretConfigurationType; const params = execOptions.params as ExecutorParams; const { action, actionParams } = params; const { comments, externalId, ...restParams } = actionParams; @@ -87,7 +84,10 @@ export const createConnectorExecutor = ({ actionId, }; - const externalService = createExternalService({ url: apiUrl, username, password }); + const externalService = createExternalService({ + config: execOptions.config, + secrets: execOptions.secrets, + }); if (!api[action]) { throw new Error('[Action][Connector] Unsupported action type'); From f4ab071ec195e269c0b0b3f663ea8102747d9b55 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 16 Apr 2020 12:49:21 +0300 Subject: [PATCH 06/33] Refactor validators --- .../connectors/servicenow/translations.ts | 12 ------- .../connectors/servicenow/validators.ts | 36 +++---------------- .../connectors/translations.ts | 12 +++++++ .../connectors/validators.ts | 32 +++++++++++++++++ 4 files changed, 48 insertions(+), 44 deletions(-) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/connectors/validators.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/translations.ts index 349b2e2988d623..d4ebd4c38cee68 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/translations.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/translations.ts @@ -9,15 +9,3 @@ import { i18n } from '@kbn/i18n'; export const NAME = i18n.translate('xpack.actions.builtin.servicenowTitle', { defaultMessage: 'ServiceNow', }); - -export const MAPPING_EMPTY = i18n.translate('xpack.actions.builtin.servicenow.emptyMapping', { - defaultMessage: '[casesConfiguration.mapping]: expected non-empty but got empty', -}); - -export const WHITE_LISTED_ERROR = (message: string) => - i18n.translate('xpack.actions.builtin.servicenow.servicenowApiWhitelistError', { - defaultMessage: 'error configuring servicenow action: {message}', - values: { - message, - }, - }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/validators.ts index 9773ec3d61a174..aad631aad8ae26 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/validators.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/validators.ts @@ -4,38 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEmpty } from 'lodash'; - -import { ActionsConfigurationUtilities } from '../../../actions_config'; -import { - ConnectorSecretConfigurationType, - ConnectorPublicConfigurationType, - ConnectorValidation, -} from '../types'; - -import * as i18n from './translations'; - -const validateConfig = ( - configurationUtilities: ActionsConfigurationUtilities, - configObject: ConnectorPublicConfigurationType -) => { - try { - if (isEmpty(configObject.casesConfiguration.mapping)) { - return i18n.MAPPING_EMPTY; - } - - configurationUtilities.ensureWhitelistedUri(configObject.apiUrl); - } catch (whitelistError) { - return i18n.WHITE_LISTED_ERROR(whitelistError.message); - } -}; - -const validateSecrets = ( - configurationUtilities: ActionsConfigurationUtilities, - secrets: ConnectorSecretConfigurationType -) => {}; +import { validateCommonConfig, validateCommonSecrets } from '../validators'; +import { ConnectorValidation } from '../types'; export const validate: ConnectorValidation = { - config: validateConfig, - secrets: validateSecrets, + config: validateCommonConfig, + secrets: validateCommonSecrets, }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/translations.ts index 5e1dee44b0764f..692a4750b9fcb7 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/translations.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/translations.ts @@ -41,3 +41,15 @@ export const FIELD_INFORMATION = ( }); } }; + +export const MAPPING_EMPTY = i18n.translate('xpack.actions.builtin.connector.emptyMapping', { + defaultMessage: '[casesConfiguration.mapping]: expected non-empty but got empty', +}); + +export const WHITE_LISTED_ERROR = (message: string) => + i18n.translate('xpack.actions.builtin.connector.apiWhitelistError', { + defaultMessage: 'error configuring connector action: {message}', + values: { + message, + }, + }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/validators.ts new file mode 100644 index 00000000000000..2907d34ade5d8d --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/validators.ts @@ -0,0 +1,32 @@ +/* + * 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 { ConnectorSecretConfigurationType, ConnectorPublicConfigurationType } from './types'; + +import * as i18n from './translations'; + +export const validateCommonConfig = ( + configurationUtilities: ActionsConfigurationUtilities, + configObject: ConnectorPublicConfigurationType +) => { + try { + if (isEmpty(configObject.casesConfiguration.mapping)) { + return i18n.MAPPING_EMPTY; + } + + configurationUtilities.ensureWhitelistedUri(configObject.apiUrl); + } catch (whitelistError) { + return i18n.WHITE_LISTED_ERROR(whitelistError.message); + } +}; + +export const validateCommonSecrets = ( + configurationUtilities: ActionsConfigurationUtilities, + secrets: ConnectorSecretConfigurationType +) => {}; From 1273434d82cfc13e545a6c02c8228adad28dd24e Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 16 Apr 2020 13:04:19 +0300 Subject: [PATCH 07/33] Refactor get error message --- .../connectors/servicenow/service.test.ts | 9 +-------- .../connectors/servicenow/service.ts | 20 +++++++++++-------- .../connectors/utils.test.ts | 8 ++++++++ .../builtin_action_types/connectors/utils.ts | 4 ++++ 4 files changed, 25 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.test.ts index 44404ec571bdb2..070c91b471f123 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.test.ts @@ -6,7 +6,7 @@ import axios from 'axios'; -import { getErrorMessage, createExternalService } from './service'; +import { createExternalService } from './service'; import * as utils from '../utils'; import { ExternalService } from '../types'; import { object } from 'joi'; @@ -68,13 +68,6 @@ describe('ServiceNow service', () => { }); }); - describe('getErrorMessage', () => { - test('return correct message', () => { - const msg = getErrorMessage('An error has occurred'); - expect(msg).toBe('[Action][ServiceNow]: An error has occurred'); - }); - }); - describe('getIncident', () => { test('it returns the incident correctly', async () => { requestMock.mockImplementation(() => ({ diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.ts index abcfcd15fff73d..87c76f889976cd 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.ts @@ -13,7 +13,9 @@ import { ConnectorPublicConfigurationType, ConnectorSecretConfigurationType, } from '../types'; -import { addTimeZoneToDate, patch, request } from '../utils'; +import { addTimeZoneToDate, patch, request, getErrorMessage } from '../utils'; + +import * as i18n from './translations'; const API_VERSION = 'v2'; const INCIDENT_URL = `api/now/${API_VERSION}/table/incident`; @@ -22,10 +24,6 @@ const COMMENT_URL = `api/now/${API_VERSION}/table/incident`; // Based on: https://docs.servicenow.com/bundle/orlando-platform-user-interface/page/use/navigation/reference/r_NavigatingByURLExamples.html const VIEW_INCIDENT_URL = `nav_to.do?uri=incident.do?sys_id=`; -export const getErrorMessage = (msg: string) => { - return `[Action][ServiceNow]: ${msg}`; -}; - export const createExternalService = ({ config, secrets, @@ -57,7 +55,7 @@ export const createExternalService = ({ return { ...res.data.result }; } catch (error) { throw new Error( - getErrorMessage(`Unable to get incident with id ${id}. Error: ${error.message}`) + getErrorMessage(i18n.NAME, `Unable to get incident with id ${id}. Error: ${error.message}`) ); } }; @@ -78,7 +76,9 @@ export const createExternalService = ({ url: getIncidentViewURL(res.data.result.sys_id), }; } catch (error) { - throw new Error(getErrorMessage(`Unable to create incident. Error: ${error.message}`)); + throw new Error( + getErrorMessage(i18n.NAME, `Unable to create incident. Error: ${error.message}`) + ); } }; @@ -98,7 +98,10 @@ export const createExternalService = ({ }; } catch (error) { throw new Error( - getErrorMessage(`Unable to update incident with id ${incidentId}. Error: ${error.message}`) + getErrorMessage( + i18n.NAME, + `Unable to update incident with id ${incidentId}. Error: ${error.message}` + ) ); } }; @@ -118,6 +121,7 @@ export const createExternalService = ({ } catch (error) { throw new Error( getErrorMessage( + i18n.NAME, `Unable to create comment at incident with id ${incidentId}. Error: ${error.message}` ) ); diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/utils.test.ts index fc0838f5c85c39..4304410f81b873 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/utils.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/utils.test.ts @@ -17,6 +17,7 @@ import { throwIfNotAlive, request, patch, + getErrorMessage, } from './utils'; import { SUPPORTED_SOURCE_FIELDS } from './constants'; @@ -579,3 +580,10 @@ describe('patch', () => { expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'patch', data: { id: '123' } }); }); }); + +describe('getErrorMessage', () => { + test('it returns the correct error message', () => { + const msg = getErrorMessage('My connector name', 'An error has occurred'); + expect(msg).toBe('[Action][My connector name]: An error has occurred'); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/utils.ts index 77408b34faf56b..2a4285925b6be7 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/utils.ts @@ -236,3 +236,7 @@ export const transformComments = (comments: Comment[], pipes: string[]): Comment }).value, })); }; + +export const getErrorMessage = (connector: string, msg: string) => { + return `[Action][${connector}]: ${msg}`; +}; From c629c13aff4531c918031ba0a934829a8dad5a2a Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 16 Apr 2020 13:27:20 +0300 Subject: [PATCH 08/33] Init IBM Resilient --- .../connectors/resilient/config.ts | 13 ++++ .../connectors/resilient/schema.ts | 22 ++++++ .../connectors/resilient/service.ts | 70 +++++++++++++++++++ .../connectors/resilient/translations.ts | 11 +++ .../connectors/resilient/types.ts | 11 +++ .../connectors/resilient/validators.ts | 13 ++++ .../connectors/servicenow/service.ts | 2 +- 7 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/connectors/resilient/config.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/connectors/resilient/schema.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/connectors/resilient/service.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/connectors/resilient/translations.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/connectors/resilient/types.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/connectors/resilient/validators.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/resilient/config.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/resilient/config.ts new file mode 100644 index 00000000000000..a090fa40ee8ed4 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/resilient/config.ts @@ -0,0 +1,13 @@ +/* + * 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 { ConnectorConfiguration } from '../types'; +import * as i18n from './translations'; + +export const config: ConnectorConfiguration = { + id: '.resilient', + name: i18n.NAME, +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/resilient/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/resilient/schema.ts new file mode 100644 index 00000000000000..936a17a6a1fedd --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/resilient/schema.ts @@ -0,0 +1,22 @@ +/* + * 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'; +import { ConnectorPublicConfiguration } from '../schema'; + +export const ResilientPublicConfiguration = { + orgId: schema.number(), + ...ConnectorPublicConfiguration, +}; + +export const ResilientPublicConfigurationSchema = schema.object(ResilientPublicConfiguration); + +export const ResilientSecretConfiguration = { + apiKey: schema.string(), + apiSecret: schema.string(), +}; + +export const ResilientSecretConfigurationSchema = schema.object(ResilientSecretConfiguration); diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/resilient/service.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/resilient/service.ts new file mode 100644 index 00000000000000..29473942ed6914 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/resilient/service.ts @@ -0,0 +1,70 @@ +/* + * 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 axios from 'axios'; + +import { ExternalServiceCredential, ExternalService, ExternalServiceParams } from '../types'; +import { addTimeZoneToDate, patch, request, getErrorMessage } from '../utils'; +import { ResilientPublicConfigurationType, ResilientSecretConfigurationType } from './types'; + +import * as i18n from './translations'; + +const BASE_URL = 'rest/orgs/'; +const INCIDENT_URL = `incidents`; +const COMMENT_URL = `tasks`; + +const VIEW_INCIDENT_URL = `#incidents`; + +export const createExternalService = ({ + config, + secrets, +}: ExternalServiceCredential): ExternalService => { + const { apiUrl: url, orgId } = config as ResilientPublicConfigurationType; + const { apiKey, apiSecret } = secrets as ResilientSecretConfigurationType; + let apiKeyHandle; + + if (!url || !apiKey || !apiSecret) { + throw Error(`[Action]${i18n.NAME}: Wrong configuration.`); + } + + const incidentUrl = `${url}/${BASE_URL}/${orgId}/${INCIDENT_URL}`; + const commentUrl = `${url}/${BASE_URL}/${orgId}/${COMMENT_URL}`; + const sessionUrl = `${url}/rest/session`; + const axiosInstance = axios.create({ + auth: { username: apiKey, password: apiSecret }, + }); + + const getIncidentViewURL = (id: string) => { + return `${url}/${VIEW_INCIDENT_URL}/${id}`; + }; + + const getApiKeyHandle = async (): Promise => { + try { + const res = await request({ + axios: axiosInstance, + url: sessionUrl, + }); + + return res.data.api_key_handle; + } catch (error) { + throw new Error(getErrorMessage(i18n.NAME, `Unable to authenticate user`)); + } + }; + const getIncident = async (id: string) => {}; + + const createIncident = async ({ incident }: ExternalServiceParams) => {}; + + const updateIncident = async ({ incidentId, incident }: ExternalServiceParams) => {}; + + const createComment = async ({ incidentId, comment, field }: ExternalServiceParams) => {}; + + return { + getIncident, + createIncident, + updateIncident, + createComment, + }; +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/resilient/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/resilient/translations.ts new file mode 100644 index 00000000000000..10c1138cf0ec42 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/resilient/translations.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const NAME = i18n.translate('xpack.actions.builtin.resilientTitle', { + defaultMessage: 'IBM Resilient', +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/resilient/types.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/resilient/types.ts new file mode 100644 index 00000000000000..29c02c183dbf83 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/resilient/types.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TypeOf } from '@kbn/config-schema'; +import { ResilientPublicConfigurationSchema, ResilientSecretConfigurationSchema } from './schema'; + +export type ResilientPublicConfigurationType = TypeOf; +export type ResilientSecretConfigurationType = TypeOf; diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/resilient/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/resilient/validators.ts new file mode 100644 index 00000000000000..aad631aad8ae26 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/resilient/validators.ts @@ -0,0 +1,13 @@ +/* + * 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 { validateCommonConfig, validateCommonSecrets } from '../validators'; +import { ConnectorValidation } from '../types'; + +export const validate: ConnectorValidation = { + config: validateCommonConfig, + secrets: validateCommonSecrets, +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.ts index 87c76f889976cd..afaf1415a7736b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.ts @@ -32,7 +32,7 @@ export const createExternalService = ({ const { username, password } = secrets as ConnectorSecretConfigurationType; if (!url || !username || !password) { - throw Error('[Action][ServiceNow]: Wrong configuration.'); + throw Error(`[Action]${i18n.NAME}: Wrong configuration.`); } const incidentUrl = `${url}/${INCIDENT_URL}`; From cb210627f3bc952175b33746026e17e92be8de9a Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 16 Apr 2020 15:02:49 +0300 Subject: [PATCH 09/33] Sequential comments --- .../connectors/servicenow/api.ts | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/api.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/api.ts index 687b853234ed7e..aa3ac4ccadaf9f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/api.ts @@ -68,15 +68,18 @@ const pushToServiceHandler = async ({ ) { const commentsTransformed = transformComments(comments, ['informationAdded']); - const createdComments = await Promise.all( - commentsTransformed.map(comment => - externalService.createComment({ - incidentId: res.id, - comment, - field: mapping.get('comments').target, - }) - ) - ); + // Create comments sequentially. + const promises = comments.reduce(async (prevPromise, currentComment) => { + const totalComments = await prevPromise; + const comment = await externalService.createComment({ + incidentId: res.id, + comment: currentComment, + field: mapping.get('comments').target, + }); + return [...totalComments, comment]; + }, Promise.resolve([] as ExternalServiceCommentResponse[])); + + const createdComments = await promises; const zippedComments: ExternalServiceCommentResponse[] = zipWith( commentsTransformed, From 4e256ff19df1b214c4617e4b88e1e0b1f8a6bc1f Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 16 Apr 2020 19:40:21 +0300 Subject: [PATCH 10/33] Create resilient flyout --- .../connectors/servicenow/service.ts | 13 +- .../connectors/servicenow/types.ts | 10 + .../siem/public/lib/connectors/config.ts | 16 +- .../siem/public/lib/connectors/index.ts | 1 + .../public/lib/connectors/resilient/config.ts | 20 ++ .../public/lib/connectors/resilient/index.tsx | 278 ++++++++++++++++++ .../public/lib/connectors/resilient/logo.svg | 5 + .../lib/connectors/resilient/translations.ts | 34 +++ .../public/lib/connectors/resilient/types.ts | 18 ++ .../siem/public/lib/connectors/servicenow.tsx | 31 +- .../lib/connectors/servicenow/config.ts | 20 ++ .../lib/connectors/servicenow/index.tsx | 247 ++++++++++++++++ .../public/lib/connectors/servicenow/logo.svg | 5 + .../lib/connectors/servicenow/translations.ts | 23 ++ .../public/lib/connectors/servicenow/types.ts | 18 ++ .../public/lib/connectors/translations.ts | 68 +++-- .../siem/public/lib/connectors/types.ts | 6 +- .../configure_cases/connectors_dropdown.tsx | 4 +- .../case/components/configure_cases/index.tsx | 26 +- 19 files changed, 766 insertions(+), 77 deletions(-) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/types.ts create mode 100644 x-pack/plugins/siem/public/lib/connectors/resilient/config.ts create mode 100644 x-pack/plugins/siem/public/lib/connectors/resilient/index.tsx create mode 100644 x-pack/plugins/siem/public/lib/connectors/resilient/logo.svg create mode 100644 x-pack/plugins/siem/public/lib/connectors/resilient/translations.ts create mode 100644 x-pack/plugins/siem/public/lib/connectors/resilient/types.ts create mode 100644 x-pack/plugins/siem/public/lib/connectors/servicenow/config.ts create mode 100644 x-pack/plugins/siem/public/lib/connectors/servicenow/index.tsx create mode 100644 x-pack/plugins/siem/public/lib/connectors/servicenow/logo.svg create mode 100644 x-pack/plugins/siem/public/lib/connectors/servicenow/translations.ts create mode 100644 x-pack/plugins/siem/public/lib/connectors/servicenow/types.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.ts index afaf1415a7736b..04547d5a72c7bd 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.ts @@ -6,16 +6,11 @@ import axios from 'axios'; -import { - ExternalServiceCredential, - ExternalService, - ExternalServiceParams, - ConnectorPublicConfigurationType, - ConnectorSecretConfigurationType, -} from '../types'; +import { ExternalServiceCredential, ExternalService, ExternalServiceParams } from '../types'; import { addTimeZoneToDate, patch, request, getErrorMessage } from '../utils'; import * as i18n from './translations'; +import { ServiceNowPublicConfigurationType, ServiceNowSecretConfigurationType } from './types'; const API_VERSION = 'v2'; const INCIDENT_URL = `api/now/${API_VERSION}/table/incident`; @@ -28,8 +23,8 @@ export const createExternalService = ({ config, secrets, }: ExternalServiceCredential): ExternalService => { - const { apiUrl: url } = config as ConnectorPublicConfigurationType; - const { username, password } = secrets as ConnectorSecretConfigurationType; + const { apiUrl: url } = config as ServiceNowPublicConfigurationType; + const { username, password } = secrets as ServiceNowSecretConfigurationType; if (!url || !username || !password) { throw Error(`[Action]${i18n.NAME}: Wrong configuration.`); diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/types.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/types.ts new file mode 100644 index 00000000000000..6bcb2c95ccdb58 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/types.ts @@ -0,0 +1,10 @@ +/* + * 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 { + ConnectorPublicConfigurationType as ServiceNowPublicConfigurationType, + ConnectorSecretConfigurationType as ServiceNowSecretConfigurationType, +} from '../types'; diff --git a/x-pack/plugins/siem/public/lib/connectors/config.ts b/x-pack/plugins/siem/public/lib/connectors/config.ts index baeb69b3f69436..f0e0bf96bbc1bb 100644 --- a/x-pack/plugins/siem/public/lib/connectors/config.ts +++ b/x-pack/plugins/siem/public/lib/connectors/config.ts @@ -5,17 +5,17 @@ */ import { CasesConfigurationMapping } from '../../containers/case/configure/types'; -import serviceNowLogo from './logos/servicenow.svg'; + import { Connector } from './types'; +import { connector as serviceNowConnectorConfig } from './servicenow/config'; +import { connector as resilientConnectorConfig } from './resilient/config'; -const connectors: Record = { - '.servicenow': { - actionTypeId: '.servicenow', - logo: serviceNowLogo, - }, +export const connectorsConfiguration: Record = { + '.servicenow': { ...serviceNowConnectorConfig }, + '.resilient': { ...resilientConnectorConfig }, }; -const defaultMapping: CasesConfigurationMapping[] = [ +export const defaultMapping: CasesConfigurationMapping[] = [ { source: 'title', target: 'short_description', @@ -32,5 +32,3 @@ const defaultMapping: CasesConfigurationMapping[] = [ actionType: 'append', }, ]; - -export { connectors, defaultMapping }; diff --git a/x-pack/plugins/siem/public/lib/connectors/index.ts b/x-pack/plugins/siem/public/lib/connectors/index.ts index fdf337b5ef1204..d95e73dfeeced9 100644 --- a/x-pack/plugins/siem/public/lib/connectors/index.ts +++ b/x-pack/plugins/siem/public/lib/connectors/index.ts @@ -5,3 +5,4 @@ */ export { getActionType as serviceNowActionType } from './servicenow'; +export { getActionType as resilientActionType } from './resilient'; diff --git a/x-pack/plugins/siem/public/lib/connectors/resilient/config.ts b/x-pack/plugins/siem/public/lib/connectors/resilient/config.ts new file mode 100644 index 00000000000000..950ad96f4ed7f7 --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/resilient/config.ts @@ -0,0 +1,20 @@ +/* + * 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 { Connector } from '../types'; + +import { RESILIENT_TITLE } from './translations'; +import logo from './logo.svg'; + +export const connector: Connector = { + id: '.resilient', + name: RESILIENT_TITLE, + logo, + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'platinum', +}; diff --git a/x-pack/plugins/siem/public/lib/connectors/resilient/index.tsx b/x-pack/plugins/siem/public/lib/connectors/resilient/index.tsx new file mode 100644 index 00000000000000..61d4e20e68e6e5 --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/resilient/index.tsx @@ -0,0 +1,278 @@ +/* + * 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, ChangeEvent, useEffect } from 'react'; +import { + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiSpacer, + EuiFieldPassword, +} from '@elastic/eui'; + +import { isEmpty, get } from 'lodash/fp'; + +import { + ActionConnectorFieldsProps, + ActionTypeModel, + ValidationResult, + ActionParamsProps, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../../../plugins/triggers_actions_ui/public/types'; + +import { FieldMapping } from '../../../pages/case/components/configure_cases/field_mapping'; + +import * as i18n from './translations'; + +import { ResilientActionConnector } from './types'; +import { isUrlInvalid } from '../validators'; + +import { defaultMapping } from '../config'; +import { CasesConfigurationMapping } from '../../../containers/case/configure/types'; + +import { connector } from './config'; + +interface ResilientActionParams { + message: string; +} + +interface Errors { + apiUrl: string[]; + orgId: string[]; + apiKey: string[]; + apiSecret: string[]; +} + +export function getActionType(): ActionTypeModel { + return { + id: connector.id, + iconClass: connector.logo, + selectMessage: i18n.RESILIENT_DESC, + actionTypeTitle: connector.name, + validateConnector: (action: ResilientActionConnector): ValidationResult => { + const errors: Errors = { + apiUrl: [], + orgId: [], + apiKey: [], + apiSecret: [], + }; + + 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]; + } + + if (!action.config.orgId) { + errors.orgId = [...errors.orgId, i18n.RESILIENT_ORG_ID_REQUIRED]; + } + + if (!action.secrets.apiKey) { + errors.apiKey = [...errors.apiKey, i18n.API_KEY_REQUIRED]; + } + + if (!action.secrets.apiSecret) { + errors.apiSecret = [...errors.apiSecret, i18n.API_SECRET_REQUIRED]; + } + + return { errors }; + }, + validateParams: (actionParams: ResilientActionParams): ValidationResult => { + return { errors: {} }; + }, + actionConnectorFields: ResilientConnectorFields, + actionParamsFields: ResilientParamsFields, + }; +} + +const ResilientConnectorFields: React.FunctionComponent> = ({ 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, orgId, casesConfiguration: { mapping = [] } = {} } = action.config; + const { apiKey, apiSecret } = action.secrets; + + const isApiUrlInvalid: boolean = errors.apiUrl.length > 0 && apiUrl != null; + const isOrgIdInvalid: boolean = errors.orgId.length > 0 && orgId != null; + const isApiKeyInvalid: boolean = errors.apiKey.length > 0 && apiKey != null; + const isApiSecretInvalid: boolean = errors.apiSecret.length > 0 && apiSecret != 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)) { + editActionSecrets('apiKey', ''); + editActionSecrets('apiSecret', ''); + } + }, []); + + if (isEmpty(mapping)) { + editActionConfig('casesConfiguration', { + ...action.config.casesConfiguration, + mapping: defaultMapping, + }); + } + + const handleOnChangeActionConfig = useCallback( + (key: string, evt: ChangeEvent) => editActionConfig(key, evt.target.value), + [] + ); + + const handleOnBlurActionConfig = useCallback( + (key: string) => { + if (['apiUrl', 'orgId'].includes(key) && get(key, action.config) == null) { + editActionConfig(key, ''); + } + }, + [action.config] + ); + + const handleOnChangeSecretConfig = useCallback( + (key: string, evt: ChangeEvent) => editActionSecrets(key, evt.target.value), + [] + ); + + const handleOnBlurSecretConfig = useCallback( + (key: string) => { + if (['apiKey', 'apiSecret'].includes(key) && get(key, action.secrets) == null) { + editActionSecrets(key, ''); + } + }, + [action.secrets] + ); + + const handleOnChangeMappingConfig = useCallback( + (newMapping: CasesConfigurationMapping[]) => + editActionConfig('casesConfiguration', { + ...action.config.casesConfiguration, + mapping: newMapping, + }), + [action.config] + ); + + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +const ResilientParamsFields: React.FunctionComponent> = ({ + actionParams, + editAction, + index, + errors, +}) => { + return null; +}; diff --git a/x-pack/plugins/siem/public/lib/connectors/resilient/logo.svg b/x-pack/plugins/siem/public/lib/connectors/resilient/logo.svg new file mode 100644 index 00000000000000..dcd022a8dca18d --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/resilient/logo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/x-pack/plugins/siem/public/lib/connectors/resilient/translations.ts b/x-pack/plugins/siem/public/lib/connectors/resilient/translations.ts new file mode 100644 index 00000000000000..e4dd8cd5400dfc --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/resilient/translations.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * 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.siem.case.connectors.resilient.selectMessageText', + { + defaultMessage: 'Push or update IBM Resilient case data to a new incident in ServiceNow', + } +); + +export const RESILIENT_TITLE = i18n.translate( + 'xpack.siem.case.connectors.resilient.actionTypeTitle', + { + defaultMessage: 'IBM Resilient', + } +); + +export const RESILIENT_ORG_ID = i18n.translate('xpack.siem.case.connectors.resilient.orgId', { + defaultMessage: 'Organization ID', +}); + +export const RESILIENT_ORG_ID_REQUIRED = i18n.translate( + 'xpack.siem.case.connectors.common.requiredOrgIdTextField', + { + defaultMessage: 'Organization ID is required', + } +); diff --git a/x-pack/plugins/siem/public/lib/connectors/resilient/types.ts b/x-pack/plugins/siem/public/lib/connectors/resilient/types.ts new file mode 100644 index 00000000000000..8c299b1ef379e2 --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/resilient/types.ts @@ -0,0 +1,18 @@ +/* + * 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 '../../../../../../../plugins/actions/server/builtin_action_types/connectors/resilient/types'; + +export interface ResilientActionConnector { + config: ResilientPublicConfigurationType; + secrets: ResilientSecretConfigurationType; +} diff --git a/x-pack/plugins/siem/public/lib/connectors/servicenow.tsx b/x-pack/plugins/siem/public/lib/connectors/servicenow.tsx index 9fe0b4a957cebd..c69e02627f89ca 100644 --- a/x-pack/plugins/siem/public/lib/connectors/servicenow.tsx +++ b/x-pack/plugins/siem/public/lib/connectors/servicenow.tsx @@ -23,17 +23,18 @@ import { // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../triggers_actions_ui/public/types'; -import { FieldMapping } from '../../pages/case/components/configure_cases/field_mapping'; +import { FieldMapping } from '../../../pages/case/components/configure_cases/field_mapping'; import * as i18n from './translations'; import { ServiceNowActionConnector } from './types'; -import { isUrlInvalid } from './validators'; +import { isUrlInvalid } from '../validators'; -import { connectors, defaultMapping } from './config'; -import { CasesConfigurationMapping } from '../../containers/case/configure/types'; +import { defaultMapping } from '../config'; +import { CasesConfigurationMapping } from '../../../containers/case/configure/types'; -const serviceNowDefinition = connectors['.servicenow']; +import { connector } from './config'; +import logo from './logo.svg'; interface ServiceNowActionParams { message: string; @@ -47,10 +48,10 @@ interface Errors { export function getActionType(): ActionTypeModel { return { - id: serviceNowDefinition.actionTypeId, - iconClass: serviceNowDefinition.logo, + id: connector.id, + iconClass: logo, selectMessage: i18n.SERVICENOW_DESC, - actionTypeTitle: i18n.SERVICENOW_TITLE, + actionTypeTitle: connector.name, validateConnector: (action: ServiceNowActionConnector): ValidationResult => { const errors: Errors = { apiUrl: [], @@ -59,19 +60,19 @@ export function getActionType(): ActionTypeModel { }; if (!action.config.apiUrl) { - errors.apiUrl = [...errors.apiUrl, i18n.SERVICENOW_API_URL_REQUIRED]; + errors.apiUrl = [...errors.apiUrl, i18n.API_URL_REQUIRED]; } if (isUrlInvalid(action.config.apiUrl)) { - errors.apiUrl = [...errors.apiUrl, i18n.SERVICENOW_API_URL_INVALID]; + errors.apiUrl = [...errors.apiUrl, i18n.API_URL_INVALID]; } if (!action.secrets.username) { - errors.username = [...errors.username, i18n.SERVICENOW_USERNAME_REQUIRED]; + errors.username = [...errors.username, i18n.USERNAME_REQUIRED]; } if (!action.secrets.password) { - errors.password = [...errors.password, i18n.SERVICENOW_PASSWORD_REQUIRED]; + errors.password = [...errors.password, i18n.PASSWORD_REQUIRED]; } return { errors }; @@ -166,7 +167,7 @@ const ServiceNowConnectorFields: React.FunctionComponent { + const errors: Errors = { + apiUrl: [], + username: [], + password: [], + }; + + 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]; + } + + if (!action.secrets.username) { + errors.username = [...errors.username, i18n.USERNAME_REQUIRED]; + } + + if (!action.secrets.password) { + errors.password = [...errors.password, i18n.PASSWORD_REQUIRED]; + } + + return { errors }; + }, + validateParams: (actionParams: ServiceNowActionParams): ValidationResult => { + return { errors: {} }; + }, + actionConnectorFields: ServiceNowConnectorFields, + actionParamsFields: ServiceNowParamsFields, + }; +} + +const ServiceNowConnectorFields: React.FunctionComponent> = ({ 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 { username, password } = action.secrets; + + const isApiUrlInvalid: boolean = errors.apiUrl.length > 0 && apiUrl != null; + const isUsernameInvalid: boolean = errors.username.length > 0 && username != null; + const isPasswordInvalid: boolean = errors.password.length > 0 && password != 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)) { + editActionSecrets('username', ''); + editActionSecrets('password', ''); + } + }, []); + + if (isEmpty(mapping)) { + editActionConfig('casesConfiguration', { + ...action.config.casesConfiguration, + mapping: defaultMapping, + }); + } + + const handleOnChangeActionConfig = useCallback( + (key: string, evt: ChangeEvent) => editActionConfig(key, evt.target.value), + [] + ); + + const handleOnBlurActionConfig = useCallback( + (key: string) => { + if (key === 'apiUrl' && action.config[key] == null) { + editActionConfig(key, ''); + } + }, + [action.config] + ); + + const handleOnChangeSecretConfig = useCallback( + (key: string, evt: ChangeEvent) => editActionSecrets(key, evt.target.value), + [] + ); + + const handleOnBlurSecretConfig = useCallback( + (key: string) => { + if (['username', 'password'].includes(key) && get(key, action.secrets) == null) { + editActionSecrets(key, ''); + } + }, + [action.secrets] + ); + + const handleOnChangeMappingConfig = useCallback( + (newMapping: CasesConfigurationMapping[]) => + editActionConfig('casesConfiguration', { + ...action.config.casesConfiguration, + mapping: newMapping, + }), + [action.config] + ); + + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +const ServiceNowParamsFields: React.FunctionComponent> = ({ actionParams, editAction, index, errors }) => { + return null; +}; diff --git a/x-pack/plugins/siem/public/lib/connectors/servicenow/logo.svg b/x-pack/plugins/siem/public/lib/connectors/servicenow/logo.svg new file mode 100644 index 00000000000000..dcd022a8dca18d --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/servicenow/logo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/x-pack/plugins/siem/public/lib/connectors/servicenow/translations.ts b/x-pack/plugins/siem/public/lib/connectors/servicenow/translations.ts new file mode 100644 index 00000000000000..5dac9eddd15369 --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/servicenow/translations.ts @@ -0,0 +1,23 @@ +/* + * 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 SERVICENOW_DESC = i18n.translate( + 'xpack.siem.case.connectors.servicenow.selectMessageText', + { + defaultMessage: 'Push or update SIEM case data to a new incident in ServiceNow', + } +); + +export const SERVICENOW_TITLE = i18n.translate( + 'xpack.siem.case.connectors.servicenow.actionTypeTitle', + { + defaultMessage: 'ServiceNow', + } +); diff --git a/x-pack/plugins/siem/public/lib/connectors/servicenow/types.ts b/x-pack/plugins/siem/public/lib/connectors/servicenow/types.ts new file mode 100644 index 00000000000000..5427ddb0afbb35 --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/servicenow/types.ts @@ -0,0 +1,18 @@ +/* + * 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 { + ServiceNowPublicConfigurationType, + ServiceNowSecretConfigurationType, +} from '../../../../../../../plugins/actions/server/builtin_action_types/connectors/servicenow/types'; + +export interface ServiceNowActionConnector { + config: ServiceNowPublicConfigurationType; + secrets: ServiceNowSecretConfigurationType; +} diff --git a/x-pack/plugins/siem/public/lib/connectors/translations.ts b/x-pack/plugins/siem/public/lib/connectors/translations.ts index ae2084120255c1..abd4c04c7e8bc3 100644 --- a/x-pack/plugins/siem/public/lib/connectors/translations.ts +++ b/x-pack/plugins/siem/public/lib/connectors/translations.ts @@ -6,65 +6,79 @@ import { i18n } from '@kbn/i18n'; -export const SERVICENOW_DESC = i18n.translate( - 'xpack.siem.case.connectors.servicenow.selectMessageText', +export const API_URL_LABEL = i18n.translate( + 'xpack.siem.case.connectors.common.apiUrlTextFieldLabel', { - defaultMessage: 'Push or update SIEM case data to a new incident in ServiceNow', + defaultMessage: 'URL', } ); -export const SERVICENOW_TITLE = i18n.translate( - 'xpack.siem.case.connectors.servicenow.actionTypeTitle', +export const API_URL_REQUIRED = i18n.translate( + 'xpack.siem.case.connectors.common.requiredApiUrlTextField', { - defaultMessage: 'ServiceNow', + defaultMessage: 'URL is required', } ); -export const SERVICENOW_API_URL_LABEL = i18n.translate( - 'xpack.siem.case.connectors.servicenow.apiUrlTextFieldLabel', +export const API_URL_INVALID = i18n.translate( + 'xpack.siem.case.connectors.common.invalidApiUrlTextField', { - defaultMessage: 'URL', + defaultMessage: 'URL is invalid', } ); -export const SERVICENOW_API_URL_REQUIRED = i18n.translate( - 'xpack.siem.case.connectors.servicenow.requiredApiUrlTextField', +export const USERNAME_LABEL = i18n.translate( + 'xpack.siem.case.connectors.common.usernameTextFieldLabel', { - defaultMessage: 'URL is required', + defaultMessage: 'Username', } ); -export const SERVICENOW_API_URL_INVALID = i18n.translate( - 'xpack.siem.case.connectors.servicenow.invalidApiUrlTextField', +export const USERNAME_REQUIRED = i18n.translate( + 'xpack.siem.case.connectors.common.requiredUsernameTextField', { - defaultMessage: 'URL is invalid', + defaultMessage: 'Username is required', } ); -export const SERVICENOW_USERNAME_LABEL = i18n.translate( - 'xpack.siem.case.connectors.servicenow.usernameTextFieldLabel', +export const PASSWORD_LABEL = i18n.translate( + 'xpack.siem.case.connectors.common.passwordTextFieldLabel', { - defaultMessage: 'Username', + defaultMessage: 'Password', } ); -export const SERVICENOW_USERNAME_REQUIRED = i18n.translate( - 'xpack.siem.case.connectors.servicenow.requiredUsernameTextField', +export const PASSWORD_REQUIRED = i18n.translate( + 'xpack.siem.case.connectors.common.requiredPasswordTextField', { - defaultMessage: 'Username is required', + defaultMessage: 'Password is required', } ); -export const SERVICENOW_PASSWORD_LABEL = i18n.translate( - 'xpack.siem.case.connectors.servicenow.passwordTextFieldLabel', +export const API_KEY_LABEL = i18n.translate( + 'xpack.siem.case.connectors.common.apiKeyTextFieldLabel', { - defaultMessage: 'Password', + defaultMessage: 'Api key', } ); -export const SERVICENOW_PASSWORD_REQUIRED = i18n.translate( - 'xpack.siem.case.connectors.servicenow.requiredPasswordTextField', +export const API_KEY_REQUIRED = i18n.translate( + 'xpack.siem.case.connectors.common.requiredApiKeyTextField', { - defaultMessage: 'Password is required', + defaultMessage: 'Api key is required', + } +); + +export const API_SECRET_LABEL = i18n.translate( + 'xpack.siem.case.connectors.common.apiSecretTextFieldLabel', + { + defaultMessage: 'Api secret', + } +); + +export const API_SECRET_REQUIRED = i18n.translate( + 'xpack.siem.case.connectors.common.requiredApiSecretTextField', + { + defaultMessage: 'Api secret is required', } ); diff --git a/x-pack/plugins/siem/public/lib/connectors/types.ts b/x-pack/plugins/siem/public/lib/connectors/types.ts index 2def4b5107aee8..792465341724d1 100644 --- a/x-pack/plugins/siem/public/lib/connectors/types.ts +++ b/x-pack/plugins/siem/public/lib/connectors/types.ts @@ -4,8 +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 '../../../../../../plugins/triggers_actions_ui/public'; import { ConfigType, @@ -17,7 +16,6 @@ export interface ServiceNowActionConnector { secrets: SecretsType; } -export interface Connector { - actionTypeId: string; +export interface Connector extends ActionType { logo: string; } diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.tsx index 15066e73eee829..d5575f3bac4c8c 100644 --- a/x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.tsx +++ b/x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.tsx @@ -9,7 +9,7 @@ import { EuiIcon, EuiSuperSelect } from '@elastic/eui'; import styled from 'styled-components'; import { Connector } from '../../../../containers/case/configure/types'; -import { connectors as connectorsDefinition } from '../../../../lib/connectors/config'; +import { connectorsConfiguration } from '../../../../lib/connectors/config'; import * as i18n from './translations'; export interface Props { @@ -54,7 +54,7 @@ const ConnectorsDropdownComponent: React.FC = ({ inputDisplay: ( <> {connector.name} diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.tsx index a970fe895eb716..591ca01bcae861 100644 --- a/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.tsx +++ b/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.tsx @@ -31,7 +31,13 @@ import { import { ActionConnectorTableItem } from '../../../../../../triggers_actions_ui/public/types'; import { getCaseUrl } from '../../../../components/link_to'; import { useGetUrlSearch } from '../../../../components/navigation/use_get_url_search'; -import { CCMapsCombinedActionAttributes } from '../../../../containers/case/configure/types'; +import { + ClosureType, + CasesConfigurationMapping, + CCMapsCombinedActionAttributes, +} from '../../../../containers/case/configure/types'; +import { connectorsConfiguration } from '../../../../lib/connectors/config'; + import { Connectors } from '../configure_cases/connectors'; import { ClosureOptions } from '../configure_cases/closure_options'; import { Mapping } from '../configure_cases/mapping'; @@ -54,16 +60,14 @@ const FormWrapper = styled.div` `} `; -const actionTypes: ActionType[] = [ - { - id: '.servicenow', - name: 'ServiceNow', - enabled: true, - enabledInConfig: true, - enabledInLicense: true, - minimumLicenseRequired: 'platinum', - }, -]; +const initialState: State = { + connectorId: 'none', + closureType: 'close-by-user', + mapping: null, + currentConfiguration: { connectorId: 'none', closureType: 'close-by-user' }, +}; + +const actionTypes: ActionType[] = Object.values(connectorsConfiguration); interface ConfigureCasesComponentProps { userCanCrud: boolean; From aadf8bfad0b33681459d39a4626d750e5c29febb Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 21 Apr 2020 21:32:42 +0300 Subject: [PATCH 11/33] Refactor connectors flyout --- x-pack/plugins/actions/common/types.ts | 4 + .../components/connector_flyout/index.tsx | 146 +++++++++++ .../siem/public/lib/connectors/config.ts | 4 +- .../lib/connectors/logos/servicenow.svg | 5 - .../lib/connectors/servicenow/flyout.tsx | 83 ++++++ .../lib/connectors/servicenow/index.tsx | 243 ++---------------- .../public/lib/connectors/servicenow/types.ts | 2 +- .../siem/public/lib/connectors/types.ts | 31 +++ .../siem/public/lib/connectors/utils.ts | 71 +++++ 9 files changed, 361 insertions(+), 228 deletions(-) create mode 100644 x-pack/plugins/siem/public/lib/connectors/components/connector_flyout/index.tsx delete mode 100755 x-pack/plugins/siem/public/lib/connectors/logos/servicenow.svg create mode 100644 x-pack/plugins/siem/public/lib/connectors/servicenow/flyout.tsx create mode 100644 x-pack/plugins/siem/public/lib/connectors/utils.ts diff --git a/x-pack/plugins/actions/common/types.ts b/x-pack/plugins/actions/common/types.ts index 49e8f3e80b14a0..f5e61fb0eaeadf 100644 --- a/x-pack/plugins/actions/common/types.ts +++ b/x-pack/plugins/actions/common/types.ts @@ -6,6 +6,10 @@ import { LicenseType } from '../../licensing/common/types'; +export { ConnectorPublicConfigurationType } from '../server/builtin_action_types/connectors/types'; +export { ServiceNowPublicConfigurationType } from '../server/builtin_action_types/connectors/servicenow/types'; +export { ServiceNowSecretConfigurationType } from '../server/builtin_action_types/connectors/servicenow/types'; + export interface ActionType { id: string; name: string; diff --git a/x-pack/plugins/siem/public/lib/connectors/components/connector_flyout/index.tsx b/x-pack/plugins/siem/public/lib/connectors/components/connector_flyout/index.tsx new file mode 100644 index 00000000000000..57c41a74a0ccf2 --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/components/connector_flyout/index.tsx @@ -0,0 +1,146 @@ +/* + * 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, ChangeEvent, 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 '../../../../../../../../plugins/triggers_actions_ui/public/types'; +import { FieldMapping } from '../../../../pages/case/components/configure_cases/field_mapping'; + +import { defaultMapping } from '../../config'; +import { CasesConfigurationMapping } from '../../../../containers/case/configure/types'; + +import * as i18n from '../../translations'; +import { ActionConnector, ConnectorFlyoutHOCProps } from '../../types'; + +export const withConnectorFlyout = ({ + ConnectorFormComponent, + 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, '')); + } + }, []); + + if (isEmpty(mapping)) { + editActionConfig('casesConfiguration', { + ...action.config.casesConfiguration, + mapping: defaultMapping, + }); + } + + const handleOnChangeActionConfig = useCallback( + (key: string, evt: ChangeEvent) => editActionConfig(key, evt.target.value), + [] + ); + + const handleOnBlurActionConfig = useCallback( + (key: string) => { + if (configKeysWithDefault.includes(key) && get(key, action.config) == null) { + editActionConfig(key, ''); + } + }, + [action.config] + ); + + const handleOnChangeSecretConfig = useCallback( + (key: string, value: string) => editActionSecrets(key, value), + [] + ); + + const handleOnBlurSecretConfig = useCallback( + (key: string) => { + if (secretKeys.includes(key) && get(key, action.secrets) == null) { + editActionSecrets(key, ''); + } + }, + [action.secrets] + ); + + const handleOnChangeMappingConfig = useCallback( + (newMapping: CasesConfigurationMapping[]) => + editActionConfig('casesConfiguration', { + ...action.config.casesConfiguration, + mapping: newMapping, + }), + [action.config] + ); + + return ( + <> + + + + + + + + + + + + + + + + + ); + }; + + return ConnectorFlyout; +}; diff --git a/x-pack/plugins/siem/public/lib/connectors/config.ts b/x-pack/plugins/siem/public/lib/connectors/config.ts index f0e0bf96bbc1bb..ddc557d57abd95 100644 --- a/x-pack/plugins/siem/public/lib/connectors/config.ts +++ b/x-pack/plugins/siem/public/lib/connectors/config.ts @@ -11,8 +11,8 @@ import { connector as serviceNowConnectorConfig } from './servicenow/config'; import { connector as resilientConnectorConfig } from './resilient/config'; export const connectorsConfiguration: Record = { - '.servicenow': { ...serviceNowConnectorConfig }, - '.resilient': { ...resilientConnectorConfig }, + '.servicenow': serviceNowConnectorConfig, + '.resilient': resilientConnectorConfig, }; export const defaultMapping: CasesConfigurationMapping[] = [ diff --git a/x-pack/plugins/siem/public/lib/connectors/logos/servicenow.svg b/x-pack/plugins/siem/public/lib/connectors/logos/servicenow.svg deleted file mode 100755 index dcd022a8dca18d..00000000000000 --- a/x-pack/plugins/siem/public/lib/connectors/logos/servicenow.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/x-pack/plugins/siem/public/lib/connectors/servicenow/flyout.tsx b/x-pack/plugins/siem/public/lib/connectors/servicenow/flyout.tsx new file mode 100644 index 00000000000000..6e881844088512 --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/servicenow/flyout.tsx @@ -0,0 +1,83 @@ +/* + * 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 { ServiceNowActionConnector } from './types'; +import { withConnectorFlyout } from '../components/connector_flyout'; + +const ServiceNowConnectorForm: React.FC> = ({ + errors, + action, + onChangeSecret, + onBlurSecret, +}) => { + const { username, password } = action.secrets; + const isUsernameInvalid: boolean = errors.username.length > 0 && username != null; + const isPasswordInvalid: boolean = errors.password.length > 0 && password != null; + + return ( + <> + + + + onChangeSecret('username', evt.target.value)} + onBlur={() => onBlurSecret('username')} + /> + + + + + + + + onChangeSecret('password', evt.target.value)} + onBlur={() => onBlurSecret('password')} + /> + + + + + ); +}; + +export const ServiceNowConnectorFlyout = withConnectorFlyout({ + ConnectorFormComponent: ServiceNowConnectorForm, + secretKeys: ['username', 'password'], +}); diff --git a/x-pack/plugins/siem/public/lib/connectors/servicenow/index.tsx b/x-pack/plugins/siem/public/lib/connectors/servicenow/index.tsx index c69e02627f89ca..4a2f08361a86af 100644 --- a/x-pack/plugins/siem/public/lib/connectors/servicenow/index.tsx +++ b/x-pack/plugins/siem/public/lib/connectors/servicenow/index.tsx @@ -3,245 +3,48 @@ * 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, ChangeEvent, useEffect } from 'react'; -import { - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiFieldPassword, - EuiSpacer, -} from '@elastic/eui'; -import { isEmpty, get } from 'lodash/fp'; +import React from 'react'; import { - ActionConnectorFieldsProps, - ActionTypeModel, ValidationResult, - ActionParamsProps, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../triggers_actions_ui/public/types'; -import { FieldMapping } from '../../../pages/case/components/configure_cases/field_mapping'; - -import * as i18n from './translations'; - -import { ServiceNowActionConnector } from './types'; -import { isUrlInvalid } from '../validators'; - -import { defaultMapping } from '../config'; -import { CasesConfigurationMapping } from '../../../containers/case/configure/types'; - import { connector } from './config'; +import { createActionType } from '../utils'; import logo from './logo.svg'; - -interface ServiceNowActionParams { - message: string; -} +import { ServiceNowActionConnector } from './types'; +import { ServiceNowConnectorFlyout } from './flyout'; +import * as i18n from './translations'; interface Errors { - apiUrl: string[]; username: string[]; password: string[]; } -export function getActionType(): ActionTypeModel { - return { - id: connector.id, - iconClass: logo, - selectMessage: i18n.SERVICENOW_DESC, - actionTypeTitle: connector.name, - validateConnector: (action: ServiceNowActionConnector): ValidationResult => { - const errors: Errors = { - apiUrl: [], - username: [], - password: [], - }; - - 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]; - } - - if (!action.secrets.username) { - errors.username = [...errors.username, i18n.USERNAME_REQUIRED]; - } - - if (!action.secrets.password) { - errors.password = [...errors.password, i18n.PASSWORD_REQUIRED]; - } - - return { errors }; - }, - validateParams: (actionParams: ServiceNowActionParams): ValidationResult => { - return { errors: {} }; - }, - actionConnectorFields: ServiceNowConnectorFields, - actionParamsFields: ServiceNowParamsFields, +const validateConnector = (action: ServiceNowActionConnector): ValidationResult => { + const errors: Errors = { + username: [], + password: [], }; -} - -const ServiceNowConnectorFields: React.FunctionComponent> = ({ 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 { username, password } = action.secrets; - - const isApiUrlInvalid: boolean = errors.apiUrl.length > 0 && apiUrl != null; - const isUsernameInvalid: boolean = errors.username.length > 0 && username != null; - const isPasswordInvalid: boolean = errors.password.length > 0 && password != 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)) { - editActionSecrets('username', ''); - editActionSecrets('password', ''); - } - }, []); - if (isEmpty(mapping)) { - editActionConfig('casesConfiguration', { - ...action.config.casesConfiguration, - mapping: defaultMapping, - }); + if (!action.secrets.username) { + errors.username = [...errors.username, i18n.USERNAME_REQUIRED]; } - const handleOnChangeActionConfig = useCallback( - (key: string, evt: ChangeEvent) => editActionConfig(key, evt.target.value), - [] - ); - - const handleOnBlurActionConfig = useCallback( - (key: string) => { - if (key === 'apiUrl' && action.config[key] == null) { - editActionConfig(key, ''); - } - }, - [action.config] - ); - - const handleOnChangeSecretConfig = useCallback( - (key: string, evt: ChangeEvent) => editActionSecrets(key, evt.target.value), - [] - ); - - const handleOnBlurSecretConfig = useCallback( - (key: string) => { - if (['username', 'password'].includes(key) && get(key, action.secrets) == null) { - editActionSecrets(key, ''); - } - }, - [action.secrets] - ); - - const handleOnChangeMappingConfig = useCallback( - (newMapping: CasesConfigurationMapping[]) => - editActionConfig('casesConfiguration', { - ...action.config.casesConfiguration, - mapping: newMapping, - }), - [action.config] - ); + if (!action.secrets.password) { + errors.password = [...errors.password, i18n.PASSWORD_REQUIRED]; + } - return ( - <> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); + return { errors }; }; -const ServiceNowParamsFields: React.FunctionComponent> = ({ actionParams, editAction, index, errors }) => { - return null; -}; +export const getActionType = createActionType({ + id: connector.id, + iconClass: logo, + selectMessage: i18n.SERVICENOW_DESC, + actionTypeTitle: connector.name, + validateConnector, + actionConnectorFields: ServiceNowConnectorFlyout, +}); diff --git a/x-pack/plugins/siem/public/lib/connectors/servicenow/types.ts b/x-pack/plugins/siem/public/lib/connectors/servicenow/types.ts index 5427ddb0afbb35..e59d4375561c2a 100644 --- a/x-pack/plugins/siem/public/lib/connectors/servicenow/types.ts +++ b/x-pack/plugins/siem/public/lib/connectors/servicenow/types.ts @@ -10,7 +10,7 @@ import { ServiceNowPublicConfigurationType, ServiceNowSecretConfigurationType, -} from '../../../../../../../plugins/actions/server/builtin_action_types/connectors/servicenow/types'; +} from '../../../../../../../plugins/actions/common'; export interface ServiceNowActionConnector { config: ServiceNowPublicConfigurationType; diff --git a/x-pack/plugins/siem/public/lib/connectors/types.ts b/x-pack/plugins/siem/public/lib/connectors/types.ts index 792465341724d1..ce66870e852340 100644 --- a/x-pack/plugins/siem/public/lib/connectors/types.ts +++ b/x-pack/plugins/siem/public/lib/connectors/types.ts @@ -5,6 +5,9 @@ */ import { ActionType } from '../../../../../../plugins/triggers_actions_ui/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ActionConnectorFieldsProps } from '../../../../../../plugins/triggers_actions_ui/public/types'; +import { ConnectorPublicConfigurationType } from '../../../../../../plugins/actions/common'; import { ConfigType, @@ -19,3 +22,31 @@ export interface ServiceNowActionConnector { export interface Connector extends ActionType { logo: string; } + +export interface ActionConnector { + config: ConnectorPublicConfigurationType; + secrets: {}; +} + +export interface ActionConnectorParams { + message: string; +} + +export interface ActionConnectorValidationErrors { + apiUrl: string[]; +} + +export type Optional = Omit & Partial; + +export interface ConnectorFlyoutFormProps { + errors: { [key: string]: string[] }; + action: T; + onChangeSecret: (key: string, value: string) => void; + onBlurSecret: (key: string) => void; +} + +export interface ConnectorFlyoutHOCProps { + ConnectorFormComponent: React.FC>; + configKeys?: string[]; + secretKeys?: string[]; +} diff --git a/x-pack/plugins/siem/public/lib/connectors/utils.ts b/x-pack/plugins/siem/public/lib/connectors/utils.ts new file mode 100644 index 00000000000000..b6cf53fdca6ce4 --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/utils.ts @@ -0,0 +1,71 @@ +/* + * 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 { + ActionTypeModel, + ValidationResult, + ActionParamsProps, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../../plugins/triggers_actions_ui/public/types'; + +import { + ActionConnector, + ActionConnectorParams, + ActionConnectorValidationErrors, + Optional, +} from './types'; +import { isUrlInvalid } from './validators'; + +import * as i18n from './translations'; + +export const createActionType = ({ + id, + actionTypeTitle, + selectMessage, + iconClass, + validateConnector, + validateParams = connectorParamsValidator, + actionConnectorFields, + actionParamsFields = ConnectorParamsFields, +}: 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 ConnectorParamsFields: React.FunctionComponent> = ({ + actionParams, + editAction, + index, + errors, +}) => { + return null; +}; + +const connectorParamsValidator = (actionParams: ActionConnectorParams): ValidationResult => { + return { errors: {} }; +}; From 3352ca894234dabaa66a544262f2ae43873c63b3 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 22 Apr 2020 14:59:53 +0300 Subject: [PATCH 12/33] Adopt Jira --- .../builtin_action_types/connectors/index.ts | 1 + .../connectors/jira/api.ts | 103 +++++++ .../connectors/{resilient => jira}/config.ts | 2 +- .../connectors/jira/index.ts | 24 ++ .../connectors/{resilient => jira}/schema.ts | 14 +- .../connectors/jira/service.ts | 145 +++++++++ .../{resilient => jira}/translations.ts | 4 +- .../connectors/jira/types.ts | 11 + .../{resilient => jira}/validators.ts | 0 .../connectors/resilient/service.ts | 70 ----- .../connectors/resilient/types.ts | 11 - .../connectors/servicenow/index.ts | 5 + .../builtin_action_types/connectors/types.ts | 12 +- .../builtin_action_types/connectors/utils.ts | 13 +- .../server/builtin_action_types/index.ts | 3 +- x-pack/plugins/case/common/api/cases/case.ts | 11 +- x-pack/plugins/case/common/constants.ts | 2 + .../api/cases/configure/get_connectors.ts | 11 +- .../components/connector_flyout/index.tsx | 10 +- .../siem/public/lib/connectors/config.ts | 4 +- .../siem/public/lib/connectors/index.ts | 2 +- .../connectors/{resilient => jira}/config.ts | 6 +- .../public/lib/connectors/jira/flyout.tsx | 110 +++++++ .../siem/public/lib/connectors/jira/index.tsx | 54 ++++ .../connectors/{resilient => jira}/logo.svg | 0 .../lib/connectors/jira/translations.ts | 28 ++ .../connectors/{resilient => jira}/types.ts | 12 +- .../public/lib/connectors/resilient/index.tsx | 278 ------------------ .../lib/connectors/resilient/translations.ts | 34 --- .../lib/connectors/servicenow/flyout.tsx | 4 +- .../lib/connectors/servicenow/index.tsx | 2 - .../public/lib/connectors/translations.ts | 27 +- .../siem/public/lib/connectors/types.ts | 2 + 33 files changed, 551 insertions(+), 464 deletions(-) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/connectors/jira/api.ts rename x-pack/plugins/actions/server/builtin_action_types/connectors/{resilient => jira}/config.ts (95%) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/connectors/jira/index.ts rename x-pack/plugins/actions/server/builtin_action_types/connectors/{resilient => jira}/schema.ts (51%) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/connectors/jira/service.ts rename x-pack/plugins/actions/server/builtin_action_types/connectors/{resilient => jira}/translations.ts (71%) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/connectors/jira/types.ts rename x-pack/plugins/actions/server/builtin_action_types/connectors/{resilient => jira}/validators.ts (100%) delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/connectors/resilient/service.ts delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/connectors/resilient/types.ts rename x-pack/plugins/siem/public/lib/connectors/{resilient => jira}/config.ts (83%) create mode 100644 x-pack/plugins/siem/public/lib/connectors/jira/flyout.tsx create mode 100644 x-pack/plugins/siem/public/lib/connectors/jira/index.tsx rename x-pack/plugins/siem/public/lib/connectors/{resilient => jira}/logo.svg (100%) create mode 100644 x-pack/plugins/siem/public/lib/connectors/jira/translations.ts rename x-pack/plugins/siem/public/lib/connectors/{resilient => jira}/types.ts (63%) delete mode 100644 x-pack/plugins/siem/public/lib/connectors/resilient/index.tsx delete mode 100644 x-pack/plugins/siem/public/lib/connectors/resilient/translations.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/index.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/index.ts index 9ecf39b8922e42..a6ca6ea199c306 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/index.ts @@ -5,3 +5,4 @@ */ export { connector as getServiceNowConnector } from './servicenow'; +export { connector as getJiraConnector } from './jira'; diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/api.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/api.ts new file mode 100644 index 00000000000000..aa3ac4ccadaf9f --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/api.ts @@ -0,0 +1,103 @@ +/* + * 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 { zipWith } from 'lodash'; + +import { + ConnectorApi, + ExternalServiceParams, + ExternalServiceCommentResponse, + Comment, + PushToServiceResponse, + ConnectorApiHandlerArgs, +} from '../types'; +import { prepareFieldsForTransformation, transformFields, transformComments } from '../utils'; + +const handshakeHandler = async ({ + externalService, + mapping, + params, +}: ConnectorApiHandlerArgs) => {}; +const getIncidentHandler = async ({ + externalService, + mapping, + params, +}: ConnectorApiHandlerArgs) => {}; + +const pushToServiceHandler = async ({ + externalService, + mapping, + params, +}: ConnectorApiHandlerArgs): 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({ + params, + 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']); + + // Create comments sequentially. + const promises = comments.reduce(async (prevPromise, currentComment) => { + const totalComments = await prevPromise; + const comment = await externalService.createComment({ + incidentId: res.id, + comment: currentComment, + field: mapping.get('comments').target, + }); + return [...totalComments, comment]; + }, Promise.resolve([] as ExternalServiceCommentResponse[])); + + const createdComments = await promises; + + const zippedComments: ExternalServiceCommentResponse[] = zipWith( + commentsTransformed, + createdComments, + (a: Comment, b: ExternalServiceCommentResponse) => ({ + commentId: a.commentId, + pushedDate: b.pushedDate, + }) + ); + + res.comments = [...zippedComments]; + } + + return res; +}; + +export const api: ConnectorApi = { + handshake: handshakeHandler, + pushToService: pushToServiceHandler, + getIncident: getIncidentHandler, +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/resilient/config.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/config.ts similarity index 95% rename from x-pack/plugins/actions/server/builtin_action_types/connectors/resilient/config.ts rename to x-pack/plugins/actions/server/builtin_action_types/connectors/jira/config.ts index a090fa40ee8ed4..e205a4f3b079bc 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/resilient/config.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/config.ts @@ -8,6 +8,6 @@ import { ConnectorConfiguration } from '../types'; import * as i18n from './translations'; export const config: ConnectorConfiguration = { - id: '.resilient', + id: '.jira', name: i18n.NAME, }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/index.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/index.ts new file mode 100644 index 00000000000000..7af531ffd8a18e --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/index.ts @@ -0,0 +1,24 @@ +/* + * 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 { createConnector } from '../utils'; + +import { api } from './api'; +import { config } from './config'; +import { validate } from './validators'; +import { createExternalService } from './service'; +import { JiraSecretConfiguration, JiraPublicConfiguration } from './schema'; + +export const connector = createConnector({ + api, + config, + validate, + createExternalService, + validationSchema: { + config: JiraPublicConfiguration, + secrets: JiraSecretConfiguration, + }, +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/resilient/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/schema.ts similarity index 51% rename from x-pack/plugins/actions/server/builtin_action_types/connectors/resilient/schema.ts rename to x-pack/plugins/actions/server/builtin_action_types/connectors/jira/schema.ts index 936a17a6a1fedd..26ba62ca2deefe 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/resilient/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/schema.ts @@ -7,16 +7,16 @@ import { schema } from '@kbn/config-schema'; import { ConnectorPublicConfiguration } from '../schema'; -export const ResilientPublicConfiguration = { - orgId: schema.number(), +export const JiraPublicConfiguration = { + projectKey: schema.string(), ...ConnectorPublicConfiguration, }; -export const ResilientPublicConfigurationSchema = schema.object(ResilientPublicConfiguration); +export const JiraPublicConfigurationSchema = schema.object(JiraPublicConfiguration); -export const ResilientSecretConfiguration = { - apiKey: schema.string(), - apiSecret: schema.string(), +export const JiraSecretConfiguration = { + email: schema.string(), + apiToken: schema.string(), }; -export const ResilientSecretConfigurationSchema = schema.object(ResilientSecretConfiguration); +export const JiraSecretConfigurationSchema = schema.object(JiraSecretConfiguration); diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/service.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/service.ts new file mode 100644 index 00000000000000..682b26f94e1226 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/service.ts @@ -0,0 +1,145 @@ +/* + * 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 axios from 'axios'; + +import { ExternalServiceCredential, ExternalService, ExternalServiceParams } from '../types'; +import { JiraPublicConfigurationType, JiraSecretConfigurationType } from './types'; + +import * as i18n from './translations'; +import { getErrorMessage, request } from '../utils'; + +const VERSION = '2'; +const BASE_URL = `rest/api/${VERSION}`; +const INCIDENT_URL = `issue`; +const COMMENT_URL = `comment`; + +const VIEW_INCIDENT_URL = `browse`; + +export const createExternalService = ({ + config, + secrets, +}: ExternalServiceCredential): ExternalService => { + const { apiUrl: url, projectKey } = config as JiraPublicConfigurationType; + const { apiToken, email } = secrets as JiraSecretConfigurationType; + + if (!url || !apiToken || !email) { + throw Error(`[Action]${i18n.NAME}: Wrong configuration.`); + } + + const incidentUrl = `${url}/${BASE_URL}/${INCIDENT_URL}`; + const commentUrl = `${incidentUrl}/{issueId}/${COMMENT_URL}`; + const axiosInstance = axios.create({ + auth: { username: email, password: apiToken }, + }); + + const getIncidentViewURL = (key: string) => { + return `${url}/${VIEW_INCIDENT_URL}/${key}`; + }; + + const getCommentsURL = (issueId: string) => { + return commentUrl.replace('{issueId}', issueId); + }; + + const getIncident = async (id: string) => { + try { + const res = await request({ + axios: axiosInstance, + url: `${incidentUrl}/${id}`, + }); + + return { ...res.data }; + } catch (error) { + throw new Error( + getErrorMessage(i18n.NAME, `Unable to get incident with id ${id}. Error: ${error.message}`) + ); + } + }; + + const createIncident = async ({ incident }: ExternalServiceParams) => { + try { + const res = await request({ + axios: axiosInstance, + url: `${incidentUrl}`, + method: 'post', + data: { + fields: { ...incident, project: { key: projectKey }, issuetype: { name: 'Task' } }, + }, + }); + + const updatedIncident = await getIncident(res.data.id); + + return { + title: updatedIncident.key, + id: updatedIncident.id, + pushedDate: new Date(updatedIncident.fields.created).toISOString(), + url: getIncidentViewURL(updatedIncident.key), + }; + } catch (error) { + throw new Error( + getErrorMessage(i18n.NAME, `Unable to create incident. Error: ${error.message}`) + ); + } + }; + + const updateIncident = async ({ incidentId, incident }: ExternalServiceParams) => { + try { + await request({ + axios: axiosInstance, + method: 'put', + url: `${incidentUrl}/${incidentId}`, + data: { fields: { ...incident } }, + }); + + const updatedIncident = await getIncident(incidentId); + + return { + title: updatedIncident.key, + id: updatedIncident.id, + pushedDate: new Date(updatedIncident.fields.updated).toISOString(), + url: getIncidentViewURL(updatedIncident.key), + }; + } catch (error) { + throw new Error( + getErrorMessage( + i18n.NAME, + `Unable to update incident with id ${incidentId}. Error: ${error.message}` + ) + ); + } + }; + + const createComment = async ({ incidentId, comment, field }: ExternalServiceParams) => { + try { + const res = await request({ + axios: axiosInstance, + method: 'post', + url: getCommentsURL(incidentId), + data: { body: comment.comment }, + }); + + return { + commentId: comment.commentId, + externalCommentId: res.data.id, + pushedDate: new Date(res.data.created).toISOString(), + }; + } catch (error) { + throw new Error( + getErrorMessage( + i18n.NAME, + `Unable to create comment at incident with id ${incidentId}. Error: ${error.message}` + ) + ); + } + }; + + return { + getIncident, + createIncident, + updateIncident, + createComment, + }; +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/resilient/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/translations.ts similarity index 71% rename from x-pack/plugins/actions/server/builtin_action_types/connectors/resilient/translations.ts rename to x-pack/plugins/actions/server/builtin_action_types/connectors/jira/translations.ts index 10c1138cf0ec42..88e5eccd57305a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/resilient/translations.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/translations.ts @@ -6,6 +6,6 @@ import { i18n } from '@kbn/i18n'; -export const NAME = i18n.translate('xpack.actions.builtin.resilientTitle', { - defaultMessage: 'IBM Resilient', +export const NAME = i18n.translate('xpack.actions.builtin.jiraTitle', { + defaultMessage: 'Jira', }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/types.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/types.ts new file mode 100644 index 00000000000000..0051ef6c70b2c6 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/types.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TypeOf } from '@kbn/config-schema'; +import { JiraPublicConfigurationSchema, JiraSecretConfigurationSchema } from './schema'; + +export type JiraPublicConfigurationType = TypeOf; +export type JiraSecretConfigurationType = TypeOf; diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/resilient/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/validators.ts similarity index 100% rename from x-pack/plugins/actions/server/builtin_action_types/connectors/resilient/validators.ts rename to x-pack/plugins/actions/server/builtin_action_types/connectors/jira/validators.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/resilient/service.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/resilient/service.ts deleted file mode 100644 index 29473942ed6914..00000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/resilient/service.ts +++ /dev/null @@ -1,70 +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 axios from 'axios'; - -import { ExternalServiceCredential, ExternalService, ExternalServiceParams } from '../types'; -import { addTimeZoneToDate, patch, request, getErrorMessage } from '../utils'; -import { ResilientPublicConfigurationType, ResilientSecretConfigurationType } from './types'; - -import * as i18n from './translations'; - -const BASE_URL = 'rest/orgs/'; -const INCIDENT_URL = `incidents`; -const COMMENT_URL = `tasks`; - -const VIEW_INCIDENT_URL = `#incidents`; - -export const createExternalService = ({ - config, - secrets, -}: ExternalServiceCredential): ExternalService => { - const { apiUrl: url, orgId } = config as ResilientPublicConfigurationType; - const { apiKey, apiSecret } = secrets as ResilientSecretConfigurationType; - let apiKeyHandle; - - if (!url || !apiKey || !apiSecret) { - throw Error(`[Action]${i18n.NAME}: Wrong configuration.`); - } - - const incidentUrl = `${url}/${BASE_URL}/${orgId}/${INCIDENT_URL}`; - const commentUrl = `${url}/${BASE_URL}/${orgId}/${COMMENT_URL}`; - const sessionUrl = `${url}/rest/session`; - const axiosInstance = axios.create({ - auth: { username: apiKey, password: apiSecret }, - }); - - const getIncidentViewURL = (id: string) => { - return `${url}/${VIEW_INCIDENT_URL}/${id}`; - }; - - const getApiKeyHandle = async (): Promise => { - try { - const res = await request({ - axios: axiosInstance, - url: sessionUrl, - }); - - return res.data.api_key_handle; - } catch (error) { - throw new Error(getErrorMessage(i18n.NAME, `Unable to authenticate user`)); - } - }; - const getIncident = async (id: string) => {}; - - const createIncident = async ({ incident }: ExternalServiceParams) => {}; - - const updateIncident = async ({ incidentId, incident }: ExternalServiceParams) => {}; - - const createComment = async ({ incidentId, comment, field }: ExternalServiceParams) => {}; - - return { - getIncident, - createIncident, - updateIncident, - createComment, - }; -}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/resilient/types.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/resilient/types.ts deleted file mode 100644 index 29c02c183dbf83..00000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/resilient/types.ts +++ /dev/null @@ -1,11 +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 { ResilientPublicConfigurationSchema, ResilientSecretConfigurationSchema } from './schema'; - -export type ResilientPublicConfigurationType = TypeOf; -export type ResilientSecretConfigurationType = TypeOf; diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/index.ts index 3b4704a77afb1f..473a3ed065dfb0 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/index.ts @@ -10,10 +10,15 @@ import { api } from './api'; import { config } from './config'; import { validate } from './validators'; import { createExternalService } from './service'; +import { ConnectorPublicConfiguration, ConnectorSecretConfiguration } from '../schema'; export const connector = createConnector({ api, config, validate, createExternalService, + validationSchema: { + config: ConnectorPublicConfiguration, + secrets: ConnectorSecretConfiguration, + }, }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/types.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/types.ts index 5465f918592b7c..ecbbd6466a7272 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/types.ts @@ -47,14 +47,8 @@ export interface ExternalServiceCredential { } export interface ConnectorValidation { - config: ( - configurationUtilities: ActionsConfigurationUtilities, - configObject: ConnectorPublicConfigurationType - ) => void; - secrets: ( - configurationUtilities: ActionsConfigurationUtilities, - secrets: ConnectorSecretConfigurationType - ) => void; + config: (configurationUtilities: ActionsConfigurationUtilities, configObject: any) => void; + secrets: (configurationUtilities: ActionsConfigurationUtilities, secrets: any) => void; } export interface ExternalServiceCaseResponse { @@ -67,6 +61,7 @@ export interface ExternalServiceCaseResponse { export interface ExternalServiceCommentResponse { commentId: string; pushedDate: string; + externalCommentId?: string; } export interface ExternalServiceParams { @@ -104,6 +99,7 @@ export interface CreateConnectorBasicArgs { export interface CreateConnectorArgs extends CreateConnectorBasicArgs { config: ConnectorConfiguration; validate: ConnectorValidation; + validationSchema: { config: any; secrets: any }; } export interface CreateActionTypeArgs { diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/utils.ts index 2a4285925b6be7..8b80b273b1ceb4 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/utils.ts @@ -10,11 +10,7 @@ import { AxiosInstance, Method, AxiosResponse } from 'axios'; import { ActionTypeExecutorOptions, ActionTypeExecutorResult, ActionType } from '../../types'; -import { - ConnectorPublicConfiguration, - ConnectorSecretConfiguration, - ExecutorParamsSchema, -} from './schema'; +import { ExecutorParamsSchema } from './schema'; import { CreateConnectorArgs, @@ -110,6 +106,7 @@ export const createConnector = ({ config, validate, createExternalService, + validationSchema, }: CreateConnectorArgs) => { return ({ configurationUtilities, @@ -119,10 +116,10 @@ export const createConnector = ({ name: config.name, minimumLicenseRequired: 'platinum', validate: { - config: schema.object(ConnectorPublicConfiguration, { + config: schema.object(validationSchema.config, { validate: curry(validate.config)(configurationUtilities), }), - secrets: schema.object(ConnectorSecretConfiguration, { + secrets: schema.object(validationSchema.secrets, { validate: curry(validate.secrets)(configurationUtilities), }), params: ExecutorParamsSchema, @@ -134,7 +131,7 @@ export const createConnector = ({ export const throwIfNotAlive = ( status: number, contentType: string, - validStatusCodes: number[] = [200, 201] + validStatusCodes: number[] = [200, 201, 204] ) => { if (!validStatusCodes.includes(status) || !contentType.includes('application/json')) { throw new Error('Instance is not alive.'); diff --git a/x-pack/plugins/actions/server/builtin_action_types/index.ts b/x-pack/plugins/actions/server/builtin_action_types/index.ts index 5e092efdef8f7d..cc6177f2f5be6d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/index.ts @@ -16,7 +16,7 @@ import { getActionType as getSlackActionType } from './slack'; import { getActionType as getWebhookActionType } from './webhook'; // Connectors -import { getServiceNowConnector } from './connectors'; +import { getServiceNowConnector, getJiraConnector } from './connectors'; export function registerBuiltInActionTypes({ actionsConfigUtils: configurationUtilities, @@ -36,4 +36,5 @@ export function registerBuiltInActionTypes({ // Connectors actionTypeRegistry.register(getServiceNowConnector({ configurationUtilities })); + actionTypeRegistry.register(getJiraConnector({ configurationUtilities })); } diff --git a/x-pack/plugins/case/common/api/cases/case.ts b/x-pack/plugins/case/common/api/cases/case.ts index 47a00f5c96c65d..a1ed3a957d68c7 100644 --- a/x-pack/plugins/case/common/api/cases/case.ts +++ b/x-pack/plugins/case/common/api/cases/case.ts @@ -152,10 +152,13 @@ export const ServiceConnectorCaseResponseRt = rt.intersection([ }), rt.partial({ comments: rt.array( - rt.type({ - commentId: rt.string, - pushedDate: rt.string, - }) + rt.intersection([ + rt.type({ + commentId: rt.string, + pushedDate: rt.string, + }), + rt.partial({ externalCommentId: rt.string }), + ]) ), }), ]); diff --git a/x-pack/plugins/case/common/constants.ts b/x-pack/plugins/case/common/constants.ts index dcfa46bfa60191..855a5c3d63507a 100644 --- a/x-pack/plugins/case/common/constants.ts +++ b/x-pack/plugins/case/common/constants.ts @@ -27,3 +27,5 @@ export const CASE_USER_ACTIONS_URL = `${CASE_DETAILS_URL}/user_actions`; export const ACTION_URL = '/api/action'; export const ACTION_TYPES_URL = '/api/action/types'; + +export const SUPPORTED_CONNECTORS = ['.servicenow', '.jira']; 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 00575655d4c426..43167d56de0159 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 @@ -8,14 +8,15 @@ import Boom from 'boom'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; -import { CASE_CONFIGURE_CONNECTORS_URL } from '../../../../../common/constants'; +import { + CASE_CONFIGURE_CONNECTORS_URL, + SUPPORTED_CONNECTORS, +} from '../../../../../common/constants'; /* * Be aware that this api will only return 20 connectors */ -const CASE_SERVICE_NOW_ACTION = '.servicenow'; - export function initCaseConfigureGetActionConnector({ caseService, router }: RouteDeps) { router.get( { @@ -30,8 +31,8 @@ export function initCaseConfigureGetActionConnector({ caseService, router }: Rou throw Boom.notFound('Action client have not been found'); } - const results = (await actionsClient.getAll()).filter( - action => action.actionTypeId === CASE_SERVICE_NOW_ACTION + const results = (await actionsClient.getAll()).filter(action => + SUPPORTED_CONNECTORS.includes(action.actionTypeId) ); return response.ok({ body: results }); } catch (error) { diff --git a/x-pack/plugins/siem/public/lib/connectors/components/connector_flyout/index.tsx b/x-pack/plugins/siem/public/lib/connectors/components/connector_flyout/index.tsx index 57c41a74a0ccf2..2e8d42d09f9d7a 100644 --- a/x-pack/plugins/siem/public/lib/connectors/components/connector_flyout/index.tsx +++ b/x-pack/plugins/siem/public/lib/connectors/components/connector_flyout/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, ChangeEvent, useEffect } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSpacer } from '@elastic/eui'; import { isEmpty, get } from 'lodash/fp'; @@ -61,7 +61,7 @@ export const withConnectorFlyout = ({ } const handleOnChangeActionConfig = useCallback( - (key: string, evt: ChangeEvent) => editActionConfig(key, evt.target.value), + (key: string, value: string) => editActionConfig(key, value), [] ); @@ -114,8 +114,8 @@ export const withConnectorFlyout = ({ name="apiUrl" value={apiUrl || ''} // Needed to prevent uncontrolled input error when value is undefined data-test-subj="apiUrlFromInput" - placeholder="https://.service-now.com" - onChange={handleOnChangeActionConfig.bind(null, 'apiUrl')} + placeholder="https://" + onChange={evt => handleOnChangeActionConfig('apiUrl', evt.target.value)} onBlur={handleOnBlurActionConfig.bind(null, 'apiUrl')} /> @@ -127,6 +127,8 @@ export const withConnectorFlyout = ({ action={action} onChangeSecret={handleOnChangeSecretConfig} onBlurSecret={handleOnBlurSecretConfig} + onChangeConfig={handleOnChangeActionConfig} + onBlurConfig={handleOnBlurActionConfig} /> diff --git a/x-pack/plugins/siem/public/lib/connectors/config.ts b/x-pack/plugins/siem/public/lib/connectors/config.ts index ddc557d57abd95..98473e49622a95 100644 --- a/x-pack/plugins/siem/public/lib/connectors/config.ts +++ b/x-pack/plugins/siem/public/lib/connectors/config.ts @@ -8,11 +8,11 @@ import { CasesConfigurationMapping } from '../../containers/case/configure/types import { Connector } from './types'; import { connector as serviceNowConnectorConfig } from './servicenow/config'; -import { connector as resilientConnectorConfig } from './resilient/config'; +import { connector as jiraConnectorConfig } from './jira/config'; export const connectorsConfiguration: Record = { '.servicenow': serviceNowConnectorConfig, - '.resilient': resilientConnectorConfig, + '.jira': jiraConnectorConfig, }; export const defaultMapping: CasesConfigurationMapping[] = [ diff --git a/x-pack/plugins/siem/public/lib/connectors/index.ts b/x-pack/plugins/siem/public/lib/connectors/index.ts index d95e73dfeeced9..2ce61bef49c5ed 100644 --- a/x-pack/plugins/siem/public/lib/connectors/index.ts +++ b/x-pack/plugins/siem/public/lib/connectors/index.ts @@ -5,4 +5,4 @@ */ export { getActionType as serviceNowActionType } from './servicenow'; -export { getActionType as resilientActionType } from './resilient'; +export { getActionType as jiraActionType } from './jira'; diff --git a/x-pack/plugins/siem/public/lib/connectors/resilient/config.ts b/x-pack/plugins/siem/public/lib/connectors/jira/config.ts similarity index 83% rename from x-pack/plugins/siem/public/lib/connectors/resilient/config.ts rename to x-pack/plugins/siem/public/lib/connectors/jira/config.ts index 950ad96f4ed7f7..42bd1b9cdc1915 100644 --- a/x-pack/plugins/siem/public/lib/connectors/resilient/config.ts +++ b/x-pack/plugins/siem/public/lib/connectors/jira/config.ts @@ -6,12 +6,12 @@ import { Connector } from '../types'; -import { RESILIENT_TITLE } from './translations'; +import { JIRA_TITLE } from './translations'; import logo from './logo.svg'; export const connector: Connector = { - id: '.resilient', - name: RESILIENT_TITLE, + id: '.jira', + name: JIRA_TITLE, logo, enabled: true, enabledInConfig: true, diff --git a/x-pack/plugins/siem/public/lib/connectors/jira/flyout.tsx b/x-pack/plugins/siem/public/lib/connectors/jira/flyout.tsx new file mode 100644 index 00000000000000..482808fca53b1a --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/jira/flyout.tsx @@ -0,0 +1,110 @@ +/* + * 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 { JiraActionConnector } from './types'; +import { withConnectorFlyout } from '../components/connector_flyout'; + +const JiraConnectorForm: React.FC> = ({ + errors, + action, + onChangeSecret, + onBlurSecret, + onChangeConfig, + onBlurConfig, +}) => { + const { projectKey } = action.config; + const { email, apiToken } = action.secrets; + const isProjectKeyInvalid: boolean = errors.projectKey.length > 0 && projectKey != null; + const isEmailInvalid: boolean = errors.email.length > 0 && email != null; + const isApiTokenInvalid: boolean = errors.apiToken.length > 0 && apiToken != null; + + return ( + <> + + + + onChangeConfig('projectKey', evt.target.value)} + onBlur={() => onBlurConfig('projectKey')} + /> + + + + + + + + onChangeSecret('email', evt.target.value)} + onBlur={() => onBlurSecret('email')} + /> + + + + + + + + onChangeSecret('apiToken', evt.target.value)} + onBlur={() => onBlurSecret('apiToken')} + /> + + + + + ); +}; + +export const JiraConnectorFlyout = withConnectorFlyout({ + ConnectorFormComponent: JiraConnectorForm, + secretKeys: ['email', 'apiToken'], + configKeys: ['projectKey'], +}); diff --git a/x-pack/plugins/siem/public/lib/connectors/jira/index.tsx b/x-pack/plugins/siem/public/lib/connectors/jira/index.tsx new file mode 100644 index 00000000000000..cb0143c6f1275f --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/jira/index.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + ValidationResult, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../../../plugins/triggers_actions_ui/public/types'; + +import { connector } from './config'; +import { createActionType } from '../utils'; +import logo from './logo.svg'; +import { JiraActionConnector } from './types'; +import { JiraConnectorFlyout } from './flyout'; +import * as i18n from './translations'; + +interface Errors { + projectKey: string[]; + email: string[]; + apiToken: string[]; +} + +const validateConnector = (action: JiraActionConnector): ValidationResult => { + const errors: Errors = { + projectKey: [], + email: [], + apiToken: [], + }; + + if (!action.config.projectKey) { + errors.projectKey = [...errors.projectKey, i18n.JIRA_PROJECT_KEY_REQUIRED]; + } + + if (!action.secrets.email) { + errors.email = [...errors.email, i18n.EMAIL_REQUIRED]; + } + + if (!action.secrets.apiToken) { + errors.apiToken = [...errors.apiToken, i18n.API_TOKEN_REQUIRED]; + } + + return { errors }; +}; + +export const getActionType = createActionType({ + id: connector.id, + iconClass: logo, + selectMessage: i18n.JIRA_DESC, + actionTypeTitle: connector.name, + validateConnector, + actionConnectorFields: JiraConnectorFlyout, +}); diff --git a/x-pack/plugins/siem/public/lib/connectors/resilient/logo.svg b/x-pack/plugins/siem/public/lib/connectors/jira/logo.svg similarity index 100% rename from x-pack/plugins/siem/public/lib/connectors/resilient/logo.svg rename to x-pack/plugins/siem/public/lib/connectors/jira/logo.svg diff --git a/x-pack/plugins/siem/public/lib/connectors/jira/translations.ts b/x-pack/plugins/siem/public/lib/connectors/jira/translations.ts new file mode 100644 index 00000000000000..751aaecdad9641 --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/jira/translations.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export * from '../translations'; + +export const JIRA_DESC = i18n.translate('xpack.siem.case.connectors.jira.selectMessageText', { + defaultMessage: 'Push or update SIEM case data to a new issue in Jira', +}); + +export const JIRA_TITLE = i18n.translate('xpack.siem.case.connectors.jira.actionTypeTitle', { + defaultMessage: 'Jira', +}); + +export const JIRA_PROJECT_KEY_LABEL = i18n.translate('xpack.siem.case.connectors.jira.projectKey', { + defaultMessage: 'Project key', +}); + +export const JIRA_PROJECT_KEY_REQUIRED = i18n.translate( + 'xpack.siem.case.connectors.jira.requiredProjectKeyTextField', + { + defaultMessage: 'Project key is required', + } +); diff --git a/x-pack/plugins/siem/public/lib/connectors/resilient/types.ts b/x-pack/plugins/siem/public/lib/connectors/jira/types.ts similarity index 63% rename from x-pack/plugins/siem/public/lib/connectors/resilient/types.ts rename to x-pack/plugins/siem/public/lib/connectors/jira/types.ts index 8c299b1ef379e2..d0732954bccd6b 100644 --- a/x-pack/plugins/siem/public/lib/connectors/resilient/types.ts +++ b/x-pack/plugins/siem/public/lib/connectors/jira/types.ts @@ -8,11 +8,11 @@ /* eslint-disable @kbn/eslint/no-restricted-paths */ import { - ResilientPublicConfigurationType, - ResilientSecretConfigurationType, -} from '../../../../../../../plugins/actions/server/builtin_action_types/connectors/resilient/types'; + JiraPublicConfigurationType, + JiraSecretConfigurationType, +} from '../../../../../../../plugins/actions/server/builtin_action_types/connectors/jira/types'; -export interface ResilientActionConnector { - config: ResilientPublicConfigurationType; - secrets: ResilientSecretConfigurationType; +export interface JiraActionConnector { + config: JiraPublicConfigurationType; + secrets: JiraSecretConfigurationType; } diff --git a/x-pack/plugins/siem/public/lib/connectors/resilient/index.tsx b/x-pack/plugins/siem/public/lib/connectors/resilient/index.tsx deleted file mode 100644 index 61d4e20e68e6e5..00000000000000 --- a/x-pack/plugins/siem/public/lib/connectors/resilient/index.tsx +++ /dev/null @@ -1,278 +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, ChangeEvent, useEffect } from 'react'; -import { - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiSpacer, - EuiFieldPassword, -} from '@elastic/eui'; - -import { isEmpty, get } from 'lodash/fp'; - -import { - ActionConnectorFieldsProps, - ActionTypeModel, - ValidationResult, - ActionParamsProps, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../../plugins/triggers_actions_ui/public/types'; - -import { FieldMapping } from '../../../pages/case/components/configure_cases/field_mapping'; - -import * as i18n from './translations'; - -import { ResilientActionConnector } from './types'; -import { isUrlInvalid } from '../validators'; - -import { defaultMapping } from '../config'; -import { CasesConfigurationMapping } from '../../../containers/case/configure/types'; - -import { connector } from './config'; - -interface ResilientActionParams { - message: string; -} - -interface Errors { - apiUrl: string[]; - orgId: string[]; - apiKey: string[]; - apiSecret: string[]; -} - -export function getActionType(): ActionTypeModel { - return { - id: connector.id, - iconClass: connector.logo, - selectMessage: i18n.RESILIENT_DESC, - actionTypeTitle: connector.name, - validateConnector: (action: ResilientActionConnector): ValidationResult => { - const errors: Errors = { - apiUrl: [], - orgId: [], - apiKey: [], - apiSecret: [], - }; - - 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]; - } - - if (!action.config.orgId) { - errors.orgId = [...errors.orgId, i18n.RESILIENT_ORG_ID_REQUIRED]; - } - - if (!action.secrets.apiKey) { - errors.apiKey = [...errors.apiKey, i18n.API_KEY_REQUIRED]; - } - - if (!action.secrets.apiSecret) { - errors.apiSecret = [...errors.apiSecret, i18n.API_SECRET_REQUIRED]; - } - - return { errors }; - }, - validateParams: (actionParams: ResilientActionParams): ValidationResult => { - return { errors: {} }; - }, - actionConnectorFields: ResilientConnectorFields, - actionParamsFields: ResilientParamsFields, - }; -} - -const ResilientConnectorFields: React.FunctionComponent> = ({ 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, orgId, casesConfiguration: { mapping = [] } = {} } = action.config; - const { apiKey, apiSecret } = action.secrets; - - const isApiUrlInvalid: boolean = errors.apiUrl.length > 0 && apiUrl != null; - const isOrgIdInvalid: boolean = errors.orgId.length > 0 && orgId != null; - const isApiKeyInvalid: boolean = errors.apiKey.length > 0 && apiKey != null; - const isApiSecretInvalid: boolean = errors.apiSecret.length > 0 && apiSecret != 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)) { - editActionSecrets('apiKey', ''); - editActionSecrets('apiSecret', ''); - } - }, []); - - if (isEmpty(mapping)) { - editActionConfig('casesConfiguration', { - ...action.config.casesConfiguration, - mapping: defaultMapping, - }); - } - - const handleOnChangeActionConfig = useCallback( - (key: string, evt: ChangeEvent) => editActionConfig(key, evt.target.value), - [] - ); - - const handleOnBlurActionConfig = useCallback( - (key: string) => { - if (['apiUrl', 'orgId'].includes(key) && get(key, action.config) == null) { - editActionConfig(key, ''); - } - }, - [action.config] - ); - - const handleOnChangeSecretConfig = useCallback( - (key: string, evt: ChangeEvent) => editActionSecrets(key, evt.target.value), - [] - ); - - const handleOnBlurSecretConfig = useCallback( - (key: string) => { - if (['apiKey', 'apiSecret'].includes(key) && get(key, action.secrets) == null) { - editActionSecrets(key, ''); - } - }, - [action.secrets] - ); - - const handleOnChangeMappingConfig = useCallback( - (newMapping: CasesConfigurationMapping[]) => - editActionConfig('casesConfiguration', { - ...action.config.casesConfiguration, - mapping: newMapping, - }), - [action.config] - ); - - return ( - <> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; - -const ResilientParamsFields: React.FunctionComponent> = ({ - actionParams, - editAction, - index, - errors, -}) => { - return null; -}; diff --git a/x-pack/plugins/siem/public/lib/connectors/resilient/translations.ts b/x-pack/plugins/siem/public/lib/connectors/resilient/translations.ts deleted file mode 100644 index e4dd8cd5400dfc..00000000000000 --- a/x-pack/plugins/siem/public/lib/connectors/resilient/translations.ts +++ /dev/null @@ -1,34 +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.siem.case.connectors.resilient.selectMessageText', - { - defaultMessage: 'Push or update IBM Resilient case data to a new incident in ServiceNow', - } -); - -export const RESILIENT_TITLE = i18n.translate( - 'xpack.siem.case.connectors.resilient.actionTypeTitle', - { - defaultMessage: 'IBM Resilient', - } -); - -export const RESILIENT_ORG_ID = i18n.translate('xpack.siem.case.connectors.resilient.orgId', { - defaultMessage: 'Organization ID', -}); - -export const RESILIENT_ORG_ID_REQUIRED = i18n.translate( - 'xpack.siem.case.connectors.common.requiredOrgIdTextField', - { - defaultMessage: 'Organization ID is required', - } -); diff --git a/x-pack/plugins/siem/public/lib/connectors/servicenow/flyout.tsx b/x-pack/plugins/siem/public/lib/connectors/servicenow/flyout.tsx index 6e881844088512..bcde802e7bd1e0 100644 --- a/x-pack/plugins/siem/public/lib/connectors/servicenow/flyout.tsx +++ b/x-pack/plugins/siem/public/lib/connectors/servicenow/flyout.tsx @@ -44,7 +44,7 @@ const ServiceNowConnectorForm: React.FC onChangeSecret('username', evt.target.value)} onBlur={() => onBlurSecret('username')} /> @@ -66,7 +66,7 @@ const ServiceNowConnectorForm: React.FC onChangeSecret('password', evt.target.value)} onBlur={() => onBlurSecret('password')} /> diff --git a/x-pack/plugins/siem/public/lib/connectors/servicenow/index.tsx b/x-pack/plugins/siem/public/lib/connectors/servicenow/index.tsx index 4a2f08361a86af..21ca6fc1e9c743 100644 --- a/x-pack/plugins/siem/public/lib/connectors/servicenow/index.tsx +++ b/x-pack/plugins/siem/public/lib/connectors/servicenow/index.tsx @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; - import { ValidationResult, // eslint-disable-next-line @kbn/eslint/no-restricted-paths diff --git a/x-pack/plugins/siem/public/lib/connectors/translations.ts b/x-pack/plugins/siem/public/lib/connectors/translations.ts index abd4c04c7e8bc3..b9c1d0fa2a17fa 100644 --- a/x-pack/plugins/siem/public/lib/connectors/translations.ts +++ b/x-pack/plugins/siem/public/lib/connectors/translations.ts @@ -55,30 +55,27 @@ export const PASSWORD_REQUIRED = i18n.translate( } ); -export const API_KEY_LABEL = i18n.translate( - 'xpack.siem.case.connectors.common.apiKeyTextFieldLabel', +export const API_TOKEN_LABEL = i18n.translate( + 'xpack.siem.case.connectors.common.apiTokenTextFieldLabel', { - defaultMessage: 'Api key', + defaultMessage: 'Api token', } ); -export const API_KEY_REQUIRED = i18n.translate( - 'xpack.siem.case.connectors.common.requiredApiKeyTextField', +export const API_TOKEN_REQUIRED = i18n.translate( + 'xpack.siem.case.connectors.common.requiredApiTokenTextField', { - defaultMessage: 'Api key is required', + defaultMessage: 'Api token is required', } ); -export const API_SECRET_LABEL = i18n.translate( - 'xpack.siem.case.connectors.common.apiSecretTextFieldLabel', - { - defaultMessage: 'Api secret', - } -); +export const EMAIL_LABEL = i18n.translate('xpack.siem.case.connectors.common.emailTextFieldLabel', { + defaultMessage: 'Email', +}); -export const API_SECRET_REQUIRED = i18n.translate( - 'xpack.siem.case.connectors.common.requiredApiSecretTextField', +export const EMAIL_REQUIRED = i18n.translate( + 'xpack.siem.case.connectors.common.requiredEmailTextField', { - defaultMessage: 'Api secret is required', + defaultMessage: 'Email is required', } ); diff --git a/x-pack/plugins/siem/public/lib/connectors/types.ts b/x-pack/plugins/siem/public/lib/connectors/types.ts index ce66870e852340..938ae814515bfc 100644 --- a/x-pack/plugins/siem/public/lib/connectors/types.ts +++ b/x-pack/plugins/siem/public/lib/connectors/types.ts @@ -43,6 +43,8 @@ export interface ConnectorFlyoutFormProps { 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 { From 4beadfe41b770ea21f0246cb70e5c241355c51ca Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 22 Apr 2020 21:49:10 +0300 Subject: [PATCH 13/33] Common API --- .../builtin_action_types/connectors/api.ts | 103 ++++++++++++++++++ .../connectors/jira/api.ts | 98 +---------------- .../connectors/servicenow/api.ts | 98 +---------------- 3 files changed, 105 insertions(+), 194 deletions(-) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/connectors/api.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/api.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/api.ts new file mode 100644 index 00000000000000..e9c7828fc18395 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/api.ts @@ -0,0 +1,103 @@ +/* + * 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 { zipWith } from 'lodash'; + +import { + ConnectorApi, + ExternalServiceParams, + ExternalServiceCommentResponse, + Comment, + PushToServiceResponse, + ConnectorApiHandlerArgs, +} from './types'; +import { prepareFieldsForTransformation, transformFields, transformComments } from './utils'; + +const handshakeHandler = async ({ + externalService, + mapping, + params, +}: ConnectorApiHandlerArgs) => {}; +const getIncidentHandler = async ({ + externalService, + mapping, + params, +}: ConnectorApiHandlerArgs) => {}; + +const pushToServiceHandler = async ({ + externalService, + mapping, + params, +}: ConnectorApiHandlerArgs): 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({ + params, + 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']); + + // Create comments sequentially. + const promises = comments.reduce(async (prevPromise, currentComment) => { + const totalComments = await prevPromise; + const comment = await externalService.createComment({ + incidentId: res.id, + comment: currentComment, + field: mapping.get('comments').target, + }); + return [...totalComments, comment]; + }, Promise.resolve([] as ExternalServiceCommentResponse[])); + + const createdComments = await promises; + + const zippedComments: ExternalServiceCommentResponse[] = zipWith( + commentsTransformed, + createdComments, + (a: Comment, b: ExternalServiceCommentResponse) => ({ + commentId: a.commentId, + pushedDate: b.pushedDate, + }) + ); + + res.comments = [...zippedComments]; + } + + return res; +}; + +export const api: ConnectorApi = { + handshake: handshakeHandler, + pushToService: pushToServiceHandler, + getIncident: getIncidentHandler, +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/api.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/api.ts index aa3ac4ccadaf9f..d0eadcd99d718d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/api.ts @@ -4,100 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import { zipWith } from 'lodash'; - -import { - ConnectorApi, - ExternalServiceParams, - ExternalServiceCommentResponse, - Comment, - PushToServiceResponse, - ConnectorApiHandlerArgs, -} from '../types'; -import { prepareFieldsForTransformation, transformFields, transformComments } from '../utils'; - -const handshakeHandler = async ({ - externalService, - mapping, - params, -}: ConnectorApiHandlerArgs) => {}; -const getIncidentHandler = async ({ - externalService, - mapping, - params, -}: ConnectorApiHandlerArgs) => {}; - -const pushToServiceHandler = async ({ - externalService, - mapping, - params, -}: ConnectorApiHandlerArgs): 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({ - params, - 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']); - - // Create comments sequentially. - const promises = comments.reduce(async (prevPromise, currentComment) => { - const totalComments = await prevPromise; - const comment = await externalService.createComment({ - incidentId: res.id, - comment: currentComment, - field: mapping.get('comments').target, - }); - return [...totalComments, comment]; - }, Promise.resolve([] as ExternalServiceCommentResponse[])); - - const createdComments = await promises; - - const zippedComments: ExternalServiceCommentResponse[] = zipWith( - commentsTransformed, - createdComments, - (a: Comment, b: ExternalServiceCommentResponse) => ({ - commentId: a.commentId, - pushedDate: b.pushedDate, - }) - ); - - res.comments = [...zippedComments]; - } - - return res; -}; - -export const api: ConnectorApi = { - handshake: handshakeHandler, - pushToService: pushToServiceHandler, - getIncident: getIncidentHandler, -}; +export { api } from '../api'; diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/api.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/api.ts index aa3ac4ccadaf9f..d0eadcd99d718d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/api.ts @@ -4,100 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import { zipWith } from 'lodash'; - -import { - ConnectorApi, - ExternalServiceParams, - ExternalServiceCommentResponse, - Comment, - PushToServiceResponse, - ConnectorApiHandlerArgs, -} from '../types'; -import { prepareFieldsForTransformation, transformFields, transformComments } from '../utils'; - -const handshakeHandler = async ({ - externalService, - mapping, - params, -}: ConnectorApiHandlerArgs) => {}; -const getIncidentHandler = async ({ - externalService, - mapping, - params, -}: ConnectorApiHandlerArgs) => {}; - -const pushToServiceHandler = async ({ - externalService, - mapping, - params, -}: ConnectorApiHandlerArgs): 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({ - params, - 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']); - - // Create comments sequentially. - const promises = comments.reduce(async (prevPromise, currentComment) => { - const totalComments = await prevPromise; - const comment = await externalService.createComment({ - incidentId: res.id, - comment: currentComment, - field: mapping.get('comments').target, - }); - return [...totalComments, comment]; - }, Promise.resolve([] as ExternalServiceCommentResponse[])); - - const createdComments = await promises; - - const zippedComments: ExternalServiceCommentResponse[] = zipWith( - commentsTransformed, - createdComments, - (a: Comment, b: ExternalServiceCommentResponse) => ({ - commentId: a.commentId, - pushedDate: b.pushedDate, - }) - ); - - res.comments = [...zippedComments]; - } - - return res; -}; - -export const api: ConnectorApi = { - handshake: handshakeHandler, - pushToService: pushToServiceHandler, - getIncident: getIncidentHandler, -}; +export { api } from '../api'; From e1484c8fd3c8cce762560e78ee5941536eeff914 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 23 Apr 2020 16:18:41 +0300 Subject: [PATCH 14/33] Fix tests --- .../connectors/servicenow/service.test.ts | 1 - .../siem/public/lib/connectors/types.ts | 1 - .../builtin_action_types/servicenow.ts | 123 +++++++++++------- 3 files changed, 76 insertions(+), 49 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.test.ts index 070c91b471f123..4550ea81a55f99 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.test.ts @@ -9,7 +9,6 @@ import axios from 'axios'; import { createExternalService } from './service'; import * as utils from '../utils'; import { ExternalService } from '../types'; -import { object } from 'joi'; jest.mock('axios'); jest.mock('../utils', () => { diff --git a/x-pack/plugins/siem/public/lib/connectors/types.ts b/x-pack/plugins/siem/public/lib/connectors/types.ts index 938ae814515bfc..8ccbb97f787508 100644 --- a/x-pack/plugins/siem/public/lib/connectors/types.ts +++ b/x-pack/plugins/siem/public/lib/connectors/types.ts @@ -6,7 +6,6 @@ import { ActionType } from '../../../../../../plugins/triggers_actions_ui/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ActionConnectorFieldsProps } from '../../../../../../plugins/triggers_actions_ui/public/types'; import { ConnectorPublicConfigurationType } from '../../../../../../plugins/actions/common'; import { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts index 399ae0f27f5b1a..d277ed519e4a1d 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts @@ -49,25 +49,28 @@ export default function servicenowTest({ getService }: FtrProviderContext) { username: 'changeme', }, params: { - caseId: '123', - title: 'a title', - description: 'a description', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - incidentId: null, - comments: [ - { - commentId: '456', - version: 'WzU3LDFd', - comment: 'first comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - }, - ], + action: 'pushToService', + actionParams: { + caseId: '123', + title: 'a title', + description: 'a description', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + externalId: null, + comments: [ + { + commentId: '456', + version: 'WzU3LDFd', + comment: 'first comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + }, + ], + }, }, }; @@ -167,7 +170,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - 'error validating action type config: error configuring servicenow action: target url "http://servicenow.mynonexistent.com" is not whitelisted in the Kibana config xpack.actions.whitelistedHosts', + 'error validating action type config: error configuring connector action: target url "http://servicenow.mynonexistent.com" is not whitelisted in the Kibana config xpack.actions.whitelistedHosts', }); }); }); @@ -289,7 +292,10 @@ export default function servicenowTest({ getService }: FtrProviderContext) { .post(`/api/action/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ - params: { ...mockServiceNow.params, title: 'success', comments: [] }, + params: { + ...mockServiceNow.params, + actionParams: { ...mockServiceNow.params.actionParams, title: 'success', comments: [] }, + }, }) .expect(200); @@ -297,8 +303,8 @@ export default function servicenowTest({ getService }: FtrProviderContext) { status: 'ok', actionId: simulatedActionId, data: { - incidentId: '123', - number: 'INC01', + id: '123', + title: 'INC01', pushedDate: '2020-03-10T12:24:20.000Z', url: `${servicenowSimulatorURL}/nav_to.do?uri=incident.do?sys_id=123`, }, @@ -310,7 +316,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { .post(`/api/action/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ - params: {}, + params: { action: 'pushToService', actionParams: {} }, }) .then((resp: any) => { expect(resp.body).to.eql({ @@ -318,7 +324,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [caseId]: expected value of type [string] but got [undefined]', + 'error validating action params: [actionParams.caseId]: expected value of type [string] but got [undefined]', }); }); }); @@ -328,7 +334,12 @@ export default function servicenowTest({ getService }: FtrProviderContext) { .post(`/api/action/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ - params: { caseId: 'success' }, + params: { + ...mockServiceNow.params, + actionParams: { + caseId: 'success', + }, + }, }) .then((resp: any) => { expect(resp.body).to.eql({ @@ -336,7 +347,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [title]: expected value of type [string] but got [undefined]', + 'error validating action params: [actionParams.title]: expected value of type [string] but got [undefined]', }); }); }); @@ -346,7 +357,13 @@ export default function servicenowTest({ getService }: FtrProviderContext) { .post(`/api/action/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ - params: { caseId: 'success', title: 'success' }, + params: { + ...mockServiceNow.params, + actionParams: { + caseId: 'success', + title: 'success', + }, + }, }) .then((resp: any) => { expect(resp.body).to.eql({ @@ -354,7 +371,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [createdAt]: expected value of type [string] but got [undefined]', + 'error validating action params: [actionParams.createdAt]: expected value of type [string] but got [undefined]', }); }); }); @@ -365,11 +382,15 @@ export default function servicenowTest({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'foo') .send({ params: { - caseId: 'success', - title: 'success', - createdAt: 'success', - createdBy: { username: 'elastic' }, - comments: [{}], + ...mockServiceNow.params, + actionParams: { + ...mockServiceNow.params.actionParams, + caseId: 'success', + title: 'success', + createdAt: 'success', + createdBy: { username: 'elastic' }, + comments: [{}], + }, }, }) .then((resp: any) => { @@ -378,7 +399,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [comments.0.commentId]: expected value of type [string] but got [undefined]', + 'error validating action params: [actionParams.comments.0.commentId]: expected value of type [string] but got [undefined]', }); }); }); @@ -389,11 +410,15 @@ export default function servicenowTest({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'foo') .send({ params: { - caseId: 'success', - title: 'success', - createdAt: 'success', - createdBy: { username: 'elastic' }, - comments: [{ commentId: 'success' }], + ...mockServiceNow.params, + actionParams: { + ...mockServiceNow.params.actionParams, + caseId: 'success', + title: 'success', + createdAt: 'success', + createdBy: { username: 'elastic' }, + comments: [{ commentId: 'success' }], + }, }, }) .then((resp: any) => { @@ -402,7 +427,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [comments.0.comment]: expected value of type [string] but got [undefined]', + 'error validating action params: [actionParams.comments.0.comment]: expected value of type [string] but got [undefined]', }); }); }); @@ -413,11 +438,15 @@ export default function servicenowTest({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'foo') .send({ params: { - caseId: 'success', - title: 'success', - createdAt: 'success', - createdBy: { username: 'elastic' }, - comments: [{ commentId: 'success', comment: 'success' }], + ...mockServiceNow.params, + actionParams: { + ...mockServiceNow.params.actionParams, + caseId: 'success', + title: 'success', + createdAt: 'success', + createdBy: { username: 'elastic' }, + comments: [{ commentId: 'success', comment: 'success' }], + }, }, }) .then((resp: any) => { @@ -426,7 +455,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [comments.0.createdAt]: expected value of type [string] but got [undefined]', + 'error validating action params: [actionParams.comments.0.createdAt]: expected value of type [string] but got [undefined]', }); }); }); From c9b1267b848e9c646ecf559fdfe364e733a43ed4 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 23 Apr 2020 16:48:22 +0300 Subject: [PATCH 15/33] Update README --- x-pack/plugins/actions/README.md | 57 ++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index decd170ca5dd64..1e5f84b022bdcc 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -28,7 +28,7 @@ Table of Contents - [RESTful API](#restful-api) - [`POST /api/action`: Create action](#post-apiaction-create-action) - [`DELETE /api/action/{id}`: Delete action](#delete-apiactionid-delete-action) - - [`GET /api/action/_getAll`: Get all actions](#get-apiaction-get-all-actions) + - [`GET /api/action/_getAll`: Get all actions](#get-apiactiongetall-get-all-actions) - [`GET /api/action/{id}`: Get action](#get-apiactionid-get-action) - [`GET /api/action/types`: List action types](#get-apiactiontypes-list-action-types) - [`PUT /api/action/{id}`: Update action](#put-apiactionid-update-action) @@ -64,6 +64,12 @@ Table of Contents - [`config`](#config-6) - [`secrets`](#secrets-6) - [`params`](#params-6) + - [`actionParams (pushToService)`](#actionparams-pushtoservice) + - [Jira](#jira) + - [`config`](#config-7) + - [`secrets`](#secrets-7) + - [`params`](#params-7) + - [`actionParams (pushToService)`](#actionparams-pushtoservice-1) - [Command Line Utility](#command-line-utility) ## Terminology @@ -483,13 +489,60 @@ The ServiceNow action uses the [V2 Table API](https://developer.servicenow.com/a ### `params` +| Property | Description | Type | +| ------------ | -------------------------------------------------------------------------------- | ------ | +| action | The action to perform. It can be `pushToService`, `handshake`, and `getIncident` | string | +| actionParams | The parameters of the action | object | + +#### `actionParams (pushToService)` + | Property | Description | Type | | ----------- | -------------------------------------------------------------------------------------------------------------------------- | --------------------- | | caseId | The case id | 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)_ | -| incidentID | The id of the incident in ServiceNow . If presented the incident will be update. Otherwise a new incident will be created. | string _(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)_ | + + +--- + +## Jira + +ID: `.jira` + +The Jira action uses the [V2 API](https://developer.atlassian.com/cloud/jira/platform/rest/v2/) to create and update Jira incidents. + +### `config` + +| Property | Description | Type | +| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | +| apiUrl | ServiceNow instance URL. | string | +| casesConfiguration | 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 ServiceNow and will be overwrite on each update. | object | + +### `secrets` + +| Property | Description | Type | +| -------- | --------------------------------------- | ------ | +| email | email for HTTP Basic authentication | string | +| apiToken | API token for HTTP Basic authentication | string | + +### `params` + +| Property | Description | Type | +| ------------ | -------------------------------------------------------------------------------- | ------ | +| action | The action to perform. It can be `pushToService`, `handshake`, and `getIncident` | string | +| actionParams | The parameters of the action | object | + +#### `actionParams (pushToService)` + +| Property | Description | Type | +| ----------- | ------------------------------------------------------------------------------------------------------------------- | --------------------- | +| caseId | The case id | 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)_ | +| externalId | The id of the incident in Jira. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ | # Command Line Utility From 6b481c97ae56c080c4d05e081df955da96a36de4 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 23 Apr 2020 18:54:30 +0300 Subject: [PATCH 16/33] Fix i18n --- .../connectors/translations.ts | 12 ++++++------ .../translations/translations/ja-JP.json | 17 ----------------- .../translations/translations/zh-CN.json | 17 ----------------- 3 files changed, 6 insertions(+), 40 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/translations.ts index 692a4750b9fcb7..d1a95937040d52 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/translations.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/translations.ts @@ -20,34 +20,34 @@ export const FIELD_INFORMATION = ( ) => { switch (mode) { case 'create': - return i18n.translate('xpack.actions.builtin.connector.informationCreated', { + return i18n.translate('xpack.actions.builtin.connector.common.informationCreated', { values: { date, user }, defaultMessage: '(created at {date} by {user})', }); case 'update': - return i18n.translate('xpack.actions.builtin.connector.informationUpdated', { + return i18n.translate('xpack.actions.builtin.connector.common.informationUpdated', { values: { date, user }, defaultMessage: '(updated at {date} by {user})', }); case 'add': - return i18n.translate('xpack.actions.builtin.connector.informationAdded', { + return i18n.translate('xpack.actions.builtin.connector.common.informationAdded', { values: { date, user }, defaultMessage: '(added at {date} by {user})', }); default: - return i18n.translate('xpack.actions.builtin.connector.informationDefault', { + return i18n.translate('xpack.actions.builtin.connector.common.informationDefault', { values: { date, user }, defaultMessage: '(created at {date} by {user})', }); } }; -export const MAPPING_EMPTY = i18n.translate('xpack.actions.builtin.connector.emptyMapping', { +export const MAPPING_EMPTY = i18n.translate('xpack.actions.builtin.connector.common.emptyMapping', { defaultMessage: '[casesConfiguration.mapping]: expected non-empty but got empty', }); export const WHITE_LISTED_ERROR = (message: string) => - i18n.translate('xpack.actions.builtin.connector.apiWhitelistError', { + i18n.translate('xpack.actions.builtin.connector.common.apiWhitelistError', { defaultMessage: 'error configuring connector action: {message}', values: { message, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 81dc44f3a4cb41..9e2d0f757ee2f1 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -3998,16 +3998,6 @@ "xpack.actions.builtin.pagerdutyTitle": "PagerDuty", "xpack.actions.builtin.serverLog.errorLoggingErrorMessage": "メッセージのロギングエラー", "xpack.actions.builtin.serverLogTitle": "サーバーログ", - "xpack.actions.builtin.servicenow.emptyMapping": "[casesConfiguration.mapping]: 空以外の値が必要ですが空でした", - "xpack.actions.builtin.servicenow.informationAdded": "({date} に {user} が追加)", - "xpack.actions.builtin.servicenow.informationCreated": "({date} に {user} が作成)", - "xpack.actions.builtin.servicenow.informationDefault": "({date} に {user} が作成)", - "xpack.actions.builtin.servicenow.informationUpdated": "({date} に {user} が更新)", - "xpack.actions.builtin.servicenow.postingErrorMessage": "servicenow イベントの送信エラー", - "xpack.actions.builtin.servicenow.postingRetryErrorMessage": "servicenow イベントの送信エラー: http status {status}、後で再試行", - "xpack.actions.builtin.servicenow.postingUnexpectedErrorMessage": "servicenow イベントの送信エラー: 予期しないステータス {status}", - "xpack.actions.builtin.servicenow.servicenowApiNullError": "ServiceNow [apiUrl] が必要です", - "xpack.actions.builtin.servicenow.servicenowApiWhitelistError": "servicenow アクションの構成エラー: {message}", "xpack.actions.builtin.servicenowTitle": "ServiceNow", "xpack.actions.builtin.slack.errorPostingErrorMessage": "slack メッセージの投稿エラー", "xpack.actions.builtin.slack.errorPostingRetryDateErrorMessage": "slack メッセージの投稿エラー、 {retryString} に再試行", @@ -13296,14 +13286,7 @@ "xpack.siem.case.confirmDeleteCase.deleteTitle": "「{caseTitle}」を削除", "xpack.siem.case.confirmDeleteCase.selectedCases": "選択したケースを削除", "xpack.siem.case.connectors.servicenow.actionTypeTitle": "ServiceNow", - "xpack.siem.case.connectors.servicenow.apiUrlTextFieldLabel": "URL", - "xpack.siem.case.connectors.servicenow.invalidApiUrlTextField": "URL が無効です", - "xpack.siem.case.connectors.servicenow.passwordTextFieldLabel": "パスワード", - "xpack.siem.case.connectors.servicenow.requiredApiUrlTextField": "URL が必要です", - "xpack.siem.case.connectors.servicenow.requiredPasswordTextField": "パスワードが必要です", - "xpack.siem.case.connectors.servicenow.requiredUsernameTextField": "ユーザー名が必要です", "xpack.siem.case.connectors.servicenow.selectMessageText": "ServiceNow で SIEM ケースデータをb\\更新するか、または新しいインシデントにプッシュする", - "xpack.siem.case.connectors.servicenow.usernameTextFieldLabel": "ユーザー名", "xpack.siem.case.createCase.descriptionFieldRequiredError": "説明が必要です。", "xpack.siem.case.createCase.fieldTagsHelpText": "このケースの 1 つ以上のカスタム識別タグを入力します。新しいタグを開始するには、各タグの後でEnterを押します。", "xpack.siem.case.createCase.titleFieldRequiredError": "タイトルが必要です。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index e06edb45de8fa9..9df72c9d881cf8 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -3999,16 +3999,6 @@ "xpack.actions.builtin.pagerdutyTitle": "PagerDuty", "xpack.actions.builtin.serverLog.errorLoggingErrorMessage": "记录消息时出错", "xpack.actions.builtin.serverLogTitle": "服务器日志", - "xpack.actions.builtin.servicenow.emptyMapping": "[casesConfiguration.mapping]:应为非空,但却为空", - "xpack.actions.builtin.servicenow.informationAdded": "(由 {user} 于 {date}添加)", - "xpack.actions.builtin.servicenow.informationCreated": "(由 {user} 于 {date}创建)", - "xpack.actions.builtin.servicenow.informationDefault": "(由 {user} 于 {date}创建)", - "xpack.actions.builtin.servicenow.informationUpdated": "(由 {user} 于 {date}更新)", - "xpack.actions.builtin.servicenow.postingErrorMessage": "发布 servicenow 事件时出错", - "xpack.actions.builtin.servicenow.postingRetryErrorMessage": "发布 servicenow 事件时出错:http 状态 {status},请稍后重试", - "xpack.actions.builtin.servicenow.postingUnexpectedErrorMessage": "发布 servicenow 事件时出错:非预期状态 {status}", - "xpack.actions.builtin.servicenow.servicenowApiNullError": "必须指定 ServiceNow [apiUrl]", - "xpack.actions.builtin.servicenow.servicenowApiWhitelistError": "配置 servicenow 操作时出错:{message}", "xpack.actions.builtin.servicenowTitle": "ServiceNow", "xpack.actions.builtin.slack.errorPostingErrorMessage": "发布 slack 消息时出错", "xpack.actions.builtin.slack.errorPostingRetryDateErrorMessage": "发布 slack 消息时出错,在 {retryString} 重试", @@ -13300,14 +13290,7 @@ "xpack.siem.case.confirmDeleteCase.deleteTitle": "删除“{caseTitle}”", "xpack.siem.case.confirmDeleteCase.selectedCases": "删除选定案例", "xpack.siem.case.connectors.servicenow.actionTypeTitle": "ServiceNow", - "xpack.siem.case.connectors.servicenow.apiUrlTextFieldLabel": "URL", - "xpack.siem.case.connectors.servicenow.invalidApiUrlTextField": "URL 无效", - "xpack.siem.case.connectors.servicenow.passwordTextFieldLabel": "密码", - "xpack.siem.case.connectors.servicenow.requiredApiUrlTextField": "“URL”必填", - "xpack.siem.case.connectors.servicenow.requiredPasswordTextField": "“密码”必填", - "xpack.siem.case.connectors.servicenow.requiredUsernameTextField": "“用户名”必填", "xpack.siem.case.connectors.servicenow.selectMessageText": "将 SIEM 案例数据推送或更新到 ServiceNow 中的新事件", - "xpack.siem.case.connectors.servicenow.usernameTextFieldLabel": "用户名", "xpack.siem.case.createCase.descriptionFieldRequiredError": "描述必填。", "xpack.siem.case.createCase.fieldTagsHelpText": "为此案例键入一个或多个定制识别标记。在每个标记后按 Enter 键可开始新的标记。", "xpack.siem.case.createCase.titleFieldRequiredError": "标题必填。", From 84b4d133e7e9c2ce2e2991ee3bda7c3a35a4ec89 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 27 Apr 2020 11:38:30 +0300 Subject: [PATCH 17/33] Fix lint errors --- .../builtin_action_types/connectors/api.ts | 4 +- .../connectors/servicenow/mocks.ts | 4 +- .../builtin_action_types/connectors/types.ts | 16 +++++-- .../connectors/utils.test.ts | 1 + .../builtin_action_types/connectors/utils.ts | 46 ++++++++++++------- 5 files changed, 48 insertions(+), 23 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/api.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/api.ts index e9c7828fc18395..bcc49b2301816d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/api.ts @@ -64,7 +64,7 @@ const pushToServiceHandler = async ({ comments && Array.isArray(comments) && comments.length > 0 && - mapping.get('comments').actionType !== 'nothing' + mapping.get('comments')?.actionType !== 'nothing' ) { const commentsTransformed = transformComments(comments, ['informationAdded']); @@ -74,7 +74,7 @@ const pushToServiceHandler = async ({ const comment = await externalService.createComment({ incidentId: res.id, comment: currentComment, - field: mapping.get('comments').target, + field: mapping.get('comments')?.target ?? 'comments', }); return [...totalComments, comment]; }, Promise.resolve([] as ExternalServiceCommentResponse[])); diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/mocks.ts index 52472d05633a28..14aea1cc27921e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/mocks.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/mocks.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExternalService, ApiParams, ExecutorActionParams } from '../types'; +import { ExternalService, ApiParams, ExecutorActionParams, MapRecord } from '../types'; const createMock = (): jest.Mocked => ({ getIncident: jest.fn().mockImplementation(() => @@ -41,7 +41,7 @@ const externalServiceMock = { create: createMock, }; -const mapping: Map = new Map(); +const mapping: Map> = new Map(); mapping.set('title', { target: 'short_description', diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/types.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/types.ts index ecbbd6466a7272..c424368c4e91cd 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/types.ts @@ -19,20 +19,22 @@ import { ActionsConfigurationUtilities } from '../../actions_config'; import { ExecutorType } from '../../types'; export interface AnyParams { - [index: string]: string; + [index: string]: string | number | object | null | undefined; } export type ConnectorPublicConfigurationType = TypeOf; export type ConnectorSecretConfigurationType = TypeOf; export type ExecutorParams = TypeOf; -export type ExecutorActionParams = TypeOf; +export type ExecutorActionParams = TypeOf & AnyParams; export type CaseConfiguration = TypeOf; export type MapRecord = TypeOf; export type Comment = TypeOf; export interface ApiParams extends ExecutorActionParams { + // This will have to remain `any` until we can extend connectors with generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any externalCase: Record; } @@ -42,12 +44,16 @@ export interface ConnectorConfiguration { } export interface ExternalServiceCredential { + // eslint-disable-next-line @typescript-eslint/no-explicit-any config: Record; + // eslint-disable-next-line @typescript-eslint/no-explicit-any secrets: Record; } export interface ConnectorValidation { + // eslint-disable-next-line @typescript-eslint/no-explicit-any config: (configurationUtilities: ActionsConfigurationUtilities, configObject: any) => void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any secrets: (configurationUtilities: ActionsConfigurationUtilities, secrets: any) => void; } @@ -65,10 +71,12 @@ export interface ExternalServiceCommentResponse { } export interface ExternalServiceParams { + // eslint-disable-next-line @typescript-eslint/no-explicit-any [index: string]: any; } export interface ExternalService { + // eslint-disable-next-line @typescript-eslint/no-explicit-any getIncident: (id: string) => Promise; createIncident: (params: ExternalServiceParams) => Promise; updateIncident: (params: ExternalServiceParams) => Promise; @@ -77,6 +85,7 @@ export interface ExternalService { export interface ConnectorApiHandlerArgs { externalService: ExternalService; + // eslint-disable-next-line @typescript-eslint/no-explicit-any mapping: Map; params: ApiParams; } @@ -99,6 +108,7 @@ export interface CreateConnectorBasicArgs { export interface CreateConnectorArgs extends CreateConnectorBasicArgs { config: ConnectorConfiguration; validate: ConnectorValidation; + // eslint-disable-next-line @typescript-eslint/no-explicit-any validationSchema: { config: any; secrets: any }; } @@ -116,7 +126,7 @@ export interface PipedField { export interface PrepareFieldsForTransformArgs { params: ApiParams; - mapping: Map; + mapping: Map; defaultPipes?: string[]; } diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/utils.test.ts index 4304410f81b873..07ac2024825012 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/utils.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/utils.test.ts @@ -32,6 +32,7 @@ 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', { diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/utils.ts index 8b80b273b1ceb4..5f4b55badbd346 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/utils.ts @@ -24,6 +24,7 @@ import { PipedField, TransformFieldsArgs, Comment, + ExecutorActionParams, } from './types'; import * as transformers from './transformers'; @@ -37,7 +38,7 @@ export const normalizeMapping = (supportedFields: string[], mapping: MapRecord[] ); }; -export const buildMap = (mapping: MapRecord[]): Map => { +export const buildMap = (mapping: MapRecord[]): Map => { return normalizeMapping(SUPPORTED_SOURCE_FIELDS, mapping).reduce((fieldsMap, field) => { const { source, target, actionType } = field; fieldsMap.set(source, { target, actionType }); @@ -46,14 +47,17 @@ export const buildMap = (mapping: MapRecord[]): Map => { }, new Map()); }; -export const mapParams = (params: any, mapping: Map) => { +export const mapParams = ( + params: Partial, + mapping: Map +): AnyParams => { return Object.keys(params).reduce((prev: AnyParams, curr: string): AnyParams => { const field = mapping.get(curr); if (field) { prev[field.target] = params[curr]; } return prev; - }, {}); + }, {} as AnyParams); }; export const createConnectorExecutor = ({ @@ -72,7 +76,7 @@ export const createConnectorExecutor = ({ const { comments, externalId, ...restParams } = actionParams; const mapping = buildMap(configurationMapping); - const externalCase = mapParams(restParams, mapping); + const externalCase = mapParams(restParams as ExecutorActionParams, mapping); const res: Pick & Pick = { @@ -147,6 +151,8 @@ export const request = async ({ axios: AxiosInstance; url: string; method?: Method; + // This will have to remain `any` until we can extend connectors with generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any data?: any; }): Promise => { const res = await axios(url, { method, data }); @@ -161,6 +167,8 @@ export const patch = ({ }: { axios: AxiosInstance; url: string; + // This will have to remain `any` until we can extend connectors with generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any data: any; }): Promise => { return request({ @@ -180,18 +188,22 @@ export const prepareFieldsForTransformation = ({ mapping, defaultPipes = ['informationCreated'], }: PrepareFieldsForTransformArgs): PipedField[] => { - return Object.keys(params.externalCase) - .filter(p => mapping.get(p).actionType !== 'nothing') - .map(p => ({ - key: p, - value: params.externalCase[p], - actionType: mapping.get(p).actionType, - pipes: [...defaultPipes], - })) - .map(p => ({ - ...p, - pipes: p.actionType === 'append' ? [...p.pipes, 'append'] : p.pipes, - })); + return ( + Object.keys(params.externalCase) + // This clears undefined values but typescript does not get it + .filter(p => mapping.get(p)?.actionType) + .filter(p => mapping.get(p)?.actionType !== 'nothing') + .map(p => ({ + key: p, + value: params.externalCase[p], + actionType: mapping.get(p)?.actionType ?? 'nothing', + pipes: [...defaultPipes], + })) + .map(p => ({ + ...p, + pipes: p.actionType === 'append' ? [...p.pipes, 'append'] : p.pipes, + })) + ); }; const t = { ...transformers } as { [index: string]: Function }; // TODO: Find a better solution if exists. @@ -213,6 +225,8 @@ export const transformFields = ({ params, fields, currentIncident }: TransformFi previousValue: currentIncident ? currentIncident[cur.key] : '', }).value; return prev; + // This will have to remain `any` until we can extend connectors with generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any }, {} as any); }; From 4af3820f50dd838a51f17934c7e2c05f03e7a91e Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 27 Apr 2020 14:19:31 +0300 Subject: [PATCH 18/33] Test Jira service --- .../connectors/jira/service.test.ts | 297 ++++++++++++++++++ .../connectors/jira/service.ts | 13 +- .../connectors/servicenow/api.test.ts | 2 +- 3 files changed, 307 insertions(+), 5 deletions(-) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/connectors/jira/service.test.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/service.test.ts new file mode 100644 index 00000000000000..fbf42a4fae3176 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/service.test.ts @@ -0,0 +1,297 @@ +/* + * 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 axios from 'axios'; + +import { createExternalService } from './service'; +import * as utils from '../utils'; +import { ExternalService } from '../types'; + +jest.mock('axios'); +jest.mock('../utils', () => { + const originalUtils = jest.requireActual('../utils'); + return { + ...originalUtils, + request: jest.fn(), + }; +}); + +axios.create = jest.fn(() => axios); +const requestMock = utils.request as jest.Mock; + +describe('Jira service', () => { + let service: ExternalService; + + beforeAll(() => { + service = createExternalService({ + config: { apiUrl: 'https://siem-kibana.atlassian.net', projectKey: 'CK' }, + secrets: { apiToken: 'token', email: 'elastic@elastic.com' }, + }); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('createExternalService', () => { + test('throws without url', () => { + expect(() => + createExternalService({ + config: { apiUrl: null, projectKey: 'CK' }, + secrets: { apiToken: 'token', email: 'elastic@elastic.com' }, + }) + ).toThrow(); + }); + + test('throws without projectKey', () => { + expect(() => + createExternalService({ + config: { apiUrl: 'test.com', projectKey: null }, + secrets: { apiToken: 'token', email: 'elastic@elastic.com' }, + }) + ).toThrow(); + }); + + test('throws without username', () => { + expect(() => + createExternalService({ + config: { apiUrl: 'test.com' }, + secrets: { apiToken: '', email: 'elastic@elastic.com' }, + }) + ).toThrow(); + }); + + test('throws without password', () => { + expect(() => + createExternalService({ + config: { apiUrl: 'test.com' }, + secrets: { apiToken: '', email: undefined }, + }) + ).toThrow(); + }); + }); + + describe('getIncident', () => { + test('it returns the incident correctly', async () => { + requestMock.mockImplementation(() => ({ + data: { id: '1', key: 'CK-1', fields: { summary: 'title', description: 'description' } }, + })); + const res = await service.getIncident('1'); + expect(res).toEqual({ id: '1', key: 'CK-1', summary: 'title', description: 'description' }); + }); + + test('it should call request with correct arguments', async () => { + requestMock.mockImplementation(() => ({ + data: { id: '1', key: 'CK-1' }, + })); + + await service.getIncident('1'); + expect(requestMock).toHaveBeenCalledWith({ + axios, + url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1', + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementation(() => { + throw new Error('An error has occurred'); + }); + expect(service.getIncident('1')).rejects.toThrow( + 'Unable to get incident with id 1. Error: An error has occurred' + ); + }); + }); + + describe('createIncident', () => { + test('it creates the incident correctly', async () => { + // The response from Jira when creating an issue contains only the key and the id. + // The service makes two calls when creating an issue. One to create and one to get + // the created incident with all the necessary fields. + requestMock.mockImplementationOnce(() => ({ + data: { id: '1', key: 'CK-1', fields: { summary: 'title', description: 'description' } }, + })); + + requestMock.mockImplementationOnce(() => ({ + data: { id: '1', key: 'CK-1', fields: { created: '2020-04-27T10:59:46.202Z' } }, + })); + + const res = await service.createIncident({ + incident: { summary: 'title', description: 'desc' }, + }); + + expect(res).toEqual({ + title: 'CK-1', + id: '1', + pushedDate: '2020-04-27T10:59:46.202Z', + url: 'https://siem-kibana.atlassian.net/browse/CK-1', + }); + }); + + test('it should call request with correct arguments', async () => { + requestMock.mockImplementation(() => ({ + data: { + id: '1', + key: 'CK-1', + fields: { created: '2020-04-27T10:59:46.202Z' }, + }, + })); + + await service.createIncident({ + incident: { summary: 'title', description: 'desc' }, + }); + + expect(requestMock).toHaveBeenCalledWith({ + axios, + url: 'https://siem-kibana.atlassian.net/rest/api/2/issue', + method: 'post', + data: { + fields: { + summary: 'title', + description: 'desc', + project: { key: 'CK' }, + issuetype: { name: 'Task' }, + }, + }, + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementation(() => { + throw new Error('An error has occurred'); + }); + + expect( + service.createIncident({ + incident: { summary: 'title', description: 'desc' }, + }) + ).rejects.toThrow('[Action][Jira]: Unable to create incident. Error: An error has occurred'); + }); + }); + + describe('updateIncident', () => { + test('it updates the incident correctly', async () => { + requestMock.mockImplementation(() => ({ + data: { + id: '1', + key: 'CK-1', + fields: { updated: '2020-04-27T10:59:46.202Z' }, + }, + })); + + const res = await service.updateIncident({ + incidentId: '1', + incident: { summary: 'title', description: 'desc' }, + }); + + expect(res).toEqual({ + title: 'CK-1', + id: '1', + pushedDate: '2020-04-27T10:59:46.202Z', + url: 'https://siem-kibana.atlassian.net/browse/CK-1', + }); + }); + + test('it should call request with correct arguments', async () => { + requestMock.mockImplementation(() => ({ + data: { + id: '1', + key: 'CK-1', + fields: { updated: '2020-04-27T10:59:46.202Z' }, + }, + })); + + await service.updateIncident({ + incidentId: '1', + incident: { summary: 'title', description: 'desc' }, + }); + + expect(requestMock).toHaveBeenCalledWith({ + axios, + method: 'put', + url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1', + data: { fields: { summary: 'title', description: 'desc' } }, + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementation(() => { + throw new Error('An error has occurred'); + }); + + expect( + service.updateIncident({ + incidentId: '1', + incident: { summary: 'title', description: 'desc' }, + }) + ).rejects.toThrow( + '[Action][Jira]: Unable to update incident with id 1. Error: An error has occurred' + ); + }); + }); + + describe('createComment', () => { + test('it creates the comment correctly', async () => { + requestMock.mockImplementation(() => ({ + data: { + id: '1', + key: 'CK-1', + created: '2020-04-27T10:59:46.202Z', + }, + })); + + const res = await service.createComment({ + incidentId: '1', + comment: { comment: 'comment', commentId: 'comment-1' }, + field: 'comments', + }); + + expect(res).toEqual({ + commentId: 'comment-1', + pushedDate: '2020-04-27T10:59:46.202Z', + externalCommentId: '1', + }); + }); + + test('it should call request with correct arguments', async () => { + requestMock.mockImplementation(() => ({ + data: { + id: '1', + key: 'CK-1', + created: '2020-04-27T10:59:46.202Z', + }, + })); + + await service.createComment({ + incidentId: '1', + comment: { comment: 'comment', commentId: 'comment-1' }, + field: 'my_field', + }); + + expect(requestMock).toHaveBeenCalledWith({ + axios, + method: 'post', + url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1/comment', + data: { body: 'comment' }, + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementation(() => { + throw new Error('An error has occurred'); + }); + + expect( + service.createComment({ + incidentId: '1', + comment: { comment: 'comment', commentId: 'comment-1' }, + field: 'comments', + }) + ).rejects.toThrow( + '[Action][Jira]: Unable to create comment at incident with id 1. Error: An error has occurred' + ); + }); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/service.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/service.ts index 682b26f94e1226..72a9189fa90585 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/service.ts @@ -26,7 +26,7 @@ export const createExternalService = ({ const { apiUrl: url, projectKey } = config as JiraPublicConfigurationType; const { apiToken, email } = secrets as JiraSecretConfigurationType; - if (!url || !apiToken || !email) { + if (!url || !projectKey || !apiToken || !email) { throw Error(`[Action]${i18n.NAME}: Wrong configuration.`); } @@ -51,7 +51,9 @@ export const createExternalService = ({ url: `${incidentUrl}/${id}`, }); - return { ...res.data }; + const { fields, ...rest } = res.data; + + return { ...rest, ...fields }; } catch (error) { throw new Error( getErrorMessage(i18n.NAME, `Unable to get incident with id ${id}. Error: ${error.message}`) @@ -60,6 +62,9 @@ export const createExternalService = ({ }; const createIncident = async ({ incident }: ExternalServiceParams) => { + // The response from Jira when creating an issue contains only the key and the id. + // The function makes two calls when creating an issue. One to create the issue and one to get + // the created issue with all the necessary fields. try { const res = await request({ axios: axiosInstance, @@ -75,7 +80,7 @@ export const createExternalService = ({ return { title: updatedIncident.key, id: updatedIncident.id, - pushedDate: new Date(updatedIncident.fields.created).toISOString(), + pushedDate: new Date(updatedIncident.created).toISOString(), url: getIncidentViewURL(updatedIncident.key), }; } catch (error) { @@ -99,7 +104,7 @@ export const createExternalService = ({ return { title: updatedIncident.key, id: updatedIncident.id, - pushedDate: new Date(updatedIncident.fields.updated).toISOString(), + pushedDate: new Date(updatedIncident.updated).toISOString(), url: getIncidentViewURL(updatedIncident.key), }; } catch (error) { diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/api.test.ts index 0ce7d2dfac9e4f..54228f0c3a0254 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/api.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/api.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { api } from './api'; +import { api } from '../api'; import { externalServiceMock, mapping, apiParams } from './mocks'; import { ExternalService } from '../types'; From eb166e2357d47fd5e5273564c5c8148fd5562e9c Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 27 Apr 2020 14:52:58 +0300 Subject: [PATCH 19/33] Test Jira api --- .../builtin_action_types/connectors/api.ts | 2 +- .../connectors/jira/api.test.ts | 521 ++++++++++++++++++ .../connectors/jira/mocks.ts | 107 ++++ 3 files changed, 629 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/connectors/jira/api.test.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/connectors/jira/mocks.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/api.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/api.ts index bcc49b2301816d..05d29597b632e1 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/api.ts @@ -69,7 +69,7 @@ const pushToServiceHandler = async ({ const commentsTransformed = transformComments(comments, ['informationAdded']); // Create comments sequentially. - const promises = comments.reduce(async (prevPromise, currentComment) => { + const promises = commentsTransformed.reduce(async (prevPromise, currentComment) => { const totalComments = await prevPromise; const comment = await externalService.createComment({ incidentId: res.id, diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/api.test.ts new file mode 100644 index 00000000000000..b8c56e84ca90ea --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/api.test.ts @@ -0,0 +1,521 @@ +/* + * 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 { api } from '../api'; +import { externalServiceMock, mapping, apiParams } from './mocks'; +import { ExternalService } from '../types'; + +describe('api', () => { + let externalService: jest.Mocked; + + beforeAll(() => { + externalService = externalServiceMock.create(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('pushToService', () => { + describe('create incident', () => { + test('it creates an incident', async () => { + const params = { ...apiParams, externalId: null }; + const res = await api.pushToService({ externalService, mapping, params }); + + expect(res).toEqual({ + id: '1', + title: 'CK-1', + pushedDate: '2020-04-27T10:59:46.202Z', + url: 'https://siem-kibana.atlassian.net/browse/CK-1', + comments: [ + { + commentId: 'case-comment-1', + pushedDate: '2020-04-27T10:59:46.202Z', + }, + { + commentId: 'case-comment-2', + pushedDate: '2020-04-27T10:59:46.202Z', + }, + ], + }); + }); + + test('it creates an incident without comments', async () => { + const params = { ...apiParams, externalId: null, comments: [] }; + const res = await api.pushToService({ externalService, mapping, params }); + + expect(res).toEqual({ + id: '1', + title: 'CK-1', + pushedDate: '2020-04-27T10:59:46.202Z', + url: 'https://siem-kibana.atlassian.net/browse/CK-1', + }); + }); + + test('it calls createIncident correctly', async () => { + const params = { ...apiParams, externalId: null }; + await api.pushToService({ externalService, mapping, params }); + + expect(externalService.createIncident).toHaveBeenCalledWith({ + incident: { + description: + 'Incident description (created at 2020-04-27T10:59:46.202Z by Elastic User)', + summary: 'Incident title (created at 2020-04-27T10:59:46.202Z by Elastic User)', + }, + }); + expect(externalService.updateIncident).not.toHaveBeenCalled(); + }); + + test('it calls createComment correctly', async () => { + const params = { ...apiParams, externalId: null }; + await api.pushToService({ externalService, mapping, params }); + expect(externalService.createComment).toHaveBeenCalledTimes(2); + expect(externalService.createComment).toHaveBeenNthCalledWith(1, { + incidentId: '1', + comment: { + commentId: 'case-comment-1', + version: 'WzU3LDFd', + comment: 'A comment (added at 2020-04-27T10:59:46.202Z by Elastic User)', + createdAt: '2020-04-27T10:59:46.202Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + updatedAt: '2020-04-27T10:59:46.202Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + }, + field: 'comments', + }); + + expect(externalService.createComment).toHaveBeenNthCalledWith(2, { + incidentId: '1', + comment: { + commentId: 'case-comment-2', + version: 'WlK3LDFd', + comment: 'Another comment (added at 2020-04-27T10:59:46.202Z by Elastic User)', + createdAt: '2020-04-27T10:59:46.202Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + updatedAt: '2020-04-27T10:59:46.202Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + }, + field: 'comments', + }); + }); + }); + + describe('update incident', () => { + test('it updates an incident', async () => { + const res = await api.pushToService({ externalService, mapping, params: apiParams }); + + expect(res).toEqual({ + id: 'incident-2', + title: 'INC02', + pushedDate: '2020-04-27T10:59:46.202Z', + url: 'https://siem-kibana.atlassian.net/browse/CK-1', + comments: [ + { + commentId: 'case-comment-1', + pushedDate: '2020-04-27T10:59:46.202Z', + }, + { + commentId: 'case-comment-2', + pushedDate: '2020-04-27T10:59:46.202Z', + }, + ], + }); + }); + + test('it updates an incident without comments', async () => { + const params = { ...apiParams, comments: [] }; + const res = await api.pushToService({ externalService, mapping, params }); + + expect(res).toEqual({ + id: 'incident-2', + title: 'INC02', + pushedDate: '2020-04-27T10:59:46.202Z', + url: 'https://siem-kibana.atlassian.net/browse/CK-1', + }); + }); + + test('it calls updateIncident correctly', async () => { + const params = { ...apiParams }; + await api.pushToService({ externalService, mapping, params }); + + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + description: + 'Incident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + summary: 'Incident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + }, + }); + expect(externalService.createIncident).not.toHaveBeenCalled(); + }); + + test('it calls createComment correctly', async () => { + const params = { ...apiParams }; + await api.pushToService({ externalService, mapping, params }); + expect(externalService.createComment).toHaveBeenCalledTimes(2); + expect(externalService.createComment).toHaveBeenNthCalledWith(1, { + incidentId: 'incident-2', + comment: { + commentId: 'case-comment-1', + version: 'WzU3LDFd', + comment: 'A comment (added at 2020-04-27T10:59:46.202Z by Elastic User)', + createdAt: '2020-04-27T10:59:46.202Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + updatedAt: '2020-04-27T10:59:46.202Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + }, + field: 'comments', + }); + + expect(externalService.createComment).toHaveBeenNthCalledWith(2, { + incidentId: 'incident-2', + comment: { + commentId: 'case-comment-2', + version: 'WlK3LDFd', + comment: 'Another comment (added at 2020-04-27T10:59:46.202Z by Elastic User)', + createdAt: '2020-04-27T10:59:46.202Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + updatedAt: '2020-04-27T10:59:46.202Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + }, + field: 'comments', + }); + }); + }); + + describe('mapping variations', () => { + test('overwrite & append', async () => { + mapping.set('title', { + target: 'summary', + actionType: 'overwrite', + }); + + mapping.set('description', { + target: 'description', + actionType: 'append', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('summary', { + target: 'title', + actionType: 'overwrite', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + summary: 'Incident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + description: + 'description from jira \r\nIncident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + }, + }); + }); + + test('nothing & append', async () => { + mapping.set('title', { + target: 'summary', + actionType: 'nothing', + }); + + mapping.set('description', { + target: 'description', + actionType: 'append', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('summary', { + target: 'title', + actionType: 'nothing', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + description: + 'description from jira \r\nIncident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + }, + }); + }); + + test('append & append', async () => { + mapping.set('title', { + target: 'summary', + actionType: 'append', + }); + + mapping.set('description', { + target: 'description', + actionType: 'append', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('summary', { + target: 'title', + actionType: 'append', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + summary: + 'title from jira \r\nIncident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + description: + 'description from jira \r\nIncident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + }, + }); + }); + + test('nothing & nothing', async () => { + mapping.set('title', { + target: 'summary', + actionType: 'nothing', + }); + + mapping.set('description', { + target: 'description', + actionType: 'nothing', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('summary', { + target: 'title', + actionType: 'nothing', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: {}, + }); + }); + + test('overwrite & nothing', async () => { + mapping.set('title', { + target: 'summary', + actionType: 'overwrite', + }); + + mapping.set('description', { + target: 'description', + actionType: 'nothing', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('summary', { + target: 'title', + actionType: 'overwrite', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + summary: 'Incident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + }, + }); + }); + + test('overwrite & overwrite', async () => { + mapping.set('title', { + target: 'summary', + actionType: 'overwrite', + }); + + mapping.set('description', { + target: 'description', + actionType: 'overwrite', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('summary', { + target: 'title', + actionType: 'overwrite', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + summary: 'Incident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + description: + 'Incident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + }, + }); + }); + + test('nothing & overwrite', async () => { + mapping.set('title', { + target: 'summary', + actionType: 'nothing', + }); + + mapping.set('description', { + target: 'description', + actionType: 'overwrite', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('summary', { + target: 'title', + actionType: 'nothing', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + description: + 'Incident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + }, + }); + }); + + test('append & overwrite', async () => { + mapping.set('title', { + target: 'summary', + actionType: 'append', + }); + + mapping.set('description', { + target: 'description', + actionType: 'overwrite', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('summary', { + target: 'title', + actionType: 'append', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + summary: + 'title from jira \r\nIncident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + description: + 'Incident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + }, + }); + }); + + test('append & nothing', async () => { + mapping.set('title', { + target: 'summary', + actionType: 'append', + }); + + mapping.set('description', { + target: 'description', + actionType: 'nothing', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('summary', { + target: 'title', + actionType: 'append', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + summary: + 'title from jira \r\nIncident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + }, + }); + }); + + test('comment nothing', async () => { + mapping.set('title', { + target: 'summary', + actionType: 'overwrite', + }); + + mapping.set('description', { + target: 'description', + actionType: 'nothing', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'nothing', + }); + + mapping.set('summary', { + target: 'title', + actionType: 'overwrite', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.createComment).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/mocks.ts new file mode 100644 index 00000000000000..a1160ce63807b4 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/mocks.ts @@ -0,0 +1,107 @@ +/* + * 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 { ExternalService, ApiParams, ExecutorActionParams, MapRecord } from '../types'; + +const createMock = (): jest.Mocked => ({ + getIncident: jest.fn().mockImplementation(() => + Promise.resolve({ + id: '1', + key: 'CK-1', + summary: 'title from jira', + description: 'description from jira', + created: '2020-04-27T10:59:46.202Z', + updated: '2020-04-27T10:59:46.202Z', + }) + ), + createIncident: jest.fn().mockImplementation(() => + Promise.resolve({ + id: '1', + title: 'CK-1', + pushedDate: '2020-04-27T10:59:46.202Z', + url: 'https://siem-kibana.atlassian.net/browse/CK-1', + }) + ), + updateIncident: jest.fn().mockImplementation(() => + Promise.resolve({ + id: 'incident-2', + title: 'INC02', + pushedDate: '2020-04-27T10:59:46.202Z', + url: 'https://siem-kibana.atlassian.net/browse/CK-1', + }) + ), + createComment: jest.fn().mockImplementation(() => + Promise.resolve({ + commentId: 'comment-1', + pushedDate: '2020-04-27T10:59:46.202Z', + externalCommentId: '1', + }) + ), +}); + +const externalServiceMock = { + create: createMock, +}; + +const mapping: Map> = new Map(); + +mapping.set('title', { + target: 'summary', + actionType: 'overwrite', +}); + +mapping.set('description', { + target: 'description', + actionType: 'overwrite', +}); + +mapping.set('comments', { + target: 'comments', + actionType: 'append', +}); + +mapping.set('summary', { + target: 'title', + actionType: 'overwrite', +}); + +const executorParams: ExecutorActionParams = { + caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', + externalId: 'incident-3', + createdAt: '2020-04-27T10:59:46.202Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: '2020-04-27T10:59:46.202Z', + updatedBy: { fullName: 'Elastic User', username: 'elastic' }, + title: 'Incident title', + description: 'Incident description', + comments: [ + { + commentId: 'case-comment-1', + version: 'WzU3LDFd', + comment: 'A comment', + createdAt: '2020-04-27T10:59:46.202Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: '2020-04-27T10:59:46.202Z', + updatedBy: { fullName: 'Elastic User', username: 'elastic' }, + }, + { + commentId: 'case-comment-2', + version: 'WlK3LDFd', + comment: 'Another comment', + createdAt: '2020-04-27T10:59:46.202Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: '2020-04-27T10:59:46.202Z', + updatedBy: { fullName: 'Elastic User', username: 'elastic' }, + }, + ], +}; + +const apiParams: ApiParams = { + ...executorParams, + externalCase: { summary: 'Incident title', description: 'Incident description' }, +}; + +export { externalServiceMock, mapping, executorParams, apiParams }; From 0ed84b1c07789295177030cc0913f6c3caa36a12 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 27 Apr 2020 18:36:21 +0300 Subject: [PATCH 20/33] Add integration tests --- .../alerting_api_integration/common/config.ts | 1 + .../plugins/actions/jira_simulation.ts | 101 +++ .../plugins/actions_simulators/index.ts | 9 +- .../servicenow_simulation.ts | 8 +- .../actions/builtin_action_types/jira.ts | 550 +++++++++++++ .../builtin_action_types/servicenow.ts | 736 ++++++++++-------- .../tests/actions/index.ts | 1 + 7 files changed, 1058 insertions(+), 348 deletions(-) create mode 100644 x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/jira_simulation.ts create mode 100644 x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 870ed3cf0cc0fc..72a2774e672f1b 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -23,6 +23,7 @@ const enabledActionTypes = [ '.pagerduty', '.server-log', '.servicenow', + '.jira', '.slack', '.webhook', 'test.authorization', diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/jira_simulation.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/jira_simulation.ts new file mode 100644 index 00000000000000..629d0197b22923 --- /dev/null +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/jira_simulation.ts @@ -0,0 +1,101 @@ +/* + * 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 Hapi from 'hapi'; + +interface JiraRequest extends Hapi.Request { + payload: { + summary: string; + description?: string; + comments?: string; + }; +} +export function initPlugin(server: Hapi.Server, path: string) { + server.route({ + method: 'POST', + path: `${path}/rest/api/2/issue`, + options: { + auth: false, + }, + handler: createHandler as Hapi.Lifecycle.Method, + }); + + server.route({ + method: 'PUT', + path: `${path}/rest/api/2/issue/{id}`, + options: { + auth: false, + }, + handler: updateHandler as Hapi.Lifecycle.Method, + }); + + server.route({ + method: 'GET', + path: `${path}/rest/api/2/issue/{id}`, + options: { + auth: false, + }, + handler: getHandler as Hapi.Lifecycle.Method, + }); + + server.route({ + method: 'POST', + path: `${path}/rest/api/2/issue/{id}/comment`, + options: { + auth: false, + }, + handler: createCommentHanlder as Hapi.Lifecycle.Method, + }); +} + +// ServiceNow simulator: create a servicenow action pointing here, and you can get +// different responses based on the message posted. See the README.md for +// more info. +function createHandler(request: JiraRequest, h: any) { + return jsonResponse(h, 200, { + id: '123', + key: 'CK-1', + created: '2020-04-27T14:17:45.490Z', + }); +} + +function updateHandler(request: JiraRequest, h: any) { + return jsonResponse(h, 200, { + id: '123', + key: 'CK-1', + created: '2020-04-27T14:17:45.490Z', + updated: '2020-04-27T14:17:45.490Z', + }); +} + +function getHandler(request: JiraRequest, h: any) { + return jsonResponse(h, 200, { + id: '123', + key: 'CK-1', + created: '2020-04-27T14:17:45.490Z', + updated: '2020-04-27T14:17:45.490Z', + summary: 'title', + description: 'description', + }); +} + +function createCommentHanlder(request: JiraRequest, h: any) { + return jsonResponse(h, 200, { + id: '123', + created: '2020-04-27T14:17:45.490Z', + }); +} + +function jsonResponse(h: any, code: number, object?: any) { + if (object == null) { + return h.response('').code(code); + } + + return h + .response(JSON.stringify(object)) + .type('application/json') + .code(code); +} diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/index.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/index.ts index 45edd4c092da97..6e7e9e37937786 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/index.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/index.ts @@ -8,15 +8,17 @@ import { PluginSetupContract as ActionsPluginSetupContract } from '../../../../. import { ActionType } from '../../../../../../plugins/actions/server'; import { initPlugin as initPagerduty } from './pagerduty_simulation'; -import { initPlugin as initServiceNow } from './servicenow_simulation'; import { initPlugin as initSlack } from './slack_simulation'; import { initPlugin as initWebhook } from './webhook_simulation'; +import { initPlugin as initServiceNow } from './servicenow_simulation'; +import { initPlugin as initJira } from './jira_simulation'; const NAME = 'actions-FTS-external-service-simulators'; export enum ExternalServiceSimulator { PAGERDUTY = 'pagerduty', SERVICENOW = 'servicenow', + JIRA = 'jira', SLACK = 'slack', WEBHOOK = 'webhook', } @@ -29,7 +31,9 @@ export function getAllExternalServiceSimulatorPaths(): string[] { const allPaths = Object.values(ExternalServiceSimulator).map(service => getExternalServiceSimulatorPath(service) ); + allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.SERVICENOW}/api/now/v2/table/incident`); + allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.JIRA}/rest/api/2/issue`); return allPaths; } @@ -78,9 +82,10 @@ export default function(kibana: any) { }); initPagerduty(server, getExternalServiceSimulatorPath(ExternalServiceSimulator.PAGERDUTY)); - initServiceNow(server, getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW)); initSlack(server, getExternalServiceSimulatorPath(ExternalServiceSimulator.SLACK)); initWebhook(server, getExternalServiceSimulatorPath(ExternalServiceSimulator.WEBHOOK)); + initServiceNow(server, getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW)); + initJira(server, getExternalServiceSimulatorPath(ExternalServiceSimulator.JIRA)); }, }); } diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/servicenow_simulation.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/servicenow_simulation.ts index a58738e387aeb0..cc9521369a47df 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/servicenow_simulation.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/servicenow_simulation.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import Joi from 'joi'; import Hapi from 'hapi'; interface ServiceNowRequest extends Hapi.Request { @@ -29,18 +28,13 @@ export function initPlugin(server: Hapi.Server, path: string) { path: `${path}/api/now/v2/table/incident/{id}`, options: { auth: false, - validate: { - params: Joi.object({ - id: Joi.string(), - }), - }, }, handler: updateHandler as Hapi.Lifecycle.Method, }); server.route({ method: 'GET', - path: `${path}/api/now/v2/table/incident`, + path: `${path}/api/now/v2/table/incident/{id}`, options: { auth: false, }, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts new file mode 100644 index 00000000000000..4c501bac568e3b --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts @@ -0,0 +1,550 @@ +/* + * 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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { + getExternalServiceSimulatorPath, + ExternalServiceSimulator, +} from '../../../../common/fixtures/plugins/actions'; + +const mapping = [ + { + source: 'title', + target: 'summary', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, +]; + +// eslint-disable-next-line import/no-default-export +export default function jiraTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + + const mockJira = { + config: { + apiUrl: 'www.jiraisinkibanaactions.com', + projectKey: 'CK', + casesConfiguration: { mapping }, + }, + secrets: { + apiToken: 'elastic', + email: 'elastic@elastic.co', + }, + params: { + action: 'pushToService', + actionParams: { + caseId: '123', + title: 'a title', + description: 'a description', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + externalId: null, + comments: [ + { + commentId: '456', + version: 'WzU3LDFd', + comment: 'first comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + }, + ], + }, + }, + }; + + let jiraSimulatorURL: string = ''; + + describe('Jira', () => { + before(() => { + jiraSimulatorURL = kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.JIRA) + ); + }); + + after(() => esArchiver.unload('empty_kibana')); + + describe('Jira - Action Creation', () => { + it('should return 200 when creating a jira action successfully', async () => { + const { body: createdAction } = await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A jira action', + actionTypeId: '.jira', + config: { + ...mockJira.config, + apiUrl: jiraSimulatorURL, + }, + secrets: mockJira.secrets, + }) + .expect(200); + + expect(createdAction).to.eql({ + id: createdAction.id, + isPreconfigured: false, + name: 'A jira action', + actionTypeId: '.jira', + config: { + apiUrl: jiraSimulatorURL, + projectKey: mockJira.config.projectKey, + casesConfiguration: mockJira.config.casesConfiguration, + }, + }); + + const { body: fetchedAction } = await supertest + .get(`/api/action/${createdAction.id}`) + .expect(200); + + expect(fetchedAction).to.eql({ + id: fetchedAction.id, + isPreconfigured: false, + name: 'A jira action', + actionTypeId: '.jira', + config: { + apiUrl: jiraSimulatorURL, + projectKey: mockJira.config.projectKey, + casesConfiguration: mockJira.config.casesConfiguration, + }, + }); + }); + + it('should respond with a 400 Bad Request when creating a jira action with no apiUrl', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A jira action', + actionTypeId: '.jira', + config: { projectKey: 'CK' }, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type config: [apiUrl]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should respond with a 400 Bad Request when creating a jira action with no projectKey', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A jira action', + actionTypeId: '.jira', + config: { apiUrl: jiraSimulatorURL }, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type config: [projectKey]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should respond with a 400 Bad Request when creating a jira action with a non whitelisted apiUrl', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A jira action', + actionTypeId: '.jira', + config: { + apiUrl: 'http://jira.mynonexistent.com', + projectKey: mockJira.config.projectKey, + casesConfiguration: mockJira.config.casesConfiguration, + }, + secrets: mockJira.secrets, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type config: error configuring connector action: target url "http://jira.mynonexistent.com" is not whitelisted in the Kibana config xpack.actions.whitelistedHosts', + }); + }); + }); + + it('should respond with a 400 Bad Request when creating a jira action without secrets', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A jira action', + actionTypeId: '.jira', + config: { + apiUrl: jiraSimulatorURL, + projectKey: mockJira.config.projectKey, + casesConfiguration: mockJira.config.casesConfiguration, + }, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type secrets: [email]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should respond with a 400 Bad Request when creating a jira action without casesConfiguration', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A jira action', + actionTypeId: '.jira', + config: { + apiUrl: jiraSimulatorURL, + projectKey: mockJira.config.projectKey, + }, + secrets: mockJira.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 jira action with empty mapping', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A jira action', + actionTypeId: '.jira', + config: { + apiUrl: jiraSimulatorURL, + projectKey: mockJira.config.projectKey, + casesConfiguration: { mapping: [] }, + }, + secrets: mockJira.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 non-empty but got empty', + }); + }); + }); + + it('should respond with a 400 Bad Request when creating a jira action with wrong actionType', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A jira action', + actionTypeId: '.jira', + config: { + apiUrl: jiraSimulatorURL, + projectKey: mockJira.config.projectKey, + casesConfiguration: { + mapping: [ + { + source: 'title', + target: 'description', + actionType: 'non-supported', + }, + ], + }, + }, + secrets: mockJira.secrets, + }) + .expect(400); + }); + }); + + describe('Jira - Executor', () => { + let simulatedActionId: string; + before(async () => { + const { body } = await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A jira simulator', + actionTypeId: '.jira', + config: { + apiUrl: jiraSimulatorURL, + projectKey: mockJira.config.projectKey, + casesConfiguration: mockJira.config.casesConfiguration, + }, + secrets: mockJira.secrets, + }); + simulatedActionId = body.id; + }); + + describe('Validation', () => { + it('should handle failing with a simulated success without action', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: {}, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: [action]: expected at least one defined value but got [undefined]', + }); + }); + }); + + it('should handle failing with a simulated success without unsupported action', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { action: 'non-supported' }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: [action]: types that failed validation:\n- [action.0]: expected value to equal [getIncident]\n- [action.1]: expected value to equal [pushToService]\n- [action.2]: expected value to equal [handshake]', + }); + }); + }); + + it('should handle failing with a simulated success without actionParams', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { action: 'pushToService' }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: [actionParams.caseId]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should handle failing with a simulated success without caseId', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { action: 'pushToService', actionParams: {} }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: [actionParams.caseId]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should handle failing with a simulated success without title', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockJira.params, + actionParams: { + caseId: 'success', + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: [actionParams.title]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should handle failing with a simulated success without createdAt', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockJira.params, + actionParams: { + caseId: 'success', + title: 'success', + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: [actionParams.createdAt]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should handle failing with a simulated success without commentId', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockJira.params, + actionParams: { + ...mockJira.params.actionParams, + caseId: 'success', + title: 'success', + createdAt: 'success', + createdBy: { username: 'elastic' }, + comments: [{}], + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: [actionParams.comments.0.commentId]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should handle failing with a simulated success without comment message', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockJira.params, + actionParams: { + ...mockJira.params.actionParams, + caseId: 'success', + title: 'success', + createdAt: 'success', + createdBy: { username: 'elastic' }, + comments: [{ commentId: 'success' }], + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: [actionParams.comments.0.comment]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should handle failing with a simulated success without comment.createdAt', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockJira.params, + actionParams: { + ...mockJira.params.actionParams, + caseId: '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: [actionParams.comments.0.createdAt]: expected value of type [string] but got [undefined]', + }); + }); + }); + }); + + describe('Execution', () => { + it('should handle creating an incident without comments', async () => { + const { body } = await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockJira.params, + actionParams: { + ...mockJira.params.actionParams, + comments: [], + }, + }, + }) + .expect(200); + + expect(body).to.eql({ + status: 'ok', + actionId: simulatedActionId, + data: { + id: '123', + title: 'CK-1', + pushedDate: '2020-04-27T14:17:45.490Z', + url: `${jiraSimulatorURL}/browse/CK-1`, + }, + }); + }); + }); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts index d277ed519e4a1d..b574bb88845e90 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts @@ -13,8 +13,6 @@ import { ExternalServiceSimulator, } from '../../../../common/fixtures/plugins/actions_simulators'; -// node ../scripts/functional_test_runner.js --grep "servicenow" --config=test/alerting_api_integration/security_and_spaces/config.ts - const mapping = [ { source: 'title', @@ -24,7 +22,7 @@ const mapping = [ { source: 'description', target: 'description', - actionType: 'append', + actionType: 'overwrite', }, { source: 'comments', @@ -42,7 +40,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { const mockServiceNow = { config: { apiUrl: 'www.servicenowisinkibanaactions.com', - casesConfiguration: { mapping: [...mapping] }, + casesConfiguration: { mapping }, }, secrets: { password: 'elastic', @@ -74,11 +72,9 @@ export default function servicenowTest({ getService }: FtrProviderContext) { }, }; - describe('servicenow', () => { - let simulatedActionId = ''; - let servicenowSimulatorURL: string = ''; + let servicenowSimulatorURL: string = ''; - // need to wait for kibanaServer to settle ... + describe('ServiceNow', () => { before(() => { servicenowSimulatorURL = kibanaServer.resolveUrl( getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) @@ -87,377 +83,439 @@ export default function servicenowTest({ getService }: FtrProviderContext) { after(() => esArchiver.unload('empty_kibana')); - it('should return 200 when creating a servicenow action successfully', async () => { - const { body: createdAction } = await supertest - .post('/api/action') - .set('kbn-xsrf', 'foo') - .send({ + describe('ServiceNow - Action Creation', () => { + it('should return 200 when creating a servicenow action successfully', async () => { + const { body: createdAction } = await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A servicenow action', + actionTypeId: '.servicenow', + config: { + apiUrl: servicenowSimulatorURL, + casesConfiguration: mockServiceNow.config.casesConfiguration, + }, + secrets: mockServiceNow.secrets, + }) + .expect(200); + + expect(createdAction).to.eql({ + id: createdAction.id, + isPreconfigured: false, name: 'A servicenow action', actionTypeId: '.servicenow', config: { apiUrl: servicenowSimulatorURL, - casesConfiguration: { ...mockServiceNow.config.casesConfiguration }, + casesConfiguration: mockServiceNow.config.casesConfiguration, }, - secrets: { ...mockServiceNow.secrets }, - }) - .expect(200); - - expect(createdAction).to.eql({ - id: createdAction.id, - isPreconfigured: false, - name: 'A servicenow action', - actionTypeId: '.servicenow', - config: { - apiUrl: servicenowSimulatorURL, - casesConfiguration: { ...mockServiceNow.config.casesConfiguration }, - }, - }); - - expect(typeof createdAction.id).to.be('string'); - - const { body: fetchedAction } = await supertest - .get(`/api/action/${createdAction.id}`) - .expect(200); - - expect(fetchedAction).to.eql({ - id: fetchedAction.id, - isPreconfigured: false, - name: 'A servicenow action', - actionTypeId: '.servicenow', - config: { - apiUrl: servicenowSimulatorURL, - casesConfiguration: { ...mockServiceNow.config.casesConfiguration }, - }, - }); - }); - - it('should respond with a 400 Bad Request when creating a servicenow action with no apiUrl', async () => { - await supertest - .post('/api/action') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A servicenow action', - actionTypeId: '.servicenow', - config: {}, - }) - .expect(400) - .then((resp: any) => { - expect(resp.body).to.eql({ - statusCode: 400, - error: 'Bad Request', - message: - 'error validating action type config: [apiUrl]: expected value of type [string] but got [undefined]', - }); }); - }); - it('should respond with a 400 Bad Request when creating a servicenow action with a non whitelisted apiUrl', async () => { - await supertest - .post('/api/action') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A servicenow action', - actionTypeId: '.servicenow', - config: { - apiUrl: 'http://servicenow.mynonexistent.com', - casesConfiguration: { ...mockServiceNow.config.casesConfiguration }, - }, - secrets: { ...mockServiceNow.secrets }, - }) - .expect(400) - .then((resp: any) => { - expect(resp.body).to.eql({ - statusCode: 400, - error: 'Bad Request', - message: - 'error validating action type config: error configuring connector action: target url "http://servicenow.mynonexistent.com" is not whitelisted in the Kibana config xpack.actions.whitelistedHosts', - }); - }); - }); + const { body: fetchedAction } = await supertest + .get(`/api/action/${createdAction.id}`) + .expect(200); - it('should respond with a 400 Bad Request when creating a servicenow action without secrets', async () => { - await supertest - .post('/api/action') - .set('kbn-xsrf', 'foo') - .send({ + expect(fetchedAction).to.eql({ + id: fetchedAction.id, + isPreconfigured: false, name: 'A servicenow action', actionTypeId: '.servicenow', config: { apiUrl: servicenowSimulatorURL, - casesConfiguration: { ...mockServiceNow.config.casesConfiguration }, + casesConfiguration: mockServiceNow.config.casesConfiguration, }, - }) - .expect(400) - .then((resp: any) => { - expect(resp.body).to.eql({ - statusCode: 400, - error: 'Bad Request', - message: - 'error validating action type secrets: [password]: expected value of type [string] but got [undefined]', - }); }); - }); + }); - it('should respond with a 400 Bad Request when creating a servicenow action without casesConfiguration', async () => { - await supertest - .post('/api/action') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A servicenow action', - actionTypeId: '.servicenow', - config: { - apiUrl: servicenowSimulatorURL, - }, - secrets: { ...mockServiceNow.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 servicenow action with no apiUrl', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A servicenow action', + actionTypeId: '.servicenow', + config: {}, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type config: [apiUrl]: expected value of type [string] but got [undefined]', + }); }); - }); - }); + }); - it('should respond with a 400 Bad Request when creating a servicenow action with empty mapping', async () => { - await supertest - .post('/api/action') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A servicenow action', - actionTypeId: '.servicenow', - config: { - apiUrl: servicenowSimulatorURL, - casesConfiguration: { mapping: [] }, - }, - secrets: { ...mockServiceNow.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 non-empty but got empty', + it('should respond with a 400 Bad Request when creating a servicenow action with a non whitelisted apiUrl', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A servicenow action', + actionTypeId: '.servicenow', + config: { + apiUrl: 'http://servicenow.mynonexistent.com', + casesConfiguration: mockServiceNow.config.casesConfiguration, + }, + secrets: mockServiceNow.secrets, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type config: error configuring connector action: target url "http://servicenow.mynonexistent.com" is not whitelisted in the Kibana config xpack.actions.whitelistedHosts', + }); }); - }); - }); + }); - it('should respond with a 400 Bad Request when creating a servicenow action with wrong actionType', async () => { - await supertest - .post('/api/action') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A servicenow action', - actionTypeId: '.servicenow', - config: { - apiUrl: servicenowSimulatorURL, - casesConfiguration: { - mapping: [ - { - source: 'title', - target: 'description', - actionType: 'non-supported', - }, - ], + it('should respond with a 400 Bad Request when creating a servicenow action without secrets', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A servicenow action', + actionTypeId: '.servicenow', + config: { + apiUrl: servicenowSimulatorURL, + casesConfiguration: mockServiceNow.config.casesConfiguration, }, - }, - secrets: { ...mockServiceNow.secrets }, - }) - .expect(400); - }); - - it('should create our servicenow simulator action successfully', async () => { - const { body: createdSimulatedAction } = await supertest - .post('/api/action') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A servicenow simulator', - actionTypeId: '.servicenow', - config: { - apiUrl: servicenowSimulatorURL, - casesConfiguration: { ...mockServiceNow.config.casesConfiguration }, - }, - secrets: { ...mockServiceNow.secrets }, - }) - .expect(200); + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type secrets: [password]: expected value of type [string] but got [undefined]', + }); + }); + }); - simulatedActionId = createdSimulatedAction.id; - }); + it('should respond with a 400 Bad Request when creating a servicenow action without casesConfiguration', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A servicenow action', + actionTypeId: '.servicenow', + config: { + apiUrl: servicenowSimulatorURL, + }, + secrets: mockServiceNow.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 handle executing with a simulated success', async () => { - const { body: result } = await supertest - .post(`/api/action/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - ...mockServiceNow.params, - actionParams: { ...mockServiceNow.params.actionParams, title: 'success', comments: [] }, - }, - }) - .expect(200); + it('should respond with a 400 Bad Request when creating a servicenow action with empty mapping', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A servicenow action', + actionTypeId: '.servicenow', + config: { + apiUrl: servicenowSimulatorURL, + casesConfiguration: { mapping: [] }, + }, + secrets: mockServiceNow.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 non-empty but got empty', + }); + }); + }); - expect(result).to.eql({ - status: 'ok', - actionId: simulatedActionId, - data: { - id: '123', - title: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: `${servicenowSimulatorURL}/nav_to.do?uri=incident.do?sys_id=123`, - }, + it('should respond with a 400 Bad Request when creating a servicenow action with wrong actionType', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A servicenow action', + actionTypeId: '.servicenow', + config: { + apiUrl: servicenowSimulatorURL, + casesConfiguration: { + mapping: [ + { + source: 'title', + target: 'description', + actionType: 'non-supported', + }, + ], + }, + }, + secrets: mockServiceNow.secrets, + }) + .expect(400); }); }); - it('should handle failing with a simulated success without caseId', async () => { - await supertest - .post(`/api/action/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { action: 'pushToService', actionParams: {} }, - }) - .then((resp: any) => { - expect(resp.body).to.eql({ - actionId: simulatedActionId, - status: 'error', - retry: false, - message: - 'error validating action params: [actionParams.caseId]: expected value of type [string] but got [undefined]', + describe('ServiceNow - Executor', () => { + let simulatedActionId: string; + before(async () => { + const { body } = await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A servicenow simulator', + actionTypeId: '.servicenow', + config: { + apiUrl: servicenowSimulatorURL, + casesConfiguration: mockServiceNow.config.casesConfiguration, + }, + secrets: mockServiceNow.secrets, }); + simulatedActionId = body.id; + }); + + describe('Validation', () => { + it('should handle failing with a simulated success without action', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: {}, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: [action]: expected at least one defined value but got [undefined]', + }); + }); }); - }); - it('should handle failing with a simulated success without title', async () => { - await supertest - .post(`/api/action/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - ...mockServiceNow.params, - actionParams: { - caseId: 'success', - }, - }, - }) - .then((resp: any) => { - expect(resp.body).to.eql({ - actionId: simulatedActionId, - status: 'error', - retry: false, - message: - 'error validating action params: [actionParams.title]: expected value of type [string] but got [undefined]', - }); + it('should handle failing with a simulated success without unsupported action', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { action: 'non-supported' }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: [action]: types that failed validation:\n- [action.0]: expected value to equal [getIncident]\n- [action.1]: expected value to equal [pushToService]\n- [action.2]: expected value to equal [handshake]', + }); + }); }); - }); - it('should handle failing with a simulated success without createdAt', async () => { - await supertest - .post(`/api/action/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - ...mockServiceNow.params, - actionParams: { - caseId: 'success', - title: 'success', - }, - }, - }) - .then((resp: any) => { - expect(resp.body).to.eql({ - actionId: simulatedActionId, - status: 'error', - retry: false, - message: - 'error validating action params: [actionParams.createdAt]: expected value of type [string] but got [undefined]', - }); + it('should handle failing with a simulated success without actionParams', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { action: 'pushToService' }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: [actionParams.caseId]: expected value of type [string] but got [undefined]', + }); + }); }); - }); - it('should handle failing with a simulated success without commentId', async () => { - await supertest - .post(`/api/action/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - ...mockServiceNow.params, - actionParams: { - ...mockServiceNow.params.actionParams, - caseId: 'success', - title: 'success', - createdAt: 'success', - createdBy: { username: 'elastic' }, - comments: [{}], - }, - }, - }) - .then((resp: any) => { - expect(resp.body).to.eql({ - actionId: simulatedActionId, - status: 'error', - retry: false, - message: - 'error validating action params: [actionParams.comments.0.commentId]: expected value of type [string] but got [undefined]', - }); + it('should handle failing with a simulated success without caseId', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { action: 'pushToService', actionParams: {} }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: [actionParams.caseId]: expected value of type [string] but got [undefined]', + }); + }); }); - }); - it('should handle failing with a simulated success without comment message', async () => { - await supertest - .post(`/api/action/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - ...mockServiceNow.params, - actionParams: { - ...mockServiceNow.params.actionParams, - caseId: 'success', - title: 'success', - createdAt: 'success', - createdBy: { username: 'elastic' }, - comments: [{ commentId: 'success' }], - }, - }, - }) - .then((resp: any) => { - expect(resp.body).to.eql({ - actionId: simulatedActionId, - status: 'error', - retry: false, - message: - 'error validating action params: [actionParams.comments.0.comment]: expected value of type [string] but got [undefined]', - }); + it('should handle failing with a simulated success without title', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockServiceNow.params, + actionParams: { + caseId: 'success', + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: [actionParams.title]: expected value of type [string] but got [undefined]', + }); + }); }); - }); - it('should handle failing with a simulated success without comment.createdAt', async () => { - await supertest - .post(`/api/action/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - ...mockServiceNow.params, - actionParams: { - ...mockServiceNow.params.actionParams, - caseId: 'success', - title: 'success', - createdAt: 'success', - createdBy: { username: 'elastic' }, - comments: [{ commentId: 'success', comment: 'success' }], - }, - }, - }) - .then((resp: any) => { - expect(resp.body).to.eql({ + it('should handle failing with a simulated success without createdAt', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockServiceNow.params, + actionParams: { + caseId: 'success', + title: 'success', + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: [actionParams.createdAt]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should handle failing with a simulated success without commentId', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockServiceNow.params, + actionParams: { + ...mockServiceNow.params.actionParams, + caseId: 'success', + title: 'success', + createdAt: 'success', + createdBy: { username: 'elastic' }, + comments: [{}], + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: [actionParams.comments.0.commentId]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should handle failing with a simulated success without comment message', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockServiceNow.params, + actionParams: { + ...mockServiceNow.params.actionParams, + caseId: 'success', + title: 'success', + createdAt: 'success', + createdBy: { username: 'elastic' }, + comments: [{ commentId: 'success' }], + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: [actionParams.comments.0.comment]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should handle failing with a simulated success without comment.createdAt', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockServiceNow.params, + actionParams: { + ...mockServiceNow.params.actionParams, + caseId: '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: [actionParams.comments.0.createdAt]: expected value of type [string] but got [undefined]', + }); + }); + }); + }); + + describe('Execution', () => { + it('should handle creating an incident without comments', async () => { + const { body: result } = await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockServiceNow.params, + actionParams: { + ...mockServiceNow.params.actionParams, + comments: [], + }, + }, + }) + .expect(200); + + expect(result).to.eql({ + status: 'ok', actionId: simulatedActionId, - status: 'error', - retry: false, - message: - 'error validating action params: [actionParams.comments.0.createdAt]: expected value of type [string] but got [undefined]', + data: { + id: '123', + title: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + url: `${servicenowSimulatorURL}/nav_to.do?uri=incident.do?sys_id=123`, + }, }); }); + }); }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts index 8e002bcc8d3daf..18b1714582d131 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts @@ -15,6 +15,7 @@ export default function actionsTests({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./builtin_action_types/pagerduty')); loadTestFile(require.resolve('./builtin_action_types/server_log')); loadTestFile(require.resolve('./builtin_action_types/servicenow')); + loadTestFile(require.resolve('./builtin_action_types/jira')); loadTestFile(require.resolve('./builtin_action_types/slack')); loadTestFile(require.resolve('./builtin_action_types/webhook')); loadTestFile(require.resolve('./create')); From af483b9ebfed44a3ec65e9f53e99c1f6f6a1fa38 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 27 Apr 2020 19:01:28 +0300 Subject: [PATCH 21/33] Refactor structure --- x-pack/plugins/actions/common/types.ts | 4 - .../{connectors => common}/api.ts | 0 .../{connectors => common}/constants.ts | 0 .../{connectors => common}/schema.ts | 0 .../transformers.test.ts | 0 .../{connectors => common}/transformers.ts | 0 .../{connectors => common}/translations.ts | 0 .../{connectors => common}/types.ts | 0 .../{connectors => common}/utils.test.ts | 0 .../{connectors => common}/utils.ts | 0 .../{connectors => common}/validators.ts | 0 .../builtin_action_types/connectors/index.ts | 8 - .../server/builtin_action_types/index.ts | 11 +- .../{connectors => }/jira/api.test.ts | 4 +- .../{connectors/servicenow => jira}/api.ts | 2 +- .../{connectors => }/jira/config.ts | 2 +- .../{connectors => }/jira/index.ts | 4 +- .../{connectors => }/jira/mocks.ts | 2 +- .../{connectors => }/jira/schema.ts | 2 +- .../{connectors => }/jira/service.test.ts | 8 +- .../{connectors => }/jira/service.ts | 4 +- .../{connectors => }/jira/translations.ts | 0 .../{connectors => }/jira/types.ts | 0 .../{connectors => }/jira/validators.ts | 4 +- .../{connectors => }/servicenow/api.test.ts | 4 +- .../{connectors/jira => servicenow}/api.ts | 2 +- .../{connectors => }/servicenow/config.ts | 2 +- .../servicenow/index.test.ts | 268 ------------------ .../{connectors => }/servicenow/index.ts | 6 +- .../{connectors => }/servicenow/mocks.ts | 2 +- .../servicenow/service.test.ts | 8 +- .../{connectors => }/servicenow/service.ts | 4 +- .../servicenow/translations.ts | 0 .../{connectors => }/servicenow/types.ts | 2 +- .../{connectors => }/servicenow/validators.ts | 4 +- .../components/connector_flyout/index.tsx | 2 +- .../siem/public/lib/connectors/jira/index.tsx | 2 +- .../siem/public/lib/connectors/jira/types.ts | 2 +- .../siem/public/lib/connectors/servicenow.tsx | 247 ---------------- .../lib/connectors/servicenow/index.tsx | 2 +- .../public/lib/connectors/servicenow/types.ts | 2 +- .../siem/public/lib/connectors/types.ts | 18 +- .../siem/public/lib/connectors/utils.ts | 2 +- .../components/configure_cases/index.test.tsx | 10 + .../case/components/configure_cases/index.tsx | 13 +- x-pack/plugins/siem/public/plugin.tsx | 3 +- 46 files changed, 61 insertions(+), 599 deletions(-) rename x-pack/plugins/actions/server/builtin_action_types/{connectors => common}/api.ts (100%) rename x-pack/plugins/actions/server/builtin_action_types/{connectors => common}/constants.ts (100%) rename x-pack/plugins/actions/server/builtin_action_types/{connectors => common}/schema.ts (100%) rename x-pack/plugins/actions/server/builtin_action_types/{connectors => common}/transformers.test.ts (100%) rename x-pack/plugins/actions/server/builtin_action_types/{connectors => common}/transformers.ts (100%) rename x-pack/plugins/actions/server/builtin_action_types/{connectors => common}/translations.ts (100%) rename x-pack/plugins/actions/server/builtin_action_types/{connectors => common}/types.ts (100%) rename x-pack/plugins/actions/server/builtin_action_types/{connectors => common}/utils.test.ts (100%) rename x-pack/plugins/actions/server/builtin_action_types/{connectors => common}/utils.ts (100%) rename x-pack/plugins/actions/server/builtin_action_types/{connectors => common}/validators.ts (100%) delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/connectors/index.ts rename x-pack/plugins/actions/server/builtin_action_types/{connectors => }/jira/api.test.ts (99%) rename x-pack/plugins/actions/server/builtin_action_types/{connectors/servicenow => jira}/api.ts (86%) rename x-pack/plugins/actions/server/builtin_action_types/{connectors => }/jira/config.ts (86%) rename x-pack/plugins/actions/server/builtin_action_types/{connectors => }/jira/index.ts (86%) rename x-pack/plugins/actions/server/builtin_action_types/{connectors => }/jira/mocks.ts (98%) rename x-pack/plugins/actions/server/builtin_action_types/{connectors => }/jira/schema.ts (91%) rename x-pack/plugins/actions/server/builtin_action_types/{connectors => }/jira/service.test.ts (97%) rename x-pack/plugins/actions/server/builtin_action_types/{connectors => }/jira/service.ts (97%) rename x-pack/plugins/actions/server/builtin_action_types/{connectors => }/jira/translations.ts (100%) rename x-pack/plugins/actions/server/builtin_action_types/{connectors => }/jira/types.ts (100%) rename x-pack/plugins/actions/server/builtin_action_types/{connectors => }/jira/validators.ts (84%) rename x-pack/plugins/actions/server/builtin_action_types/{connectors => }/servicenow/api.test.ts (99%) rename x-pack/plugins/actions/server/builtin_action_types/{connectors/jira => servicenow}/api.ts (86%) rename x-pack/plugins/actions/server/builtin_action_types/{connectors => }/servicenow/config.ts (86%) delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts rename x-pack/plugins/actions/server/builtin_action_types/{connectors => }/servicenow/index.ts (83%) rename x-pack/plugins/actions/server/builtin_action_types/{connectors => }/servicenow/mocks.ts (98%) rename x-pack/plugins/actions/server/builtin_action_types/{connectors => }/servicenow/service.test.ts (97%) rename x-pack/plugins/actions/server/builtin_action_types/{connectors => }/servicenow/service.ts (98%) rename x-pack/plugins/actions/server/builtin_action_types/{connectors => }/servicenow/translations.ts (100%) rename x-pack/plugins/actions/server/builtin_action_types/{connectors => }/servicenow/types.ts (93%) rename x-pack/plugins/actions/server/builtin_action_types/{connectors => }/servicenow/validators.ts (84%) delete mode 100644 x-pack/plugins/siem/public/lib/connectors/servicenow.tsx diff --git a/x-pack/plugins/actions/common/types.ts b/x-pack/plugins/actions/common/types.ts index f5e61fb0eaeadf..49e8f3e80b14a0 100644 --- a/x-pack/plugins/actions/common/types.ts +++ b/x-pack/plugins/actions/common/types.ts @@ -6,10 +6,6 @@ import { LicenseType } from '../../licensing/common/types'; -export { ConnectorPublicConfigurationType } from '../server/builtin_action_types/connectors/types'; -export { ServiceNowPublicConfigurationType } from '../server/builtin_action_types/connectors/servicenow/types'; -export { ServiceNowSecretConfigurationType } from '../server/builtin_action_types/connectors/servicenow/types'; - export interface ActionType { id: string; name: string; diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/api.ts b/x-pack/plugins/actions/server/builtin_action_types/common/api.ts similarity index 100% rename from x-pack/plugins/actions/server/builtin_action_types/connectors/api.ts rename to x-pack/plugins/actions/server/builtin_action_types/common/api.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/constants.ts b/x-pack/plugins/actions/server/builtin_action_types/common/constants.ts similarity index 100% rename from x-pack/plugins/actions/server/builtin_action_types/connectors/constants.ts rename to x-pack/plugins/actions/server/builtin_action_types/common/constants.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/common/schema.ts similarity index 100% rename from x-pack/plugins/actions/server/builtin_action_types/connectors/schema.ts rename to x-pack/plugins/actions/server/builtin_action_types/common/schema.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/transformers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/common/transformers.test.ts similarity index 100% rename from x-pack/plugins/actions/server/builtin_action_types/connectors/transformers.test.ts rename to x-pack/plugins/actions/server/builtin_action_types/common/transformers.test.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/transformers.ts b/x-pack/plugins/actions/server/builtin_action_types/common/transformers.ts similarity index 100% rename from x-pack/plugins/actions/server/builtin_action_types/connectors/transformers.ts rename to x-pack/plugins/actions/server/builtin_action_types/common/transformers.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/common/translations.ts similarity index 100% rename from x-pack/plugins/actions/server/builtin_action_types/connectors/translations.ts rename to x-pack/plugins/actions/server/builtin_action_types/common/translations.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/types.ts b/x-pack/plugins/actions/server/builtin_action_types/common/types.ts similarity index 100% rename from x-pack/plugins/actions/server/builtin_action_types/connectors/types.ts rename to x-pack/plugins/actions/server/builtin_action_types/common/types.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/common/utils.test.ts similarity index 100% rename from x-pack/plugins/actions/server/builtin_action_types/connectors/utils.test.ts rename to x-pack/plugins/actions/server/builtin_action_types/common/utils.test.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/common/utils.ts similarity index 100% rename from x-pack/plugins/actions/server/builtin_action_types/connectors/utils.ts rename to x-pack/plugins/actions/server/builtin_action_types/common/utils.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/common/validators.ts similarity index 100% rename from x-pack/plugins/actions/server/builtin_action_types/connectors/validators.ts rename to x-pack/plugins/actions/server/builtin_action_types/common/validators.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/index.ts b/x-pack/plugins/actions/server/builtin_action_types/connectors/index.ts deleted file mode 100644 index a6ca6ea199c306..00000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/index.ts +++ /dev/null @@ -1,8 +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 { connector as getServiceNowConnector } from './servicenow'; -export { connector as getJiraConnector } from './jira'; diff --git a/x-pack/plugins/actions/server/builtin_action_types/index.ts b/x-pack/plugins/actions/server/builtin_action_types/index.ts index cc6177f2f5be6d..6ba4d7cfc7de03 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/index.ts @@ -14,9 +14,8 @@ import { getActionType as getPagerDutyActionType } from './pagerduty'; import { getActionType as getServerLogActionType } from './server_log'; import { getActionType as getSlackActionType } from './slack'; import { getActionType as getWebhookActionType } from './webhook'; - -// Connectors -import { getServiceNowConnector, getJiraConnector } from './connectors'; +import { getActionType as getServiceNowActionType } from './servicenow'; +import { getActionType as getJiraActionType } from './jira'; export function registerBuiltInActionTypes({ actionsConfigUtils: configurationUtilities, @@ -33,8 +32,6 @@ export function registerBuiltInActionTypes({ actionTypeRegistry.register(getServerLogActionType({ logger })); actionTypeRegistry.register(getSlackActionType({ configurationUtilities })); actionTypeRegistry.register(getWebhookActionType({ logger, configurationUtilities })); - - // Connectors - actionTypeRegistry.register(getServiceNowConnector({ configurationUtilities })); - actionTypeRegistry.register(getJiraConnector({ configurationUtilities })); + actionTypeRegistry.register(getServiceNowActionType({ configurationUtilities })); + actionTypeRegistry.register(getJiraActionType({ configurationUtilities })); } diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts similarity index 99% rename from x-pack/plugins/actions/server/builtin_action_types/connectors/jira/api.test.ts rename to x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts index b8c56e84ca90ea..d76a9ca781caef 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/api.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { api } from '../api'; +import { api } from '../common/api'; import { externalServiceMock, mapping, apiParams } from './mocks'; -import { ExternalService } from '../types'; +import { ExternalService } from '../common/types'; describe('api', () => { let externalService: jest.Mocked; diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/api.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts similarity index 86% rename from x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/api.ts rename to x-pack/plugins/actions/server/builtin_action_types/jira/api.ts index d0eadcd99d718d..9f8746feee03fe 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { api } from '../api'; +export { api } from '../common/api'; diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/config.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/config.ts similarity index 86% rename from x-pack/plugins/actions/server/builtin_action_types/connectors/jira/config.ts rename to x-pack/plugins/actions/server/builtin_action_types/jira/config.ts index e205a4f3b079bc..518abbb1f5d8fa 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/config.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/config.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ConnectorConfiguration } from '../types'; +import { ConnectorConfiguration } from '../common/types'; import * as i18n from './translations'; export const config: ConnectorConfiguration = { diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/index.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts similarity index 86% rename from x-pack/plugins/actions/server/builtin_action_types/connectors/jira/index.ts rename to x-pack/plugins/actions/server/builtin_action_types/jira/index.ts index 7af531ffd8a18e..f68858b7223715 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createConnector } from '../utils'; +import { createConnector } from '../common/utils'; import { api } from './api'; import { config } from './config'; @@ -12,7 +12,7 @@ import { validate } from './validators'; import { createExternalService } from './service'; import { JiraSecretConfiguration, JiraPublicConfiguration } from './schema'; -export const connector = createConnector({ +export const getActionType = createConnector({ api, config, validate, diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts similarity index 98% rename from x-pack/plugins/actions/server/builtin_action_types/connectors/jira/mocks.ts rename to x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts index a1160ce63807b4..0e3e0262453ee7 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/mocks.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExternalService, ApiParams, ExecutorActionParams, MapRecord } from '../types'; +import { ExternalService, ApiParams, ExecutorActionParams, MapRecord } from '../common/types'; const createMock = (): jest.Mocked => ({ getIncident: jest.fn().mockImplementation(() => diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts similarity index 91% rename from x-pack/plugins/actions/server/builtin_action_types/connectors/jira/schema.ts rename to x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts index 26ba62ca2deefe..9cd9883b988f08 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts @@ -5,7 +5,7 @@ */ import { schema } from '@kbn/config-schema'; -import { ConnectorPublicConfiguration } from '../schema'; +import { ConnectorPublicConfiguration } from '../common/schema'; export const JiraPublicConfiguration = { projectKey: schema.string(), diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts similarity index 97% rename from x-pack/plugins/actions/server/builtin_action_types/connectors/jira/service.test.ts rename to x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts index fbf42a4fae3176..4ec11538ff72f8 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts @@ -7,12 +7,12 @@ import axios from 'axios'; import { createExternalService } from './service'; -import * as utils from '../utils'; -import { ExternalService } from '../types'; +import * as utils from '../common/utils'; +import { ExternalService } from '../common/types'; jest.mock('axios'); -jest.mock('../utils', () => { - const originalUtils = jest.requireActual('../utils'); +jest.mock('../common/utils', () => { + const originalUtils = jest.requireActual('../common/utils'); return { ...originalUtils, request: jest.fn(), diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/service.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts similarity index 97% rename from x-pack/plugins/actions/server/builtin_action_types/connectors/jira/service.ts rename to x-pack/plugins/actions/server/builtin_action_types/jira/service.ts index 72a9189fa90585..ae8b9242a76e7a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts @@ -6,11 +6,11 @@ import axios from 'axios'; -import { ExternalServiceCredential, ExternalService, ExternalServiceParams } from '../types'; +import { ExternalServiceCredential, ExternalService, ExternalServiceParams } from '../common/types'; import { JiraPublicConfigurationType, JiraSecretConfigurationType } from './types'; import * as i18n from './translations'; -import { getErrorMessage, request } from '../utils'; +import { getErrorMessage, request } from '../common/utils'; const VERSION = '2'; const BASE_URL = `rest/api/${VERSION}`; diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/translations.ts similarity index 100% rename from x-pack/plugins/actions/server/builtin_action_types/connectors/jira/translations.ts rename to x-pack/plugins/actions/server/builtin_action_types/jira/translations.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/types.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts similarity index 100% rename from x-pack/plugins/actions/server/builtin_action_types/connectors/jira/types.ts rename to x-pack/plugins/actions/server/builtin_action_types/jira/types.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts similarity index 84% rename from x-pack/plugins/actions/server/builtin_action_types/connectors/jira/validators.ts rename to x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts index aad631aad8ae26..22773d3037b6d8 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/validators.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { validateCommonConfig, validateCommonSecrets } from '../validators'; -import { ConnectorValidation } from '../types'; +import { validateCommonConfig, validateCommonSecrets } from '../common/validators'; +import { ConnectorValidation } from '../common/types'; export const validate: ConnectorValidation = { config: validateCommonConfig, diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts similarity index 99% rename from x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/api.test.ts rename to x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts index 54228f0c3a0254..8663eb39194f6b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/api.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { api } from '../api'; +import { api } from '../common/api'; import { externalServiceMock, mapping, apiParams } from './mocks'; -import { ExternalService } from '../types'; +import { ExternalService } from '../common/types'; describe('api', () => { let externalService: jest.Mocked; diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/api.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts similarity index 86% rename from x-pack/plugins/actions/server/builtin_action_types/connectors/jira/api.ts rename to x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts index d0eadcd99d718d..9f8746feee03fe 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/jira/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { api } from '../api'; +export { api } from '../common/api'; diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/config.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.ts similarity index 86% rename from x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/config.ts rename to x-pack/plugins/actions/server/builtin_action_types/servicenow/config.ts index 3f303f00be1d05..cc1eb811313d84 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/config.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ConnectorConfiguration } from '../types'; +import { ConnectorConfiguration } from '../common/types'; import * as i18n from './translations'; export const config: ConnectorConfiguration = { diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts deleted file mode 100644 index a6c3ae88765ac0..00000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts +++ /dev/null @@ -1,268 +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 { getActionType } from '.'; -import { ActionType, Services, ActionTypeExecutorOptions } from '../../types'; -import { validateConfig, validateSecrets, validateParams } from '../../lib'; -import { createActionTypeRegistry } from '../index.test'; -import { actionsConfigMock } from '../../actions_config.mock'; -import { actionsMock } from '../../mocks'; - -import { ACTION_TYPE_ID } from './constants'; -import * as i18n from './translations'; - -import { handleIncident } from './action_handlers'; -import { incidentResponse } from './mock'; - -jest.mock('./action_handlers'); - -const handleIncidentMock = handleIncident as jest.Mock; - -const services: Services = actionsMock.createServices(); - -let actionType: ActionType; - -const mockOptions = { - name: 'servicenow-connector', - actionTypeId: '.servicenow', - secrets: { - username: 'secret-username', - password: 'secret-password', - }, - config: { - apiUrl: 'https://service-now.com', - casesConfiguration: { - mapping: [ - { - source: 'title', - target: 'short_description', - actionType: 'overwrite', - }, - { - source: 'description', - target: 'description', - actionType: 'overwrite', - }, - { - source: 'comments', - target: 'work_notes', - actionType: 'append', - }, - ], - }, - }, - params: { - caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', - incidentId: 'ceb5986e079f00100e48fbbf7c1ed06d', - title: 'Incident title', - description: 'Incident description', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - comments: [ - { - commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', - comment: 'A comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - }, - ], - }, -}; - -beforeAll(() => { - const { actionTypeRegistry } = createActionTypeRegistry(); - actionType = actionTypeRegistry.get(ACTION_TYPE_ID); -}); - -describe('get()', () => { - test('should return correct action type', () => { - expect(actionType.id).toEqual(ACTION_TYPE_ID); - expect(actionType.name).toEqual(i18n.NAME); - }); -}); - -describe('validateConfig()', () => { - test('should validate and pass when config is valid', () => { - const { config } = mockOptions; - expect(validateConfig(actionType, config)).toEqual(config); - }); - - test('should validate and throw error when config is invalid', () => { - expect(() => { - validateConfig(actionType, { shouldNotBeHere: true }); - }).toThrowErrorMatchingInlineSnapshot( - `"error validating action type config: [apiUrl]: expected value of type [string] but got [undefined]"` - ); - }); - - test('should validate and pass when the servicenow url is whitelisted', () => { - actionType = getActionType({ - configurationUtilities: { - ...actionsConfigMock.create(), - ensureWhitelistedUri: url => { - expect(url).toEqual(mockOptions.config.apiUrl); - }, - }, - }); - - expect(validateConfig(actionType, mockOptions.config)).toEqual(mockOptions.config); - }); - - test('config validation returns an error if the specified URL isnt whitelisted', () => { - actionType = getActionType({ - configurationUtilities: { - ...actionsConfigMock.create(), - ensureWhitelistedUri: _ => { - throw new Error(`target url is not whitelisted`); - }, - }, - }); - - expect(() => { - validateConfig(actionType, mockOptions.config); - }).toThrowErrorMatchingInlineSnapshot( - `"error validating action type config: error configuring servicenow action: target url is not whitelisted"` - ); - }); -}); - -describe('validateSecrets()', () => { - test('should validate and pass when secrets is valid', () => { - const { secrets } = mockOptions; - expect(validateSecrets(actionType, secrets)).toEqual(secrets); - }); - - test('should validate and throw error when secrets is invalid', () => { - expect(() => { - validateSecrets(actionType, { username: false }); - }).toThrowErrorMatchingInlineSnapshot( - `"error validating action type secrets: [password]: expected value of type [string] but got [undefined]"` - ); - - expect(() => { - validateSecrets(actionType, { username: false, password: 'hello' }); - }).toThrowErrorMatchingInlineSnapshot( - `"error validating action type secrets: [username]: expected value of type [string] but got [boolean]"` - ); - }); -}); - -describe('validateParams()', () => { - test('should validate and pass when params is valid', () => { - const { params } = mockOptions; - expect(validateParams(actionType, params)).toEqual(params); - }); - - test('should validate and throw error when params is invalid', () => { - expect(() => { - validateParams(actionType, {}); - }).toThrowErrorMatchingInlineSnapshot( - `"error validating action params: [caseId]: expected value of type [string] but got [undefined]"` - ); - }); -}); - -describe('execute()', () => { - beforeEach(() => { - handleIncidentMock.mockReset(); - }); - - test('should create an incident', async () => { - const actionId = 'some-id'; - const { incidentId, ...rest } = mockOptions.params; - - const executorOptions: ActionTypeExecutorOptions = { - actionId, - config: mockOptions.config, - params: { ...rest }, - secrets: mockOptions.secrets, - services, - }; - - handleIncidentMock.mockImplementation(() => incidentResponse); - - const actionResponse = await actionType.executor(executorOptions); - expect(actionResponse).toEqual({ actionId, status: 'ok', data: incidentResponse }); - }); - - test('should throw an error when failed to create incident', async () => { - expect.assertions(1); - const { incidentId, ...rest } = mockOptions.params; - - const actionId = 'some-id'; - const executorOptions: ActionTypeExecutorOptions = { - actionId, - config: mockOptions.config, - params: { ...rest }, - secrets: mockOptions.secrets, - services, - }; - const errorMessage = 'Failed to create incident'; - - handleIncidentMock.mockImplementation(() => { - throw new Error(errorMessage); - }); - - try { - await actionType.executor(executorOptions); - } catch (error) { - expect(error.message).toEqual(errorMessage); - } - }); - - test('should update an incident', async () => { - const actionId = 'some-id'; - const executorOptions: ActionTypeExecutorOptions = { - actionId, - config: mockOptions.config, - params: { - ...mockOptions.params, - updatedAt: '2020-03-15T08:34:53.450Z', - updatedBy: { fullName: 'Another User', username: 'anotherUser' }, - }, - secrets: mockOptions.secrets, - services, - }; - - handleIncidentMock.mockImplementation(() => incidentResponse); - - const actionResponse = await actionType.executor(executorOptions); - expect(actionResponse).toEqual({ actionId, status: 'ok', data: incidentResponse }); - }); - - test('should throw an error when failed to update an incident', async () => { - expect.assertions(1); - - const actionId = 'some-id'; - const executorOptions: ActionTypeExecutorOptions = { - actionId, - config: mockOptions.config, - params: { - ...mockOptions.params, - updatedAt: '2020-03-15T08:34:53.450Z', - updatedBy: { fullName: 'Another User', username: 'anotherUser' }, - }, - secrets: mockOptions.secrets, - services, - }; - const errorMessage = 'Failed to update incident'; - - handleIncidentMock.mockImplementation(() => { - throw new Error(errorMessage); - }); - - try { - await actionType.executor(executorOptions); - } catch (error) { - expect(error.message).toEqual(errorMessage); - } - }); -}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts similarity index 83% rename from x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/index.ts rename to x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts index 473a3ed065dfb0..b4af78cfaaf15d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts @@ -4,15 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createConnector } from '../utils'; +import { createConnector } from '../common/utils'; import { api } from './api'; import { config } from './config'; import { validate } from './validators'; import { createExternalService } from './service'; -import { ConnectorPublicConfiguration, ConnectorSecretConfiguration } from '../schema'; +import { ConnectorPublicConfiguration, ConnectorSecretConfiguration } from '../common/schema'; -export const connector = createConnector({ +export const getActionType = createConnector({ api, config, validate, diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts similarity index 98% rename from x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/mocks.ts rename to x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts index 14aea1cc27921e..1e121e4b681114 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/mocks.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExternalService, ApiParams, ExecutorActionParams, MapRecord } from '../types'; +import { ExternalService, ApiParams, ExecutorActionParams, MapRecord } from '../common/types'; const createMock = (): jest.Mocked => ({ getIncident: jest.fn().mockImplementation(() => diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts similarity index 97% rename from x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.test.ts rename to x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts index 4550ea81a55f99..127ffe45d52405 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts @@ -7,12 +7,12 @@ import axios from 'axios'; import { createExternalService } from './service'; -import * as utils from '../utils'; -import { ExternalService } from '../types'; +import * as utils from '../common/utils'; +import { ExternalService } from '../common/types'; jest.mock('axios'); -jest.mock('../utils', () => { - const originalUtils = jest.requireActual('../utils'); +jest.mock('../common/utils', () => { + const originalUtils = jest.requireActual('../common/utils'); return { ...originalUtils, request: jest.fn(), diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts similarity index 98% rename from x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.ts rename to x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts index 04547d5a72c7bd..bfce50e78cc4f5 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts @@ -6,8 +6,8 @@ import axios from 'axios'; -import { ExternalServiceCredential, ExternalService, ExternalServiceParams } from '../types'; -import { addTimeZoneToDate, patch, request, getErrorMessage } from '../utils'; +import { ExternalServiceCredential, ExternalService, ExternalServiceParams } from '../common/types'; +import { addTimeZoneToDate, patch, request, getErrorMessage } from '../common/utils'; import * as i18n from './translations'; import { ServiceNowPublicConfigurationType, ServiceNowSecretConfigurationType } from './types'; diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts similarity index 100% rename from x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/translations.ts rename to x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts similarity index 93% rename from x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/types.ts rename to x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts index 6bcb2c95ccdb58..ccf4fb5076b9a5 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts @@ -7,4 +7,4 @@ export { ConnectorPublicConfigurationType as ServiceNowPublicConfigurationType, ConnectorSecretConfigurationType as ServiceNowSecretConfigurationType, -} from '../types'; +} from '../common/types'; diff --git a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts similarity index 84% rename from x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/validators.ts rename to x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts index aad631aad8ae26..22773d3037b6d8 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/connectors/servicenow/validators.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { validateCommonConfig, validateCommonSecrets } from '../validators'; -import { ConnectorValidation } from '../types'; +import { validateCommonConfig, validateCommonSecrets } from '../common/validators'; +import { ConnectorValidation } from '../common/types'; export const validate: ConnectorValidation = { config: validateCommonConfig, diff --git a/x-pack/plugins/siem/public/lib/connectors/components/connector_flyout/index.tsx b/x-pack/plugins/siem/public/lib/connectors/components/connector_flyout/index.tsx index 2e8d42d09f9d7a..c5a35da56284d9 100644 --- a/x-pack/plugins/siem/public/lib/connectors/components/connector_flyout/index.tsx +++ b/x-pack/plugins/siem/public/lib/connectors/components/connector_flyout/index.tsx @@ -10,7 +10,7 @@ import { EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSpacer } from ' import { isEmpty, get } from 'lodash/fp'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ActionConnectorFieldsProps } from '../../../../../../../../plugins/triggers_actions_ui/public/types'; +import { ActionConnectorFieldsProps } from '../../../../../../triggers_actions_ui/public/types'; import { FieldMapping } from '../../../../pages/case/components/configure_cases/field_mapping'; import { defaultMapping } from '../../config'; diff --git a/x-pack/plugins/siem/public/lib/connectors/jira/index.tsx b/x-pack/plugins/siem/public/lib/connectors/jira/index.tsx index cb0143c6f1275f..ada9608e37c983 100644 --- a/x-pack/plugins/siem/public/lib/connectors/jira/index.tsx +++ b/x-pack/plugins/siem/public/lib/connectors/jira/index.tsx @@ -7,7 +7,7 @@ import { ValidationResult, // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../../plugins/triggers_actions_ui/public/types'; +} from '../../../../../triggers_actions_ui/public/types'; import { connector } from './config'; import { createActionType } from '../utils'; diff --git a/x-pack/plugins/siem/public/lib/connectors/jira/types.ts b/x-pack/plugins/siem/public/lib/connectors/jira/types.ts index d0732954bccd6b..13e4e8f6a289ef 100644 --- a/x-pack/plugins/siem/public/lib/connectors/jira/types.ts +++ b/x-pack/plugins/siem/public/lib/connectors/jira/types.ts @@ -10,7 +10,7 @@ import { JiraPublicConfigurationType, JiraSecretConfigurationType, -} from '../../../../../../../plugins/actions/server/builtin_action_types/connectors/jira/types'; +} from '../../../../../actions/server/builtin_action_types/jira/types'; export interface JiraActionConnector { config: JiraPublicConfigurationType; diff --git a/x-pack/plugins/siem/public/lib/connectors/servicenow.tsx b/x-pack/plugins/siem/public/lib/connectors/servicenow.tsx deleted file mode 100644 index c69e02627f89ca..00000000000000 --- a/x-pack/plugins/siem/public/lib/connectors/servicenow.tsx +++ /dev/null @@ -1,247 +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, ChangeEvent, useEffect } from 'react'; -import { - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiFieldPassword, - EuiSpacer, -} from '@elastic/eui'; - -import { isEmpty, get } from 'lodash/fp'; - -import { - ActionConnectorFieldsProps, - ActionTypeModel, - ValidationResult, - ActionParamsProps, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../triggers_actions_ui/public/types'; - -import { FieldMapping } from '../../../pages/case/components/configure_cases/field_mapping'; - -import * as i18n from './translations'; - -import { ServiceNowActionConnector } from './types'; -import { isUrlInvalid } from '../validators'; - -import { defaultMapping } from '../config'; -import { CasesConfigurationMapping } from '../../../containers/case/configure/types'; - -import { connector } from './config'; -import logo from './logo.svg'; - -interface ServiceNowActionParams { - message: string; -} - -interface Errors { - apiUrl: string[]; - username: string[]; - password: string[]; -} - -export function getActionType(): ActionTypeModel { - return { - id: connector.id, - iconClass: logo, - selectMessage: i18n.SERVICENOW_DESC, - actionTypeTitle: connector.name, - validateConnector: (action: ServiceNowActionConnector): ValidationResult => { - const errors: Errors = { - apiUrl: [], - username: [], - password: [], - }; - - 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]; - } - - if (!action.secrets.username) { - errors.username = [...errors.username, i18n.USERNAME_REQUIRED]; - } - - if (!action.secrets.password) { - errors.password = [...errors.password, i18n.PASSWORD_REQUIRED]; - } - - return { errors }; - }, - validateParams: (actionParams: ServiceNowActionParams): ValidationResult => { - return { errors: {} }; - }, - actionConnectorFields: ServiceNowConnectorFields, - actionParamsFields: ServiceNowParamsFields, - }; -} - -const ServiceNowConnectorFields: React.FunctionComponent> = ({ 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 { username, password } = action.secrets; - - const isApiUrlInvalid: boolean = errors.apiUrl.length > 0 && apiUrl != null; - const isUsernameInvalid: boolean = errors.username.length > 0 && username != null; - const isPasswordInvalid: boolean = errors.password.length > 0 && password != 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)) { - editActionSecrets('username', ''); - editActionSecrets('password', ''); - } - }, []); - - if (isEmpty(mapping)) { - editActionConfig('casesConfiguration', { - ...action.config.casesConfiguration, - mapping: defaultMapping, - }); - } - - const handleOnChangeActionConfig = useCallback( - (key: string, evt: ChangeEvent) => editActionConfig(key, evt.target.value), - [] - ); - - const handleOnBlurActionConfig = useCallback( - (key: string) => { - if (key === 'apiUrl' && action.config[key] == null) { - editActionConfig(key, ''); - } - }, - [action.config] - ); - - const handleOnChangeSecretConfig = useCallback( - (key: string, evt: ChangeEvent) => editActionSecrets(key, evt.target.value), - [] - ); - - const handleOnBlurSecretConfig = useCallback( - (key: string) => { - if (['username', 'password'].includes(key) && get(key, action.secrets) == null) { - editActionSecrets(key, ''); - } - }, - [action.secrets] - ); - - const handleOnChangeMappingConfig = useCallback( - (newMapping: CasesConfigurationMapping[]) => - editActionConfig('casesConfiguration', { - ...action.config.casesConfiguration, - mapping: newMapping, - }), - [action.config] - ); - - return ( - <> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; - -const ServiceNowParamsFields: React.FunctionComponent> = ({ actionParams, editAction, index, errors }) => { - return null; -}; diff --git a/x-pack/plugins/siem/public/lib/connectors/servicenow/index.tsx b/x-pack/plugins/siem/public/lib/connectors/servicenow/index.tsx index 21ca6fc1e9c743..1f8e61b6d3ea7a 100644 --- a/x-pack/plugins/siem/public/lib/connectors/servicenow/index.tsx +++ b/x-pack/plugins/siem/public/lib/connectors/servicenow/index.tsx @@ -7,7 +7,7 @@ import { ValidationResult, // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../triggers_actions_ui/public/types'; +} from '../../../../../triggers_actions_ui/public/types'; import { connector } from './config'; import { createActionType } from '../utils'; diff --git a/x-pack/plugins/siem/public/lib/connectors/servicenow/types.ts b/x-pack/plugins/siem/public/lib/connectors/servicenow/types.ts index e59d4375561c2a..b7f0e79eb37e34 100644 --- a/x-pack/plugins/siem/public/lib/connectors/servicenow/types.ts +++ b/x-pack/plugins/siem/public/lib/connectors/servicenow/types.ts @@ -10,7 +10,7 @@ import { ServiceNowPublicConfigurationType, ServiceNowSecretConfigurationType, -} from '../../../../../../../plugins/actions/common'; +} from '../../../../../actions/server/builtin_action_types/servicenow/types'; export interface ServiceNowActionConnector { config: ServiceNowPublicConfigurationType; diff --git a/x-pack/plugins/siem/public/lib/connectors/types.ts b/x-pack/plugins/siem/public/lib/connectors/types.ts index 8ccbb97f787508..a4e92ab9f33169 100644 --- a/x-pack/plugins/siem/public/lib/connectors/types.ts +++ b/x-pack/plugins/siem/public/lib/connectors/types.ts @@ -4,19 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ActionType } from '../../../../../../plugins/triggers_actions_ui/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ConnectorPublicConfigurationType } from '../../../../../../plugins/actions/common'; - -import { - ConfigType, - SecretsType, -} from '../../../../actions/server/builtin_action_types/servicenow/types'; - -export interface ServiceNowActionConnector { - config: ConfigType; - secrets: SecretsType; -} +/* eslint-disable no-restricted-imports */ +/* eslint-disable @kbn/eslint/no-restricted-paths */ + +import { ActionType } from '../../../../triggers_actions_ui/public'; +import { ConnectorPublicConfigurationType } from '../../../../actions/server/builtin_action_types/common/types'; export interface Connector extends ActionType { logo: string; diff --git a/x-pack/plugins/siem/public/lib/connectors/utils.ts b/x-pack/plugins/siem/public/lib/connectors/utils.ts index b6cf53fdca6ce4..5b5270ade5a654 100644 --- a/x-pack/plugins/siem/public/lib/connectors/utils.ts +++ b/x-pack/plugins/siem/public/lib/connectors/utils.ts @@ -9,7 +9,7 @@ import { ValidationResult, ActionParamsProps, // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../plugins/triggers_actions_ui/public/types'; +} from '../../../../triggers_actions_ui/public/types'; import { ActionConnector, diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.test.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.test.tsx index 545eceb0f73a1b..fde179f3d25fc1 100644 --- a/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.test.tsx +++ b/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.test.tsx @@ -186,6 +186,16 @@ describe('ConfigureCases', () => { id: '.servicenow', name: 'ServiceNow', enabled: true, + logo: 'test-file-stub', + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'platinum', + }, + { + id: '.jira', + name: 'Jira', + logo: 'test-file-stub', + enabled: true, enabledInConfig: true, enabledInLicense: true, minimumLicenseRequired: 'platinum', diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.tsx index 591ca01bcae861..66eef9e3ec7bf8 100644 --- a/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.tsx +++ b/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.tsx @@ -31,11 +31,7 @@ import { import { ActionConnectorTableItem } from '../../../../../../triggers_actions_ui/public/types'; import { getCaseUrl } from '../../../../components/link_to'; import { useGetUrlSearch } from '../../../../components/navigation/use_get_url_search'; -import { - ClosureType, - CasesConfigurationMapping, - CCMapsCombinedActionAttributes, -} from '../../../../containers/case/configure/types'; +import { CCMapsCombinedActionAttributes } from '../../../../containers/case/configure/types'; import { connectorsConfiguration } from '../../../../lib/connectors/config'; import { Connectors } from '../configure_cases/connectors'; @@ -60,13 +56,6 @@ const FormWrapper = styled.div` `} `; -const initialState: State = { - connectorId: 'none', - closureType: 'close-by-user', - mapping: null, - currentConfiguration: { connectorId: 'none', closureType: 'close-by-user' }, -}; - const actionTypes: ActionType[] = Object.values(connectorsConfiguration); interface ConfigureCasesComponentProps { diff --git a/x-pack/plugins/siem/public/plugin.tsx b/x-pack/plugins/siem/public/plugin.tsx index e54c96364ba6b0..1128df282a5c29 100644 --- a/x-pack/plugins/siem/public/plugin.tsx +++ b/x-pack/plugins/siem/public/plugin.tsx @@ -31,7 +31,7 @@ import { SecurityPluginSetup } from '../../security/public'; import { APP_ID, APP_NAME, APP_PATH, APP_ICON } from '../common/constants'; import { initTelemetry } from './lib/telemetry'; import { KibanaServices } from './lib/kibana/services'; -import { serviceNowActionType } from './lib/connectors'; +import { serviceNowActionType, jiraActionType } from './lib/connectors'; export interface SetupPlugins { home: HomePublicPluginSetup; @@ -84,6 +84,7 @@ export class Plugin implements IPlugin { }); plugins.triggers_actions_ui.actionTypeRegistry.register(serviceNowActionType()); + plugins.triggers_actions_ui.actionTypeRegistry.register(jiraActionType()); core.application.register({ id: APP_ID, From 57d610734d5e88778401b0049ef6dbf1934b1b23 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 28 Apr 2020 13:02:08 +0300 Subject: [PATCH 22/33] Fix circular dependencies --- .../builtin_action_types/common/types.ts | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/common/types.ts b/x-pack/plugins/actions/server/builtin_action_types/common/types.ts index c424368c4e91cd..830772d64ab739 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/common/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/common/types.ts @@ -4,6 +4,10 @@ * 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 { @@ -15,8 +19,6 @@ import { CommentSchema, ExecutorActionParamsSchema, } from './schema'; -import { ActionsConfigurationUtilities } from '../../actions_config'; -import { ExecutorType } from '../../types'; export interface AnyParams { [index: string]: string | number | object | null | undefined; @@ -33,8 +35,6 @@ export type MapRecord = TypeOf; export type Comment = TypeOf; export interface ApiParams extends ExecutorActionParams { - // This will have to remain `any` until we can extend connectors with generics - // eslint-disable-next-line @typescript-eslint/no-explicit-any externalCase: Record; } @@ -44,17 +44,13 @@ export interface ConnectorConfiguration { } export interface ExternalServiceCredential { - // eslint-disable-next-line @typescript-eslint/no-explicit-any config: Record; - // eslint-disable-next-line @typescript-eslint/no-explicit-any secrets: Record; } export interface ConnectorValidation { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - config: (configurationUtilities: ActionsConfigurationUtilities, configObject: any) => void; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - secrets: (configurationUtilities: ActionsConfigurationUtilities, secrets: any) => void; + config: (configurationUtilities: any, configObject: any) => void; + secrets: (configurationUtilities: any, secrets: any) => void; } export interface ExternalServiceCaseResponse { @@ -71,12 +67,10 @@ export interface ExternalServiceCommentResponse { } export interface ExternalServiceParams { - // eslint-disable-next-line @typescript-eslint/no-explicit-any [index: string]: any; } export interface ExternalService { - // eslint-disable-next-line @typescript-eslint/no-explicit-any getIncident: (id: string) => Promise; createIncident: (params: ExternalServiceParams) => Promise; updateIncident: (params: ExternalServiceParams) => Promise; @@ -85,7 +79,6 @@ export interface ExternalService { export interface ConnectorApiHandlerArgs { externalService: ExternalService; - // eslint-disable-next-line @typescript-eslint/no-explicit-any mapping: Map; params: ApiParams; } @@ -108,13 +101,12 @@ export interface CreateConnectorBasicArgs { export interface CreateConnectorArgs extends CreateConnectorBasicArgs { config: ConnectorConfiguration; validate: ConnectorValidation; - // eslint-disable-next-line @typescript-eslint/no-explicit-any validationSchema: { config: any; secrets: any }; } export interface CreateActionTypeArgs { - configurationUtilities: ActionsConfigurationUtilities; - executor?: ExecutorType; + configurationUtilities: any; + executor?: any; } export interface PipedField { From aa6fa7cf97c7e65782047f74cba96c35cb25c10f Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 29 Apr 2020 10:54:35 +0300 Subject: [PATCH 23/33] Rename folder --- .../server/builtin_action_types/{common => case}/api.ts | 0 .../builtin_action_types/{common => case}/constants.ts | 0 .../builtin_action_types/{common => case}/schema.ts | 0 .../{common => case}/transformers.test.ts | 0 .../builtin_action_types/{common => case}/transformers.ts | 0 .../builtin_action_types/{common => case}/translations.ts | 0 .../server/builtin_action_types/{common => case}/types.ts | 0 .../builtin_action_types/{common => case}/utils.test.ts | 0 .../server/builtin_action_types/{common => case}/utils.ts | 0 .../builtin_action_types/{common => case}/validators.ts | 0 .../actions/server/builtin_action_types/jira/api.test.ts | 4 ++-- .../actions/server/builtin_action_types/jira/api.ts | 2 +- .../actions/server/builtin_action_types/jira/config.ts | 2 +- .../actions/server/builtin_action_types/jira/index.ts | 2 +- .../actions/server/builtin_action_types/jira/mocks.ts | 2 +- .../actions/server/builtin_action_types/jira/schema.ts | 2 +- .../server/builtin_action_types/jira/service.test.ts | 8 ++++---- .../actions/server/builtin_action_types/jira/service.ts | 4 ++-- .../server/builtin_action_types/jira/validators.ts | 4 ++-- .../server/builtin_action_types/servicenow/api.test.ts | 4 ++-- .../actions/server/builtin_action_types/servicenow/api.ts | 2 +- .../server/builtin_action_types/servicenow/config.ts | 2 +- .../server/builtin_action_types/servicenow/index.ts | 4 ++-- .../server/builtin_action_types/servicenow/mocks.ts | 2 +- .../builtin_action_types/servicenow/service.test.ts | 8 ++++---- .../server/builtin_action_types/servicenow/service.ts | 4 ++-- .../server/builtin_action_types/servicenow/types.ts | 2 +- .../server/builtin_action_types/servicenow/validators.ts | 4 ++-- x-pack/plugins/siem/public/lib/connectors/types.ts | 2 +- 29 files changed, 32 insertions(+), 32 deletions(-) rename x-pack/plugins/actions/server/builtin_action_types/{common => case}/api.ts (100%) rename x-pack/plugins/actions/server/builtin_action_types/{common => case}/constants.ts (100%) rename x-pack/plugins/actions/server/builtin_action_types/{common => case}/schema.ts (100%) rename x-pack/plugins/actions/server/builtin_action_types/{common => case}/transformers.test.ts (100%) rename x-pack/plugins/actions/server/builtin_action_types/{common => case}/transformers.ts (100%) rename x-pack/plugins/actions/server/builtin_action_types/{common => case}/translations.ts (100%) rename x-pack/plugins/actions/server/builtin_action_types/{common => case}/types.ts (100%) rename x-pack/plugins/actions/server/builtin_action_types/{common => case}/utils.test.ts (100%) rename x-pack/plugins/actions/server/builtin_action_types/{common => case}/utils.ts (100%) rename x-pack/plugins/actions/server/builtin_action_types/{common => case}/validators.ts (100%) diff --git a/x-pack/plugins/actions/server/builtin_action_types/common/api.ts b/x-pack/plugins/actions/server/builtin_action_types/case/api.ts similarity index 100% rename from x-pack/plugins/actions/server/builtin_action_types/common/api.ts rename to x-pack/plugins/actions/server/builtin_action_types/case/api.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/common/constants.ts b/x-pack/plugins/actions/server/builtin_action_types/case/constants.ts similarity index 100% rename from x-pack/plugins/actions/server/builtin_action_types/common/constants.ts rename to x-pack/plugins/actions/server/builtin_action_types/case/constants.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/common/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/case/schema.ts similarity index 100% rename from x-pack/plugins/actions/server/builtin_action_types/common/schema.ts rename to x-pack/plugins/actions/server/builtin_action_types/case/schema.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/common/transformers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/case/transformers.test.ts similarity index 100% rename from x-pack/plugins/actions/server/builtin_action_types/common/transformers.test.ts rename to x-pack/plugins/actions/server/builtin_action_types/case/transformers.test.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/common/transformers.ts b/x-pack/plugins/actions/server/builtin_action_types/case/transformers.ts similarity index 100% rename from x-pack/plugins/actions/server/builtin_action_types/common/transformers.ts rename to x-pack/plugins/actions/server/builtin_action_types/case/transformers.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/common/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/case/translations.ts similarity index 100% rename from x-pack/plugins/actions/server/builtin_action_types/common/translations.ts rename to x-pack/plugins/actions/server/builtin_action_types/case/translations.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/common/types.ts b/x-pack/plugins/actions/server/builtin_action_types/case/types.ts similarity index 100% rename from x-pack/plugins/actions/server/builtin_action_types/common/types.ts rename to x-pack/plugins/actions/server/builtin_action_types/case/types.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/common/utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/case/utils.test.ts similarity index 100% rename from x-pack/plugins/actions/server/builtin_action_types/common/utils.test.ts rename to x-pack/plugins/actions/server/builtin_action_types/case/utils.test.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/common/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts similarity index 100% rename from x-pack/plugins/actions/server/builtin_action_types/common/utils.ts rename to x-pack/plugins/actions/server/builtin_action_types/case/utils.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/common/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/case/validators.ts similarity index 100% rename from x-pack/plugins/actions/server/builtin_action_types/common/validators.ts rename to x-pack/plugins/actions/server/builtin_action_types/case/validators.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts index d76a9ca781caef..f5534e34435f67 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { api } from '../common/api'; +import { api } from '../case/api'; import { externalServiceMock, mapping, apiParams } from './mocks'; -import { ExternalService } from '../common/types'; +import { ExternalService } from '../case/types'; describe('api', () => { let externalService: jest.Mocked; 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 9f8746feee03fe..3db66e5884af4a 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,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { api } from '../common/api'; +export { api } from '../case/api'; diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/config.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/config.ts index 518abbb1f5d8fa..9f9922e74d2351 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/config.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/config.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ConnectorConfiguration } from '../common/types'; +import { ConnectorConfiguration } from '../case/types'; import * as i18n from './translations'; export const config: ConnectorConfiguration = { diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts index f68858b7223715..a2d7bb5930a754 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createConnector } from '../common/utils'; +import { createConnector } from '../case/utils'; import { api } from './api'; import { config } from './config'; 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 0e3e0262453ee7..4aeca922f17630 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 @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExternalService, ApiParams, ExecutorActionParams, MapRecord } from '../common/types'; +import { ExternalService, ApiParams, ExecutorActionParams, MapRecord } from '../case/types'; const createMock = (): jest.Mocked => ({ getIncident: jest.fn().mockImplementation(() => 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 9cd9883b988f08..a61d936ada53c1 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,7 +5,7 @@ */ import { schema } from '@kbn/config-schema'; -import { ConnectorPublicConfiguration } from '../common/schema'; +import { ConnectorPublicConfiguration } from '../case/schema'; export const JiraPublicConfiguration = { projectKey: schema.string(), diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts index 4ec11538ff72f8..b9225b043d526e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts @@ -7,12 +7,12 @@ import axios from 'axios'; import { createExternalService } from './service'; -import * as utils from '../common/utils'; -import { ExternalService } from '../common/types'; +import * as utils from '../case/utils'; +import { ExternalService } from '../case/types'; jest.mock('axios'); -jest.mock('../common/utils', () => { - const originalUtils = jest.requireActual('../common/utils'); +jest.mock('../case/utils', () => { + const originalUtils = jest.requireActual('../case/utils'); return { ...originalUtils, request: jest.fn(), diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts index ae8b9242a76e7a..c222b628f676bd 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts @@ -6,11 +6,11 @@ import axios from 'axios'; -import { ExternalServiceCredential, ExternalService, ExternalServiceParams } from '../common/types'; +import { ExternalServiceCredential, ExternalService, ExternalServiceParams } from '../case/types'; import { JiraPublicConfigurationType, JiraSecretConfigurationType } from './types'; import * as i18n from './translations'; -import { getErrorMessage, request } from '../common/utils'; +import { getErrorMessage, request } from '../case/utils'; const VERSION = '2'; const BASE_URL = `rest/api/${VERSION}`; diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts index 22773d3037b6d8..cb71e22e366cb6 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { validateCommonConfig, validateCommonSecrets } from '../common/validators'; -import { ConnectorValidation } from '../common/types'; +import { validateCommonConfig, validateCommonSecrets } from '../case/validators'; +import { ConnectorValidation } from '../case/types'; export const validate: ConnectorValidation = { config: validateCommonConfig, diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts index 8663eb39194f6b..e95771a5d51c70 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { api } from '../common/api'; +import { api } from '../case/api'; import { externalServiceMock, mapping, apiParams } from './mocks'; -import { ExternalService } from '../common/types'; +import { ExternalService } from '../case/types'; describe('api', () => { let externalService: jest.Mocked; 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 9f8746feee03fe..3db66e5884af4a 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 @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { api } from '../common/api'; +export { api } from '../case/api'; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.ts index cc1eb811313d84..b9022b0481aa2b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ConnectorConfiguration } from '../common/types'; +import { ConnectorConfiguration } from '../case/types'; import * as i18n from './translations'; export const config: ConnectorConfiguration = { diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts index b4af78cfaaf15d..c901f0025d7b04 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createConnector } from '../common/utils'; +import { createConnector } from '../case/utils'; import { api } from './api'; import { config } from './config'; import { validate } from './validators'; import { createExternalService } from './service'; -import { ConnectorPublicConfiguration, ConnectorSecretConfiguration } from '../common/schema'; +import { ConnectorPublicConfiguration, ConnectorSecretConfiguration } from '../case/schema'; export const getActionType = createConnector({ api, 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 1e121e4b681114..01bec4f110c1ee 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 @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExternalService, ApiParams, ExecutorActionParams, MapRecord } from '../common/types'; +import { ExternalService, ApiParams, ExecutorActionParams, MapRecord } from '../case/types'; const createMock = (): jest.Mocked => ({ getIncident: jest.fn().mockImplementation(() => diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts index 127ffe45d52405..f65cd5430560ed 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts @@ -7,12 +7,12 @@ import axios from 'axios'; import { createExternalService } from './service'; -import * as utils from '../common/utils'; -import { ExternalService } from '../common/types'; +import * as utils from '../case/utils'; +import { ExternalService } from '../case/types'; jest.mock('axios'); -jest.mock('../common/utils', () => { - const originalUtils = jest.requireActual('../common/utils'); +jest.mock('../case/utils', () => { + const originalUtils = jest.requireActual('../case/utils'); return { ...originalUtils, request: jest.fn(), diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts index bfce50e78cc4f5..4b7d28fa27b151 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts @@ -6,8 +6,8 @@ import axios from 'axios'; -import { ExternalServiceCredential, ExternalService, ExternalServiceParams } from '../common/types'; -import { addTimeZoneToDate, patch, request, getErrorMessage } from '../common/utils'; +import { ExternalServiceCredential, ExternalService, ExternalServiceParams } from '../case/types'; +import { addTimeZoneToDate, patch, request, getErrorMessage } from '../case/utils'; import * as i18n from './translations'; import { ServiceNowPublicConfigurationType, ServiceNowSecretConfigurationType } from './types'; 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 ccf4fb5076b9a5..c46d0e80518de2 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 @@ -7,4 +7,4 @@ export { ConnectorPublicConfigurationType as ServiceNowPublicConfigurationType, ConnectorSecretConfigurationType as ServiceNowSecretConfigurationType, -} from '../common/types'; +} from '../case/types'; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts index 22773d3037b6d8..cb71e22e366cb6 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { validateCommonConfig, validateCommonSecrets } from '../common/validators'; -import { ConnectorValidation } from '../common/types'; +import { validateCommonConfig, validateCommonSecrets } from '../case/validators'; +import { ConnectorValidation } from '../case/types'; export const validate: ConnectorValidation = { config: validateCommonConfig, diff --git a/x-pack/plugins/siem/public/lib/connectors/types.ts b/x-pack/plugins/siem/public/lib/connectors/types.ts index a4e92ab9f33169..8dfe7b54314ac0 100644 --- a/x-pack/plugins/siem/public/lib/connectors/types.ts +++ b/x-pack/plugins/siem/public/lib/connectors/types.ts @@ -8,7 +8,7 @@ /* eslint-disable @kbn/eslint/no-restricted-paths */ import { ActionType } from '../../../../triggers_actions_ui/public'; -import { ConnectorPublicConfigurationType } from '../../../../actions/server/builtin_action_types/common/types'; +import { ConnectorPublicConfigurationType } from '../../../../actions/server/builtin_action_types/case/types'; export interface Connector extends ActionType { logo: string; From aee3e4703edcbe415f0d94e4743ed2d4982e82d8 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 29 Apr 2020 11:01:22 +0300 Subject: [PATCH 24/33] Change translation's keys --- .../builtin_action_types/case/translations.ts | 21 ++++++++----------- .../builtin_action_types/jira/translations.ts | 2 +- .../servicenow/translations.ts | 2 +- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 5 files changed, 11 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/case/translations.ts index d1a95937040d52..525c4523d9ae78 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/case/translations.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/case/translations.ts @@ -6,12 +6,9 @@ import { i18n } from '@kbn/i18n'; -export const API_URL_REQUIRED = i18n.translate( - 'xpack.actions.builtin.connector.connectorApiNullError', - { - defaultMessage: 'connector [apiUrl] is required', - } -); +export const API_URL_REQUIRED = i18n.translate('xpack.actions.builtin.case.connectorApiNullError', { + defaultMessage: 'connector [apiUrl] is required', +}); export const FIELD_INFORMATION = ( mode: string, @@ -20,34 +17,34 @@ export const FIELD_INFORMATION = ( ) => { switch (mode) { case 'create': - return i18n.translate('xpack.actions.builtin.connector.common.informationCreated', { + return i18n.translate('xpack.actions.builtin.case.common.informationCreated', { values: { date, user }, defaultMessage: '(created at {date} by {user})', }); case 'update': - return i18n.translate('xpack.actions.builtin.connector.common.informationUpdated', { + return i18n.translate('xpack.actions.builtin.case.common.informationUpdated', { values: { date, user }, defaultMessage: '(updated at {date} by {user})', }); case 'add': - return i18n.translate('xpack.actions.builtin.connector.common.informationAdded', { + return i18n.translate('xpack.actions.builtin.case.common.informationAdded', { values: { date, user }, defaultMessage: '(added at {date} by {user})', }); default: - return i18n.translate('xpack.actions.builtin.connector.common.informationDefault', { + return i18n.translate('xpack.actions.builtin.case.common.informationDefault', { values: { date, user }, defaultMessage: '(created at {date} by {user})', }); } }; -export const MAPPING_EMPTY = i18n.translate('xpack.actions.builtin.connector.common.emptyMapping', { +export const MAPPING_EMPTY = i18n.translate('xpack.actions.builtin.case.common.emptyMapping', { defaultMessage: '[casesConfiguration.mapping]: expected non-empty but got empty', }); export const WHITE_LISTED_ERROR = (message: string) => - i18n.translate('xpack.actions.builtin.connector.common.apiWhitelistError', { + i18n.translate('xpack.actions.builtin.case.common.apiWhitelistError', { defaultMessage: 'error configuring connector action: {message}', values: { message, diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/translations.ts index 88e5eccd57305a..dae0d75952e11a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/translations.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/translations.ts @@ -6,6 +6,6 @@ import { i18n } from '@kbn/i18n'; -export const NAME = i18n.translate('xpack.actions.builtin.jiraTitle', { +export const NAME = i18n.translate('xpack.actions.builtin.case.jiraTitle', { defaultMessage: 'Jira', }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts index d4ebd4c38cee68..3d6138169c4cc2 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts @@ -6,6 +6,6 @@ import { i18n } from '@kbn/i18n'; -export const NAME = i18n.translate('xpack.actions.builtin.servicenowTitle', { +export const NAME = i18n.translate('xpack.actions.builtin.case.servicenowTitle', { defaultMessage: 'ServiceNow', }); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 9e2d0f757ee2f1..1041e9ee488204 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -3998,7 +3998,6 @@ "xpack.actions.builtin.pagerdutyTitle": "PagerDuty", "xpack.actions.builtin.serverLog.errorLoggingErrorMessage": "メッセージのロギングエラー", "xpack.actions.builtin.serverLogTitle": "サーバーログ", - "xpack.actions.builtin.servicenowTitle": "ServiceNow", "xpack.actions.builtin.slack.errorPostingErrorMessage": "slack メッセージの投稿エラー", "xpack.actions.builtin.slack.errorPostingRetryDateErrorMessage": "slack メッセージの投稿エラー、 {retryString} に再試行", "xpack.actions.builtin.slack.errorPostingRetryLaterErrorMessage": "slack メッセージの投稿エラー、後ほど再試行", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 9df72c9d881cf8..87f1f81c531768 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -3999,7 +3999,6 @@ "xpack.actions.builtin.pagerdutyTitle": "PagerDuty", "xpack.actions.builtin.serverLog.errorLoggingErrorMessage": "记录消息时出错", "xpack.actions.builtin.serverLogTitle": "服务器日志", - "xpack.actions.builtin.servicenowTitle": "ServiceNow", "xpack.actions.builtin.slack.errorPostingErrorMessage": "发布 slack 消息时出错", "xpack.actions.builtin.slack.errorPostingRetryDateErrorMessage": "发布 slack 消息时出错,在 {retryString} 重试", "xpack.actions.builtin.slack.errorPostingRetryLaterErrorMessage": "发布 slack 消息时出错,稍后重试", From 329d0835092671c209898a8a3b51f621fee193d6 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 29 Apr 2020 11:27:41 +0300 Subject: [PATCH 25/33] Change naming for sub actions --- x-pack/plugins/actions/README.md | 31 ++++++------ .../builtin_action_types/case/schema.ts | 4 +- .../server/builtin_action_types/case/utils.ts | 12 ++--- .../siem/public/containers/case/api.test.tsx | 4 +- .../siem/public/containers/case/api.ts | 4 +- .../actions/builtin_action_types/jira.ts | 50 +++++++++---------- .../builtin_action_types/servicenow.ts | 50 +++++++++---------- 7 files changed, 79 insertions(+), 76 deletions(-) diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index 1e5f84b022bdcc..4c8cc3aa503e6e 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -64,12 +64,12 @@ Table of Contents - [`config`](#config-6) - [`secrets`](#secrets-6) - [`params`](#params-6) - - [`actionParams (pushToService)`](#actionparams-pushtoservice) + - [`subActionParams (pushToService)`](#subactionparams-pushtoservice) - [Jira](#jira) - [`config`](#config-7) - [`secrets`](#secrets-7) - [`params`](#params-7) - - [`actionParams (pushToService)`](#actionparams-pushtoservice-1) + - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-1) - [Command Line Utility](#command-line-utility) ## Terminology @@ -149,8 +149,8 @@ This is the primary function for an action type. Whenever the action needs to ex | actionId | The action saved object id that the action type is executing for. | | config | The decrypted configuration given to an action. This comes from the action saved object that is partially or fully encrypted within the data store. If you would like to validate the config before being passed to the executor, define `validate.config` within the action type. | | params | Parameters for the execution. These will be given at execution time by either an alert or manually provided when calling the plugin provided execute function. | -| services.callCluster(path, opts) | Use this to do Elasticsearch queries on the cluster Kibana connects to. This function is the same as any other `callCluster` in Kibana but runs in the context of the user who is calling the action when security is enabled.| -| services.getScopedCallCluster | This function scopes an instance of CallCluster by returning a `callCluster(path, opts)` function that runs in the context of the user who is calling the action when security is enabled. This must only be called with instances of CallCluster provided by core.| +| services.callCluster(path, opts) | Use this to do Elasticsearch queries on the cluster Kibana connects to. This function is the same as any other `callCluster` in Kibana but runs in the context of the user who is calling the action when security is enabled. | +| services.getScopedCallCluster | This function scopes an instance of CallCluster by returning a `callCluster(path, opts)` function that runs in the context of the user who is calling the action when security is enabled. This must only be called with instances of CallCluster provided by core. | | services.savedObjectsClient | This is an instance of the saved objects client. This provides the ability to do CRUD on any saved objects within the same space the alert lives in.

The scope of the saved objects client is tied to the user in context calling the execute API or the API key provided to the execute plugin function (only when security isenabled). | | services.log(tags, [data], [timestamp]) | Use this to create server logs. (This is the same function as server.log) | @@ -489,12 +489,12 @@ The ServiceNow action uses the [V2 Table API](https://developer.servicenow.com/a ### `params` -| Property | Description | Type | -| ------------ | -------------------------------------------------------------------------------- | ------ | -| action | The action to perform. It can be `pushToService`, `handshake`, and `getIncident` | string | -| actionParams | The parameters of the action | object | +| Property | Description | Type | +| --------------- | ------------------------------------------------------------------------------------ | ------ | +| subAction | The sub action to perform. It can be `pushToService`, `handshake`, and `getIncident` | string | +| subActionParams | The parameters of the sub action | object | -#### `actionParams (pushToService)` +#### `subActionParams (pushToService)` | Property | Description | Type | | ----------- | -------------------------------------------------------------------------------------------------------------------------- | --------------------- | @@ -504,10 +504,9 @@ The ServiceNow action uses the [V2 Table API](https://developer.servicenow.com/a | 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)_ | - --- -## Jira +## Jira ID: `.jira` @@ -529,12 +528,12 @@ The Jira action uses the [V2 API](https://developer.atlassian.com/cloud/jira/pla ### `params` -| Property | Description | Type | -| ------------ | -------------------------------------------------------------------------------- | ------ | -| action | The action to perform. It can be `pushToService`, `handshake`, and `getIncident` | string | -| actionParams | The parameters of the action | object | +| Property | Description | Type | +| --------------- | ------------------------------------------------------------------------------------ | ------ | +| subAction | The sub action to perform. It can be `pushToService`, `handshake`, and `getIncident` | string | +| subActionParams | The parameters of the sub action | object | -#### `actionParams (pushToService)` +#### `subActionParams (pushToService)` | Property | Description | Type | | ----------- | ------------------------------------------------------------------------------------------------------------------- | --------------------- | 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 5a2d4f8f323e3d..1f23ae6492b631 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 @@ -75,6 +75,6 @@ export const ExecutorActionParams = { export const ExecutorActionParamsSchema = schema.object(ExecutorActionParams); export const ExecutorParamsSchema = schema.object({ - action: ExecutorActionSchema, - actionParams: ExecutorActionParamsSchema, + subAction: ExecutorActionSchema, + subActionParams: ExecutorActionParamsSchema, }); 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 5f4b55badbd346..effd16f7041d58 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 @@ -72,8 +72,8 @@ export const createConnectorExecutor = ({ } = execOptions.config as ConnectorPublicConfigurationType; const params = execOptions.params as ExecutorParams; - const { action, actionParams } = params; - const { comments, externalId, ...restParams } = actionParams; + const { subAction, subActionParams } = params; + const { comments, externalId, ...restParams } = subActionParams; const mapping = buildMap(configurationMapping); const externalCase = mapParams(restParams as ExecutorActionParams, mapping); @@ -89,14 +89,14 @@ export const createConnectorExecutor = ({ secrets: execOptions.secrets, }); - if (!api[action]) { - throw new Error('[Action][Connector] Unsupported action type'); + if (!api[subAction]) { + throw new Error('[Action][Connector] Unsupported subAction type'); } - const data = await api[action]({ + const data = await api[subAction]({ externalService, mapping, - params: { ...actionParams, externalCase }, + params: { ...subActionParams, externalCase }, }); return { diff --git a/x-pack/plugins/siem/public/containers/case/api.test.tsx b/x-pack/plugins/siem/public/containers/case/api.test.tsx index bf1cfc8062bf3e..174738098fa107 100644 --- a/x-pack/plugins/siem/public/containers/case/api.test.tsx +++ b/x-pack/plugins/siem/public/containers/case/api.test.tsx @@ -418,7 +418,9 @@ describe('Case Configuration API', () => { await pushToService(connectorId, casePushParams, abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith(`/api/action/${connectorId}/_execute`, { method: 'POST', - body: JSON.stringify({ params: { action: 'pushToService', actionParams: casePushParams } }), + body: JSON.stringify({ + params: { subAction: 'pushToService', subActionParams: casePushParams }, + }), signal: abortCtrl.signal, }); }); diff --git a/x-pack/plugins/siem/public/containers/case/api.ts b/x-pack/plugins/siem/public/containers/case/api.ts index 72fbf77defab9b..438eae9d88a448 100644 --- a/x-pack/plugins/siem/public/containers/case/api.ts +++ b/x-pack/plugins/siem/public/containers/case/api.ts @@ -245,7 +245,9 @@ export const pushToService = async ( `${ACTION_URL}/${connectorId}/_execute`, { method: 'POST', - body: JSON.stringify({ params: { action: 'pushToService', actionParams: casePushParams } }), + body: JSON.stringify({ + params: { subAction: 'pushToService', subActionParams: casePushParams }, + }), signal, } ); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts index 4c501bac568e3b..c6a6db1ec451a9 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts @@ -48,8 +48,8 @@ export default function jiraTest({ getService }: FtrProviderContext) { email: 'elastic@elastic.co', }, params: { - action: 'pushToService', - actionParams: { + subAction: 'pushToService', + subActionParams: { caseId: '123', title: 'a title', description: 'a description', @@ -326,7 +326,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [action]: expected at least one defined value but got [undefined]', + 'error validating action params: [subAction]: expected at least one defined value but got [undefined]', }); }); }); @@ -336,7 +336,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { .post(`/api/action/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ - params: { action: 'non-supported' }, + params: { subAction: 'non-supported' }, }) .then((resp: any) => { expect(resp.body).to.eql({ @@ -344,17 +344,17 @@ export default function jiraTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [action]: types that failed validation:\n- [action.0]: expected value to equal [getIncident]\n- [action.1]: expected value to equal [pushToService]\n- [action.2]: expected value to equal [handshake]', + 'error validating action params: [subAction]: types that failed validation:\n- [subAction.0]: expected value to equal [getIncident]\n- [subAction.1]: expected value to equal [pushToService]\n- [subAction.2]: expected value to equal [handshake]', }); }); }); - it('should handle failing with a simulated success without actionParams', async () => { + it('should handle failing with a simulated success without subActionParams', async () => { await supertest .post(`/api/action/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ - params: { action: 'pushToService' }, + params: { subAction: 'pushToService' }, }) .then((resp: any) => { expect(resp.body).to.eql({ @@ -362,7 +362,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [actionParams.caseId]: expected value of type [string] but got [undefined]', + 'error validating action params: [subActionParams.caseId]: expected value of type [string] but got [undefined]', }); }); }); @@ -372,7 +372,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { .post(`/api/action/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ - params: { action: 'pushToService', actionParams: {} }, + params: { subAction: 'pushToService', subActionParams: {} }, }) .then((resp: any) => { expect(resp.body).to.eql({ @@ -380,7 +380,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [actionParams.caseId]: expected value of type [string] but got [undefined]', + 'error validating action params: [subActionParams.caseId]: expected value of type [string] but got [undefined]', }); }); }); @@ -392,7 +392,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { .send({ params: { ...mockJira.params, - actionParams: { + subActionParams: { caseId: 'success', }, }, @@ -403,7 +403,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [actionParams.title]: expected value of type [string] but got [undefined]', + 'error validating action params: [subActionParams.title]: expected value of type [string] but got [undefined]', }); }); }); @@ -415,7 +415,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { .send({ params: { ...mockJira.params, - actionParams: { + subActionParams: { caseId: 'success', title: 'success', }, @@ -427,7 +427,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [actionParams.createdAt]: expected value of type [string] but got [undefined]', + 'error validating action params: [subActionParams.createdAt]: expected value of type [string] but got [undefined]', }); }); }); @@ -439,8 +439,8 @@ export default function jiraTest({ getService }: FtrProviderContext) { .send({ params: { ...mockJira.params, - actionParams: { - ...mockJira.params.actionParams, + subActionParams: { + ...mockJira.params.subActionParams, caseId: 'success', title: 'success', createdAt: 'success', @@ -455,7 +455,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [actionParams.comments.0.commentId]: expected value of type [string] but got [undefined]', + 'error validating action params: [subActionParams.comments.0.commentId]: expected value of type [string] but got [undefined]', }); }); }); @@ -467,8 +467,8 @@ export default function jiraTest({ getService }: FtrProviderContext) { .send({ params: { ...mockJira.params, - actionParams: { - ...mockJira.params.actionParams, + subActionParams: { + ...mockJira.params.subActionParams, caseId: 'success', title: 'success', createdAt: 'success', @@ -483,7 +483,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [actionParams.comments.0.comment]: expected value of type [string] but got [undefined]', + 'error validating action params: [subActionParams.comments.0.comment]: expected value of type [string] but got [undefined]', }); }); }); @@ -495,8 +495,8 @@ export default function jiraTest({ getService }: FtrProviderContext) { .send({ params: { ...mockJira.params, - actionParams: { - ...mockJira.params.actionParams, + subActionParams: { + ...mockJira.params.subActionParams, caseId: 'success', title: 'success', createdAt: 'success', @@ -511,7 +511,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [actionParams.comments.0.createdAt]: expected value of type [string] but got [undefined]', + 'error validating action params: [subActionParams.comments.0.createdAt]: expected value of type [string] but got [undefined]', }); }); }); @@ -525,8 +525,8 @@ export default function jiraTest({ getService }: FtrProviderContext) { .send({ params: { ...mockJira.params, - actionParams: { - ...mockJira.params.actionParams, + subActionParams: { + ...mockJira.params.subActionParams, comments: [], }, }, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts index b574bb88845e90..38c66a9ae8d83d 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts @@ -47,8 +47,8 @@ export default function servicenowTest({ getService }: FtrProviderContext) { username: 'changeme', }, params: { - action: 'pushToService', - actionParams: { + subAction: 'pushToService', + subActionParams: { caseId: '123', title: 'a title', description: 'a description', @@ -297,7 +297,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [action]: expected at least one defined value but got [undefined]', + 'error validating action params: [subAction]: expected at least one defined value but got [undefined]', }); }); }); @@ -307,7 +307,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { .post(`/api/action/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ - params: { action: 'non-supported' }, + params: { subAction: 'non-supported' }, }) .then((resp: any) => { expect(resp.body).to.eql({ @@ -315,17 +315,17 @@ export default function servicenowTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [action]: types that failed validation:\n- [action.0]: expected value to equal [getIncident]\n- [action.1]: expected value to equal [pushToService]\n- [action.2]: expected value to equal [handshake]', + 'error validating action params: [subAction]: types that failed validation:\n- [subAction.0]: expected value to equal [getIncident]\n- [subAction.1]: expected value to equal [pushToService]\n- [subAction.2]: expected value to equal [handshake]', }); }); }); - it('should handle failing with a simulated success without actionParams', async () => { + it('should handle failing with a simulated success without subActionParams', async () => { await supertest .post(`/api/action/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ - params: { action: 'pushToService' }, + params: { subAction: 'pushToService' }, }) .then((resp: any) => { expect(resp.body).to.eql({ @@ -333,7 +333,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [actionParams.caseId]: expected value of type [string] but got [undefined]', + 'error validating action params: [subActionParams.caseId]: expected value of type [string] but got [undefined]', }); }); }); @@ -343,7 +343,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { .post(`/api/action/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ - params: { action: 'pushToService', actionParams: {} }, + params: { subAction: 'pushToService', subActionParams: {} }, }) .then((resp: any) => { expect(resp.body).to.eql({ @@ -351,7 +351,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [actionParams.caseId]: expected value of type [string] but got [undefined]', + 'error validating action params: [subActionParams.caseId]: expected value of type [string] but got [undefined]', }); }); }); @@ -363,7 +363,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { .send({ params: { ...mockServiceNow.params, - actionParams: { + subActionParams: { caseId: 'success', }, }, @@ -374,7 +374,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [actionParams.title]: expected value of type [string] but got [undefined]', + 'error validating action params: [subActionParams.title]: expected value of type [string] but got [undefined]', }); }); }); @@ -386,7 +386,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { .send({ params: { ...mockServiceNow.params, - actionParams: { + subActionParams: { caseId: 'success', title: 'success', }, @@ -398,7 +398,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [actionParams.createdAt]: expected value of type [string] but got [undefined]', + 'error validating action params: [subActionParams.createdAt]: expected value of type [string] but got [undefined]', }); }); }); @@ -410,8 +410,8 @@ export default function servicenowTest({ getService }: FtrProviderContext) { .send({ params: { ...mockServiceNow.params, - actionParams: { - ...mockServiceNow.params.actionParams, + subActionParams: { + ...mockServiceNow.params.subActionParams, caseId: 'success', title: 'success', createdAt: 'success', @@ -426,7 +426,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [actionParams.comments.0.commentId]: expected value of type [string] but got [undefined]', + 'error validating action params: [subActionParams.comments.0.commentId]: expected value of type [string] but got [undefined]', }); }); }); @@ -438,8 +438,8 @@ export default function servicenowTest({ getService }: FtrProviderContext) { .send({ params: { ...mockServiceNow.params, - actionParams: { - ...mockServiceNow.params.actionParams, + subActionParams: { + ...mockServiceNow.params.subActionParams, caseId: 'success', title: 'success', createdAt: 'success', @@ -454,7 +454,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [actionParams.comments.0.comment]: expected value of type [string] but got [undefined]', + 'error validating action params: [subActionParams.comments.0.comment]: expected value of type [string] but got [undefined]', }); }); }); @@ -466,8 +466,8 @@ export default function servicenowTest({ getService }: FtrProviderContext) { .send({ params: { ...mockServiceNow.params, - actionParams: { - ...mockServiceNow.params.actionParams, + subActionParams: { + ...mockServiceNow.params.subActionParams, caseId: 'success', title: 'success', createdAt: 'success', @@ -482,7 +482,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [actionParams.comments.0.createdAt]: expected value of type [string] but got [undefined]', + 'error validating action params: [subActionParams.comments.0.createdAt]: expected value of type [string] but got [undefined]', }); }); }); @@ -496,8 +496,8 @@ export default function servicenowTest({ getService }: FtrProviderContext) { .send({ params: { ...mockServiceNow.params, - actionParams: { - ...mockServiceNow.params.actionParams, + subActionParams: { + ...mockServiceNow.params.subActionParams, comments: [], }, }, From ce9db2a324b3532e436c7c46da5b6f5721281c74 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 30 Apr 2020 11:22:36 +0300 Subject: [PATCH 26/33] Improve typing --- .../server/builtin_action_types/case/api.ts | 14 +-- .../builtin_action_types/case/schema.ts | 43 ++++++--- .../server/builtin_action_types/case/types.ts | 89 ++++++++++++------- .../builtin_action_types/case/utils.test.ts | 4 +- .../server/builtin_action_types/case/utils.ts | 49 +++++----- .../builtin_action_types/case/validators.ts | 9 +- .../builtin_action_types/jira/config.ts | 4 +- .../server/builtin_action_types/jira/mocks.ts | 11 ++- .../builtin_action_types/jira/schema.ts | 4 +- .../builtin_action_types/jira/service.ts | 4 +- .../builtin_action_types/jira/validators.ts | 4 +- .../builtin_action_types/servicenow/config.ts | 4 +- .../builtin_action_types/servicenow/index.ts | 9 +- .../builtin_action_types/servicenow/mocks.ts | 11 ++- .../servicenow/service.ts | 4 +- .../builtin_action_types/servicenow/types.ts | 4 +- .../servicenow/validators.ts | 4 +- .../siem/public/lib/connectors/types.ts | 4 +- 18 files changed, 173 insertions(+), 102 deletions(-) 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 index 05d29597b632e1..21a5ce07cbe660 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/case/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/case/api.ts @@ -7,12 +7,14 @@ import { zipWith } from 'lodash'; import { - ConnectorApi, + ExternalServiceApi, ExternalServiceParams, ExternalServiceCommentResponse, Comment, PushToServiceResponse, - ConnectorApiHandlerArgs, + GetIncidentApiHandlerArgs, + HandshakeApiHandlerArgs, + PushToServiceApiHandlerArgs, } from './types'; import { prepareFieldsForTransformation, transformFields, transformComments } from './utils'; @@ -20,18 +22,18 @@ const handshakeHandler = async ({ externalService, mapping, params, -}: ConnectorApiHandlerArgs) => {}; +}: HandshakeApiHandlerArgs) => {}; const getIncidentHandler = async ({ externalService, mapping, params, -}: ConnectorApiHandlerArgs) => {}; +}: GetIncidentApiHandlerArgs) => {}; const pushToServiceHandler = async ({ externalService, mapping, params, -}: ConnectorApiHandlerArgs): Promise => { +}: PushToServiceApiHandlerArgs): Promise => { const { externalId, comments } = params; const updateIncident = externalId ? true : false; const defaultPipes = updateIncident ? ['informationUpdated'] : ['informationCreated']; @@ -96,7 +98,7 @@ const pushToServiceHandler = async ({ return res; }; -export const api: ConnectorApi = { +export const api: ExternalServiceApi = { handshake: handshakeHandler, pushToService: pushToServiceHandler, getIncident: getIncidentHandler, 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 1f23ae6492b631..04ebf8666f6c69 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 @@ -22,19 +22,23 @@ export const CaseConfigurationSchema = schema.object({ mapping: schema.arrayOf(MapRecordSchema), }); -export const ConnectorPublicConfiguration = { +export const ExternalIncidentServiceConfiguration = { apiUrl: schema.string(), casesConfiguration: CaseConfigurationSchema, }; -export const ConnectorPublicConfigurationSchema = schema.object(ConnectorPublicConfiguration); +export const ExternalIncidentServiceConfigurationSchema = schema.object( + ExternalIncidentServiceConfiguration +); -export const ConnectorSecretConfiguration = { +export const ExternalIncidentServiceSecretConfiguration = { password: schema.string(), username: schema.string(), }; -export const ConnectorSecretConfigurationSchema = schema.object(ConnectorSecretConfiguration); +export const ExternalIncidentServiceSecretConfigurationSchema = schema.object( + ExternalIncidentServiceSecretConfiguration +); export const UserSchema = schema.object({ fullName: schema.oneOf([schema.nullable(schema.string()), schema.maybe(schema.string())]), @@ -57,24 +61,39 @@ export const CommentSchema = schema.object({ ...EntityInformation, }); -export const ExecutorActionSchema = schema.oneOf([ +export const ExecutorSubActionSchema = schema.oneOf([ schema.literal('getIncident'), schema.literal('pushToService'), schema.literal('handshake'), ]); -export const ExecutorActionParams = { +export const ExecutorSubActionPushParamsSchema = schema.object({ caseId: schema.string(), title: schema.string(), description: schema.maybe(schema.string()), comments: schema.maybe(schema.arrayOf(CommentSchema)), externalId: schema.nullable(schema.string()), ...EntityInformation, -}; - -export const ExecutorActionParamsSchema = schema.object(ExecutorActionParams); +}); -export const ExecutorParamsSchema = schema.object({ - subAction: ExecutorActionSchema, - subActionParams: ExecutorActionParamsSchema, +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 830772d64ab739..459e9d2b03f926 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 @@ -11,49 +11,59 @@ import { TypeOf } from '@kbn/config-schema'; import { - ConnectorPublicConfigurationSchema, - ConnectorSecretConfigurationSchema, + ExternalIncidentServiceConfigurationSchema, + ExternalIncidentServiceSecretConfigurationSchema, ExecutorParamsSchema, CaseConfigurationSchema, MapRecordSchema, CommentSchema, - ExecutorActionParamsSchema, + ExecutorSubActionPushParamsSchema, + ExecutorSubActionGetIncidentParamsSchema, + ExecutorSubActionHandshakeParamsSchema, } from './schema'; export interface AnyParams { - [index: string]: string | number | object | null | undefined; + [index: string]: string | number | object | undefined | null; } -export type ConnectorPublicConfigurationType = TypeOf; -export type ConnectorSecretConfigurationType = TypeOf; +export type ExternalIncidentServiceConfiguration = TypeOf< + typeof ExternalIncidentServiceConfigurationSchema +>; +export type ExternalIncidentServiceSecretConfiguration = TypeOf< + typeof ExternalIncidentServiceSecretConfigurationSchema +>; export type ExecutorParams = TypeOf; -export type ExecutorActionParams = TypeOf & AnyParams; +export type ExecutorSubActionPushParams = TypeOf; + +export type ExecutorSubActionGetIncidentParams = TypeOf< + typeof ExecutorSubActionGetIncidentParamsSchema +>; + +export type ExecutorSubActionHandshakeParams = TypeOf< + typeof ExecutorSubActionHandshakeParamsSchema +>; export type CaseConfiguration = TypeOf; export type MapRecord = TypeOf; export type Comment = TypeOf; -export interface ApiParams extends ExecutorActionParams { - externalCase: Record; -} - -export interface ConnectorConfiguration { +export interface ExternalServiceConfiguration { id: string; name: string; } -export interface ExternalServiceCredential { +export interface ExternalServiceCredentials { config: Record; secrets: Record; } -export interface ConnectorValidation { +export interface ExternalServiceValidation { config: (configurationUtilities: any, configObject: any) => void; secrets: (configurationUtilities: any, secrets: any) => void; } -export interface ExternalServiceCaseResponse { +export interface ExternalServiceIncidentResponse { id: string; title: string; url: string; @@ -72,35 +82,50 @@ export interface ExternalServiceParams { export interface ExternalService { getIncident: (id: string) => Promise; - createIncident: (params: ExternalServiceParams) => Promise; - updateIncident: (params: ExternalServiceParams) => Promise; + createIncident: (params: ExternalServiceParams) => Promise; + updateIncident: (params: ExternalServiceParams) => Promise; createComment: (params: ExternalServiceParams) => Promise; } -export interface ConnectorApiHandlerArgs { +export interface PushToServiceApiParams extends ExecutorSubActionPushParams { + externalCase: Record; +} + +export interface ExternalServiceApiHandlerArgs { externalService: ExternalService; mapping: Map; - params: ApiParams; } -export interface PushToServiceResponse extends ExternalServiceCaseResponse { +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 ConnectorApi { - handshake: (args: ConnectorApiHandlerArgs) => Promise; - pushToService: (args: ConnectorApiHandlerArgs) => Promise; - getIncident: (args: ConnectorApiHandlerArgs) => Promise; +export interface ExternalServiceApi { + handshake: (args: HandshakeApiHandlerArgs) => Promise; + pushToService: (args: PushToServiceApiHandlerArgs) => Promise; + getIncident: (args: GetIncidentApiHandlerArgs) => Promise; } -export interface CreateConnectorBasicArgs { - api: ConnectorApi; - createExternalService: (credentials: ExternalServiceCredential) => ExternalService; +export interface CreateExternalServiceBasicArgs { + api: ExternalServiceApi; + createExternalService: (credentials: ExternalServiceCredentials) => ExternalService; } -export interface CreateConnectorArgs extends CreateConnectorBasicArgs { - config: ConnectorConfiguration; - validate: ConnectorValidation; +export interface CreateExternalServiceArgs extends CreateExternalServiceBasicArgs { + config: ExternalServiceConfiguration; + validate: ExternalServiceValidation; validationSchema: { config: any; secrets: any }; } @@ -117,13 +142,13 @@ export interface PipedField { } export interface PrepareFieldsForTransformArgs { - params: ApiParams; + params: PushToServiceApiParams; mapping: Map; defaultPipes?: string[]; } export interface TransformFieldsArgs { - params: ExecutorActionParams; + params: PushToServiceApiParams; fields: PipedField[]; currentIncident?: ExternalServiceParams; } 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 07ac2024825012..1faa4f4163a3ad 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 @@ -21,7 +21,7 @@ import { } from './utils'; import { SUPPORTED_SOURCE_FIELDS } from './constants'; -import { Comment, MapRecord, ApiParams } from './types'; +import { Comment, MapRecord, PushToServiceApiParams } from './types'; jest.mock('axios'); const axiosMock = (axios as unknown) as jest.Mock; @@ -62,7 +62,7 @@ const maliciousMapping: MapRecord[] = [ { source: 'unsupportedSource', target: 'comments', actionType: 'nothing' }, ]; -const fullParams: ApiParams = { +const fullParams: PushToServiceApiParams = { caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', title: 'a title', description: 'a description', 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 effd16f7041d58..abd144b2342a7a 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,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { curry, flow } from 'lodash'; +import { curry, flow, get } from 'lodash'; import { schema } from '@kbn/config-schema'; import { AxiosInstance, Method, AxiosResponse } from 'axios'; @@ -13,18 +13,18 @@ import { ActionTypeExecutorOptions, ActionTypeExecutorResult, ActionType } from import { ExecutorParamsSchema } from './schema'; import { - CreateConnectorArgs, - ConnectorPublicConfigurationType, + CreateExternalServiceArgs, + ExternalIncidentServiceConfiguration, CreateActionTypeArgs, ExecutorParams, MapRecord, AnyParams, - CreateConnectorBasicArgs, + CreateExternalServiceBasicArgs, PrepareFieldsForTransformArgs, PipedField, TransformFieldsArgs, Comment, - ExecutorActionParams, + ExecutorSubActionPushParams, } from './types'; import * as transformers from './transformers'; @@ -48,13 +48,13 @@ export const buildMap = (mapping: MapRecord[]): Map => { }; export const mapParams = ( - params: Partial, + params: Partial, mapping: Map ): AnyParams => { return Object.keys(params).reduce((prev: AnyParams, curr: string): AnyParams => { const field = mapping.get(curr); if (field) { - prev[field.target] = params[curr]; + prev[field.target] = get(curr, params); } return prev; }, {} as AnyParams); @@ -63,20 +63,17 @@ export const mapParams = ( export const createConnectorExecutor = ({ api, createExternalService, -}: CreateConnectorBasicArgs) => async ( +}: CreateExternalServiceBasicArgs) => async ( execOptions: ActionTypeExecutorOptions ): Promise => { const actionId = execOptions.actionId; const { casesConfiguration: { mapping: configurationMapping }, - } = execOptions.config as ConnectorPublicConfigurationType; + } = execOptions.config as ExternalIncidentServiceConfiguration; + let data = {}; const params = execOptions.params as ExecutorParams; const { subAction, subActionParams } = params; - const { comments, externalId, ...restParams } = subActionParams; - - const mapping = buildMap(configurationMapping); - const externalCase = mapParams(restParams as ExecutorActionParams, mapping); const res: Pick & Pick = { @@ -90,14 +87,26 @@ export const createConnectorExecutor = ({ }); if (!api[subAction]) { - throw new Error('[Action][Connector] Unsupported subAction type'); + throw new Error('[Action][ExternalService] Unsupported subAction type.'); } - const data = await api[subAction]({ - externalService, - mapping, - params: { ...subActionParams, externalCase }, - }); + 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(configurationMapping); + const externalCase = mapParams(restParams, mapping); + + data = await api.pushToService({ + externalService, + mapping, + params: { ...pushToServiceParams, externalCase }, + }); + } return { ...res, @@ -111,7 +120,7 @@ export const createConnector = ({ validate, createExternalService, validationSchema, -}: CreateConnectorArgs) => { +}: CreateExternalServiceArgs) => { return ({ configurationUtilities, executor = createConnectorExecutor({ api, createExternalService }), 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 index 2907d34ade5d8d..80e301e5be082e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/case/validators.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/case/validators.ts @@ -7,13 +7,16 @@ import { isEmpty } from 'lodash'; import { ActionsConfigurationUtilities } from '../../actions_config'; -import { ConnectorSecretConfigurationType, ConnectorPublicConfigurationType } from './types'; +import { + ExternalIncidentServiceConfiguration, + ExternalIncidentServiceSecretConfiguration, +} from './types'; import * as i18n from './translations'; export const validateCommonConfig = ( configurationUtilities: ActionsConfigurationUtilities, - configObject: ConnectorPublicConfigurationType + configObject: ExternalIncidentServiceConfiguration ) => { try { if (isEmpty(configObject.casesConfiguration.mapping)) { @@ -28,5 +31,5 @@ export const validateCommonConfig = ( export const validateCommonSecrets = ( configurationUtilities: ActionsConfigurationUtilities, - secrets: ConnectorSecretConfigurationType + secrets: ExternalIncidentServiceSecretConfiguration ) => {}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/config.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/config.ts index 9f9922e74d2351..7e415109f1bd9d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/config.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/config.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ConnectorConfiguration } from '../case/types'; +import { ExternalServiceConfiguration } from '../case/types'; import * as i18n from './translations'; -export const config: ConnectorConfiguration = { +export const config: ExternalServiceConfiguration = { id: '.jira', name: i18n.NAME, }; 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 4aeca922f17630..5c86ade03dda21 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 @@ -4,7 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExternalService, ApiParams, ExecutorActionParams, MapRecord } from '../case/types'; +import { + ExternalService, + PushToServiceApiParams, + ExecutorSubActionPushParams, + MapRecord, +} from '../case/types'; const createMock = (): jest.Mocked => ({ getIncident: jest.fn().mockImplementation(() => @@ -68,7 +73,7 @@ mapping.set('summary', { actionType: 'overwrite', }); -const executorParams: ExecutorActionParams = { +const executorParams: ExecutorSubActionPushParams = { caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', externalId: 'incident-3', createdAt: '2020-04-27T10:59:46.202Z', @@ -99,7 +104,7 @@ const executorParams: ExecutorActionParams = { ], }; -const apiParams: ApiParams = { +const apiParams: PushToServiceApiParams = { ...executorParams, externalCase: { summary: 'Incident title', description: 'Incident description' }, }; 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 a61d936ada53c1..9c831e75d91c1c 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,11 @@ */ import { schema } from '@kbn/config-schema'; -import { ConnectorPublicConfiguration } from '../case/schema'; +import { ExternalIncidentServiceConfiguration } from '../case/schema'; export const JiraPublicConfiguration = { projectKey: schema.string(), - ...ConnectorPublicConfiguration, + ...ExternalIncidentServiceConfiguration, }; export const JiraPublicConfigurationSchema = schema.object(JiraPublicConfiguration); diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts index c222b628f676bd..554d82b6e9dd2c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts @@ -6,7 +6,7 @@ import axios from 'axios'; -import { ExternalServiceCredential, ExternalService, ExternalServiceParams } from '../case/types'; +import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from '../case/types'; import { JiraPublicConfigurationType, JiraSecretConfigurationType } from './types'; import * as i18n from './translations'; @@ -22,7 +22,7 @@ const VIEW_INCIDENT_URL = `browse`; export const createExternalService = ({ config, secrets, -}: ExternalServiceCredential): ExternalService => { +}: ExternalServiceCredentials): ExternalService => { const { apiUrl: url, projectKey } = config as JiraPublicConfigurationType; const { apiToken, email } = secrets as JiraSecretConfigurationType; diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts index cb71e22e366cb6..7226071392bc63 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts @@ -5,9 +5,9 @@ */ import { validateCommonConfig, validateCommonSecrets } from '../case/validators'; -import { ConnectorValidation } from '../case/types'; +import { ExternalServiceValidation } from '../case/types'; -export const validate: ConnectorValidation = { +export const validate: ExternalServiceValidation = { config: validateCommonConfig, secrets: validateCommonSecrets, }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.ts index b9022b0481aa2b..4ad8108c3b1374 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ConnectorConfiguration } from '../case/types'; +import { ExternalServiceConfiguration } from '../case/types'; import * as i18n from './translations'; -export const config: ConnectorConfiguration = { +export const config: ExternalServiceConfiguration = { id: '.servicenow', name: i18n.NAME, }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts index c901f0025d7b04..dbb536d2fa53de 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts @@ -10,7 +10,10 @@ import { api } from './api'; import { config } from './config'; import { validate } from './validators'; import { createExternalService } from './service'; -import { ConnectorPublicConfiguration, ConnectorSecretConfiguration } from '../case/schema'; +import { + ExternalIncidentServiceConfiguration, + ExternalIncidentServiceSecretConfiguration, +} from '../case/schema'; export const getActionType = createConnector({ api, @@ -18,7 +21,7 @@ export const getActionType = createConnector({ validate, createExternalService, validationSchema: { - config: ConnectorPublicConfiguration, - secrets: ConnectorSecretConfiguration, + config: ExternalIncidentServiceConfiguration, + secrets: ExternalIncidentServiceSecretConfiguration, }, }); 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 01bec4f110c1ee..7cc8caea259058 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 @@ -4,7 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExternalService, ApiParams, ExecutorActionParams, MapRecord } from '../case/types'; +import { + ExternalService, + PushToServiceApiParams, + ExecutorSubActionPushParams, + MapRecord, +} from '../case/types'; const createMock = (): jest.Mocked => ({ getIncident: jest.fn().mockImplementation(() => @@ -63,7 +68,7 @@ mapping.set('short_description', { actionType: 'overwrite', }); -const executorParams: ExecutorActionParams = { +const executorParams: ExecutorSubActionPushParams = { caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', externalId: 'incident-3', createdAt: '2020-03-13T08:34:53.450Z', @@ -94,7 +99,7 @@ const executorParams: ExecutorActionParams = { ], }; -const apiParams: ApiParams = { +const apiParams: PushToServiceApiParams = { ...executorParams, externalCase: { short_description: 'Incident title', description: 'Incident description' }, }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts index 4b7d28fa27b151..ca050beb10b03b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts @@ -6,7 +6,7 @@ import axios from 'axios'; -import { ExternalServiceCredential, ExternalService, ExternalServiceParams } from '../case/types'; +import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from '../case/types'; import { addTimeZoneToDate, patch, request, getErrorMessage } from '../case/utils'; import * as i18n from './translations'; @@ -22,7 +22,7 @@ const VIEW_INCIDENT_URL = `nav_to.do?uri=incident.do?sys_id=`; export const createExternalService = ({ config, secrets, -}: ExternalServiceCredential): ExternalService => { +}: ExternalServiceCredentials): ExternalService => { const { apiUrl: url } = config as ServiceNowPublicConfigurationType; const { username, password } = secrets as ServiceNowSecretConfigurationType; 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 c46d0e80518de2..7b4f781e51b134 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 @@ -5,6 +5,6 @@ */ export { - ConnectorPublicConfigurationType as ServiceNowPublicConfigurationType, - ConnectorSecretConfigurationType as ServiceNowSecretConfigurationType, + ExternalIncidentServiceConfiguration as ServiceNowPublicConfigurationType, + ExternalIncidentServiceSecretConfiguration as ServiceNowSecretConfigurationType, } from '../case/types'; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts index cb71e22e366cb6..7226071392bc63 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts @@ -5,9 +5,9 @@ */ import { validateCommonConfig, validateCommonSecrets } from '../case/validators'; -import { ConnectorValidation } from '../case/types'; +import { ExternalServiceValidation } from '../case/types'; -export const validate: ConnectorValidation = { +export const validate: ExternalServiceValidation = { config: validateCommonConfig, secrets: validateCommonSecrets, }; diff --git a/x-pack/plugins/siem/public/lib/connectors/types.ts b/x-pack/plugins/siem/public/lib/connectors/types.ts index 8dfe7b54314ac0..9af60f4995e54c 100644 --- a/x-pack/plugins/siem/public/lib/connectors/types.ts +++ b/x-pack/plugins/siem/public/lib/connectors/types.ts @@ -8,14 +8,14 @@ /* eslint-disable @kbn/eslint/no-restricted-paths */ import { ActionType } from '../../../../triggers_actions_ui/public'; -import { ConnectorPublicConfigurationType } from '../../../../actions/server/builtin_action_types/case/types'; +import { ExternalIncidentServiceConfiguration } from '../../../../actions/server/builtin_action_types/case/types'; export interface Connector extends ActionType { logo: string; } export interface ActionConnector { - config: ConnectorPublicConfigurationType; + config: ExternalIncidentServiceConfiguration; secrets: {}; } From ed41eb6ccf776a83225f6b64f4671d7434e559e2 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 30 Apr 2020 11:26:45 +0300 Subject: [PATCH 27/33] Better translation keys --- .../builtin_action_types/case/translations.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/case/translations.ts index 525c4523d9ae78..4842728b0e4e7e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/case/translations.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/case/translations.ts @@ -17,34 +17,37 @@ export const FIELD_INFORMATION = ( ) => { switch (mode) { case 'create': - return i18n.translate('xpack.actions.builtin.case.common.informationCreated', { + return i18n.translate('xpack.actions.builtin.case.common.externalIncidentCreated', { values: { date, user }, defaultMessage: '(created at {date} by {user})', }); case 'update': - return i18n.translate('xpack.actions.builtin.case.common.informationUpdated', { + return i18n.translate('xpack.actions.builtin.case.common.externalIncidentUpdated', { values: { date, user }, defaultMessage: '(updated at {date} by {user})', }); case 'add': - return i18n.translate('xpack.actions.builtin.case.common.informationAdded', { + return i18n.translate('xpack.actions.builtin.case.common.externalIncidentAdded', { values: { date, user }, defaultMessage: '(added at {date} by {user})', }); default: - return i18n.translate('xpack.actions.builtin.case.common.informationDefault', { + return i18n.translate('xpack.actions.builtin.case.common.externalIncidentDefault', { values: { date, user }, defaultMessage: '(created at {date} by {user})', }); } }; -export const MAPPING_EMPTY = i18n.translate('xpack.actions.builtin.case.common.emptyMapping', { - defaultMessage: '[casesConfiguration.mapping]: expected non-empty but got empty', -}); +export const MAPPING_EMPTY = i18n.translate( + 'xpack.actions.builtin.case.configuration.emptyMapping', + { + defaultMessage: '[casesConfiguration.mapping]: expected non-empty but got empty', + } +); export const WHITE_LISTED_ERROR = (message: string) => - i18n.translate('xpack.actions.builtin.case.common.apiWhitelistError', { + i18n.translate('xpack.actions.builtin.case.configuration.apiWhitelistError', { defaultMessage: 'error configuring connector action: {message}', values: { message, From 66b09b7665fec2fb932fb401d7ec486f4272b65b Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 30 Apr 2020 11:50:04 +0300 Subject: [PATCH 28/33] Improve comments creation --- .../server/builtin_action_types/case/api.ts | 32 ++++++------------- 1 file changed, 10 insertions(+), 22 deletions(-) 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 index 21a5ce07cbe660..6dc8a9cc9af6ad 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/case/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/case/api.ts @@ -4,13 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { zipWith } from 'lodash'; - import { ExternalServiceApi, ExternalServiceParams, - ExternalServiceCommentResponse, - Comment, PushToServiceResponse, GetIncidentApiHandlerArgs, HandshakeApiHandlerArgs, @@ -70,29 +66,21 @@ const pushToServiceHandler = async ({ ) { const commentsTransformed = transformComments(comments, ['informationAdded']); - // Create comments sequentially. - const promises = commentsTransformed.reduce(async (prevPromise, currentComment) => { - const totalComments = await prevPromise; + res.comments = []; + for (const currentComment of commentsTransformed) { const comment = await externalService.createComment({ incidentId: res.id, comment: currentComment, field: mapping.get('comments')?.target ?? 'comments', }); - return [...totalComments, comment]; - }, Promise.resolve([] as ExternalServiceCommentResponse[])); - - const createdComments = await promises; - - const zippedComments: ExternalServiceCommentResponse[] = zipWith( - commentsTransformed, - createdComments, - (a: Comment, b: ExternalServiceCommentResponse) => ({ - commentId: a.commentId, - pushedDate: b.pushedDate, - }) - ); - - res.comments = [...zippedComments]; + res.comments = [ + ...(res.comments ?? []), + { + commentId: comment.commentId, + pushedDate: comment.pushedDate, + }, + ]; + } } return res; From 92457480e0372d51cf02e68c1023fc96a634cfcc Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 30 Apr 2020 12:00:59 +0300 Subject: [PATCH 29/33] Improve transformers --- .../case/transformers.test.ts | 4 +- .../builtin_action_types/case/transformers.ts | 52 +++++-------- .../server/builtin_action_types/case/utils.ts | 73 +++++++++---------- 3 files changed, 57 insertions(+), 72 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/transformers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/case/transformers.test.ts index 5254a22237e57a..75dcab65ee9f21 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/case/transformers.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/case/transformers.test.ts @@ -4,7 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { informationCreated, informationUpdated, informationAdded, append } from './transformers'; +import { transformers } from './transformers'; + +const { informationCreated, informationUpdated, informationAdded, append } = transformers; describe('informationCreated', () => { test('transforms correctly', () => { diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/transformers.ts b/x-pack/plugins/actions/server/builtin_action_types/case/transformers.ts index dc0a03fab8c715..3dca1fd7034301 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/case/transformers.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/case/transformers.ts @@ -7,37 +7,23 @@ import { TransformerArgs } from './types'; import * as i18n from './translations'; -export const informationCreated = ({ - value, - date, - user, - ...rest -}: TransformerArgs): TransformerArgs => ({ - value: `${value} ${i18n.FIELD_INFORMATION('create', date, user)}`, - ...rest, -}); +export type Transformer = (args: TransformerArgs) => TransformerArgs; -export const informationUpdated = ({ - value, - date, - user, - ...rest -}: TransformerArgs): TransformerArgs => ({ - value: `${value} ${i18n.FIELD_INFORMATION('update', date, user)}`, - ...rest, -}); - -export const informationAdded = ({ - value, - date, - user, - ...rest -}: TransformerArgs): TransformerArgs => ({ - value: `${value} ${i18n.FIELD_INFORMATION('add', date, user)}`, - ...rest, -}); - -export const append = ({ value, previousValue, ...rest }: TransformerArgs): TransformerArgs => ({ - value: previousValue ? `${previousValue} \r\n${value}` : `${value}`, - ...rest, -}); +export const transformers: Record = { + informationCreated: ({ value, date, user, ...rest }: TransformerArgs): TransformerArgs => ({ + value: `${value} ${i18n.FIELD_INFORMATION('create', date, user)}`, + ...rest, + }), + informationUpdated: ({ value, date, user, ...rest }: TransformerArgs): TransformerArgs => ({ + value: `${value} ${i18n.FIELD_INFORMATION('update', date, user)}`, + ...rest, + }), + informationAdded: ({ value, date, user, ...rest }: TransformerArgs): TransformerArgs => ({ + value: `${value} ${i18n.FIELD_INFORMATION('add', date, user)}`, + ...rest, + }), + append: ({ value, previousValue, ...rest }: TransformerArgs): TransformerArgs => ({ + value: previousValue ? `${previousValue} \r\n${value}` : `${value}`, + ...rest, + }), +}; 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 abd144b2342a7a..c1379205a4dadd 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 @@ -27,7 +27,7 @@ import { ExecutorSubActionPushParams, } from './types'; -import * as transformers from './transformers'; +import { transformers, Transformer } from './transformers'; import { SUPPORTED_SOURCE_FIELDS } from './constants'; @@ -197,62 +197,59 @@ export const prepareFieldsForTransformation = ({ mapping, defaultPipes = ['informationCreated'], }: PrepareFieldsForTransformArgs): PipedField[] => { - return ( - Object.keys(params.externalCase) - // This clears undefined values but typescript does not get it - .filter(p => mapping.get(p)?.actionType) - .filter(p => mapping.get(p)?.actionType !== 'nothing') - .map(p => ({ + return Object.keys(params.externalCase) + .filter(p => mapping.get(p)?.actionType != null && mapping.get(p)?.actionType !== 'nothing') + .map(p => { + const actionType = mapping.get(p)?.actionType ?? 'nothing'; + return { key: p, value: params.externalCase[p], - actionType: mapping.get(p)?.actionType ?? 'nothing', - pipes: [...defaultPipes], - })) - .map(p => ({ - ...p, - pipes: p.actionType === 'append' ? [...p.pipes, 'append'] : p.pipes, - })) - ); + actionType, + pipes: actionType === 'append' ? [...defaultPipes, 'append'] : defaultPipes, + }; + }); }; -const t = { ...transformers } as { [index: string]: Function }; // TODO: Find a better solution if exists. - -export const transformFields = ({ params, fields, currentIncident }: TransformFieldsArgs) => { +export const transformFields = ({ + params, + fields, + currentIncident, +}: TransformFieldsArgs): Record => { return fields.reduce((prev, cur) => { - const transform = flow(...cur.pipes.map(p => t[p])); - prev[cur.key] = transform({ - value: cur.value, - date: params.updatedAt ?? params.createdAt, - user: - params.updatedBy != null - ? params.updatedBy.fullName + const transform = flow(...cur.pipes.map(p => transformers[p])); + return { + ...prev, + [cur.key]: transform({ + value: cur.value, + date: params.updatedAt ?? params.createdAt, + user: + (params.updatedBy != null ? params.updatedBy.fullName - : params.updatedBy.username - : params.createdBy.fullName - ? params.createdBy.fullName - : params.createdBy.username, - previousValue: currentIncident ? currentIncident[cur.key] : '', - }).value; - return prev; - // This will have to remain `any` until we can extend connectors with generics - // eslint-disable-next-line @typescript-eslint/no-explicit-any - }, {} as any); + ? params.updatedBy.fullName + : params.updatedBy.username + : params.createdBy.fullName + ? params.createdBy.fullName + : params.createdBy.username) ?? '', + previousValue: currentIncident ? currentIncident[cur.key] : '', + }).value, + }; + }, {}); }; export const transformComments = (comments: Comment[], pipes: string[]): Comment[] => { return comments.map(c => ({ ...c, - comment: flow(...pipes.map(p => t[p]))({ + comment: flow(...pipes.map(p => transformers[p]))({ value: c.comment, date: c.updatedAt ?? c.createdAt, user: - c.updatedBy != null + (c.updatedBy != null ? c.updatedBy.fullName ? c.updatedBy.fullName : c.updatedBy.username : c.createdBy.fullName ? c.createdBy.fullName - : c.createdBy.username, + : c.createdBy.username) ?? '', }).value, })); }; From 9c54ea4e721a937c2217f17dce723f29298ef565 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 30 Apr 2020 12:07:32 +0300 Subject: [PATCH 30/33] Improve variable destruction and types --- .../server/builtin_action_types/case/utils.ts | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) 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 c1379205a4dadd..8228fc1e2eb401 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 @@ -14,7 +14,6 @@ import { ExecutorParamsSchema } from './schema'; import { CreateExternalServiceArgs, - ExternalIncidentServiceConfiguration, CreateActionTypeArgs, ExecutorParams, MapRecord, @@ -57,7 +56,7 @@ export const mapParams = ( prev[field.target] = get(curr, params); } return prev; - }, {} as AnyParams); + }, {}); }; export const createConnectorExecutor = ({ @@ -66,14 +65,9 @@ export const createConnectorExecutor = ({ }: CreateExternalServiceBasicArgs) => async ( execOptions: ActionTypeExecutorOptions ): Promise => { - const actionId = execOptions.actionId; - const { - casesConfiguration: { mapping: configurationMapping }, - } = execOptions.config as ExternalIncidentServiceConfiguration; - + const { actionId, config, params, secrets } = execOptions; + const { subAction, subActionParams } = params as ExecutorParams; let data = {}; - const params = execOptions.params as ExecutorParams; - const { subAction, subActionParams } = params; const res: Pick & Pick = { @@ -82,8 +76,8 @@ export const createConnectorExecutor = ({ }; const externalService = createExternalService({ - config: execOptions.config, - secrets: execOptions.secrets, + config, + secrets, }); if (!api[subAction]) { @@ -98,7 +92,7 @@ export const createConnectorExecutor = ({ const pushToServiceParams = subActionParams as ExecutorSubActionPushParams; const { comments, externalId, ...restParams } = pushToServiceParams; - const mapping = buildMap(configurationMapping); + const mapping = buildMap(config.casesConfiguration.mapping); const externalCase = mapParams(restParams, mapping); data = await api.pushToService({ From 1a64bf6c30943a591d8715bde8010043a8fcb66c Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 30 Apr 2020 12:21:51 +0300 Subject: [PATCH 31/33] Improve API helper functions --- .../server/builtin_action_types/case/utils.ts | 16 ++++++-------- .../builtin_action_types/jira/service.ts | 14 +++++++++---- .../server/builtin_action_types/jira/types.ts | 21 +++++++++++++++++++ .../servicenow/service.ts | 14 +++++++++---- .../builtin_action_types/servicenow/types.ts | 11 ++++++++++ 5 files changed, 58 insertions(+), 18 deletions(-) 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 8228fc1e2eb401..ebb33efbee6e5f 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 @@ -145,34 +145,30 @@ export const throwIfNotAlive = ( } }; -export const request = async ({ +export const request = async ({ axios, url, method = 'get', - data = {}, + data, }: { axios: AxiosInstance; url: string; method?: Method; - // This will have to remain `any` until we can extend connectors with generics - // eslint-disable-next-line @typescript-eslint/no-explicit-any - data?: any; + data?: T; }): Promise => { - const res = await axios(url, { method, data }); + const res = await axios(url, { method, data: data ?? {} }); throwIfNotAlive(res.status, res.headers['content-type']); return res; }; -export const patch = ({ +export const patch = async ({ axios, url, data, }: { axios: AxiosInstance; url: string; - // This will have to remain `any` until we can extend connectors with generics - // eslint-disable-next-line @typescript-eslint/no-explicit-any - data: any; + data: T; }): Promise => { return request({ axios, diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts index 554d82b6e9dd2c..ff22b8368e7dd2 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts @@ -7,7 +7,13 @@ import axios from 'axios'; import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from '../case/types'; -import { JiraPublicConfigurationType, JiraSecretConfigurationType } from './types'; +import { + JiraPublicConfigurationType, + JiraSecretConfigurationType, + CreateIncidentRequest, + UpdateIncidentRequest, + CreateCommentRequest, +} from './types'; import * as i18n from './translations'; import { getErrorMessage, request } from '../case/utils'; @@ -66,7 +72,7 @@ export const createExternalService = ({ // The function makes two calls when creating an issue. One to create the issue and one to get // the created issue with all the necessary fields. try { - const res = await request({ + const res = await request({ axios: axiosInstance, url: `${incidentUrl}`, method: 'post', @@ -92,7 +98,7 @@ export const createExternalService = ({ const updateIncident = async ({ incidentId, incident }: ExternalServiceParams) => { try { - await request({ + await request({ axios: axiosInstance, method: 'put', url: `${incidentUrl}/${incidentId}`, @@ -119,7 +125,7 @@ export const createExternalService = ({ const createComment = async ({ incidentId, comment, field }: ExternalServiceParams) => { try { - const res = await request({ + const res = await request({ axios: axiosInstance, method: 'post', url: getCommentsURL(incidentId), 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 0051ef6c70b2c6..8d9c6b92abb3b2 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 @@ -9,3 +9,24 @@ import { JiraPublicConfigurationSchema, JiraSecretConfigurationSchema } from './ export type JiraPublicConfigurationType = TypeOf; export type JiraSecretConfigurationType = TypeOf; + +interface CreateIncidentBasicRequestArgs { + summary: string; + description: string; +} +interface CreateIncidentRequestArgs extends CreateIncidentBasicRequestArgs { + project: { key: string }; + issuetype: { name: string }; +} + +export interface CreateIncidentRequest { + fields: CreateIncidentRequestArgs; +} + +export interface UpdateIncidentRequest { + fields: Partial; +} + +export interface CreateCommentRequest { + body: string; +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts index ca050beb10b03b..541fefce2f2ff5 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts @@ -10,7 +10,13 @@ import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } fr import { addTimeZoneToDate, patch, request, getErrorMessage } from '../case/utils'; import * as i18n from './translations'; -import { ServiceNowPublicConfigurationType, ServiceNowSecretConfigurationType } from './types'; +import { + ServiceNowPublicConfigurationType, + ServiceNowSecretConfigurationType, + CreateIncidentRequest, + UpdateIncidentRequest, + CreateCommentRequest, +} from './types'; const API_VERSION = 'v2'; const INCIDENT_URL = `api/now/${API_VERSION}/table/incident`; @@ -57,7 +63,7 @@ export const createExternalService = ({ const createIncident = async ({ incident }: ExternalServiceParams) => { try { - const res = await request({ + const res = await request({ axios: axiosInstance, url: `${incidentUrl}`, method: 'post', @@ -79,7 +85,7 @@ export const createExternalService = ({ const updateIncident = async ({ incidentId, incident }: ExternalServiceParams) => { try { - const res = await patch({ + const res = await patch({ axios: axiosInstance, url: `${incidentUrl}/${incidentId}`, data: { ...incident }, @@ -103,7 +109,7 @@ export const createExternalService = ({ const createComment = async ({ incidentId, comment, field }: ExternalServiceParams) => { try { - const res = await patch({ + const res = await patch({ axios: axiosInstance, url: `${commentUrl}/${incidentId}`, data: { [field]: comment.comment }, 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 7b4f781e51b134..d8476b7dca54a5 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 @@ -8,3 +8,14 @@ export { ExternalIncidentServiceConfiguration as ServiceNowPublicConfigurationType, ExternalIncidentServiceSecretConfiguration as ServiceNowSecretConfigurationType, } from '../case/types'; + +export interface CreateIncidentRequest { + summary: string; + description: string; +} + +export type UpdateIncidentRequest = Partial; + +export interface CreateCommentRequest { + [key: string]: string; +} From 3603141fb76d3cf4fdb4f33ba2ef27aa86bf45d3 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 30 Apr 2020 13:51:44 +0300 Subject: [PATCH 32/33] Fix integration tests --- .../server/builtin_action_types/case/utils.ts | 2 +- .../builtin_action_types/jira/api.test.ts | 24 +++---- .../server/builtin_action_types/jira/mocks.ts | 72 +++++++++++-------- .../servicenow/api.test.ts | 5 +- .../builtin_action_types/servicenow/mocks.ts | 60 +++++++++------- .../jira_simulation.ts | 0 .../actions/builtin_action_types/jira.ts | 21 +++--- .../builtin_action_types/servicenow.ts | 19 +++-- 8 files changed, 114 insertions(+), 89 deletions(-) rename x-pack/test/alerting_api_integration/common/fixtures/plugins/{actions => actions_simulators}/jira_simulation.ts (100%) 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 ebb33efbee6e5f..7d69b2791f6240 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 @@ -53,7 +53,7 @@ export const mapParams = ( return Object.keys(params).reduce((prev: AnyParams, curr: string): AnyParams => { const field = mapping.get(curr); if (field) { - prev[field.target] = get(curr, params); + prev[field.target] = get(params, curr); } return prev; }, {}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts index f5534e34435f67..755e0660171b88 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts @@ -11,11 +11,11 @@ import { ExternalService } from '../case/types'; describe('api', () => { let externalService: jest.Mocked; - beforeAll(() => { + beforeEach(() => { externalService = externalServiceMock.create(); }); - beforeEach(() => { + afterEach(() => { jest.clearAllMocks(); }); @@ -26,7 +26,7 @@ describe('api', () => { const res = await api.pushToService({ externalService, mapping, params }); expect(res).toEqual({ - id: '1', + id: 'incident-1', title: 'CK-1', pushedDate: '2020-04-27T10:59:46.202Z', url: 'https://siem-kibana.atlassian.net/browse/CK-1', @@ -48,7 +48,7 @@ describe('api', () => { const res = await api.pushToService({ externalService, mapping, params }); expect(res).toEqual({ - id: '1', + id: 'incident-1', title: 'CK-1', pushedDate: '2020-04-27T10:59:46.202Z', url: 'https://siem-kibana.atlassian.net/browse/CK-1', @@ -74,7 +74,7 @@ describe('api', () => { await api.pushToService({ externalService, mapping, params }); expect(externalService.createComment).toHaveBeenCalledTimes(2); expect(externalService.createComment).toHaveBeenNthCalledWith(1, { - incidentId: '1', + incidentId: 'incident-1', comment: { commentId: 'case-comment-1', version: 'WzU3LDFd', @@ -94,7 +94,7 @@ describe('api', () => { }); expect(externalService.createComment).toHaveBeenNthCalledWith(2, { - incidentId: '1', + incidentId: 'incident-1', comment: { commentId: 'case-comment-2', version: 'WlK3LDFd', @@ -120,8 +120,8 @@ describe('api', () => { const res = await api.pushToService({ externalService, mapping, params: apiParams }); expect(res).toEqual({ - id: 'incident-2', - title: 'INC02', + id: 'incident-1', + title: 'CK-1', pushedDate: '2020-04-27T10:59:46.202Z', url: 'https://siem-kibana.atlassian.net/browse/CK-1', comments: [ @@ -142,8 +142,8 @@ describe('api', () => { const res = await api.pushToService({ externalService, mapping, params }); expect(res).toEqual({ - id: 'incident-2', - title: 'INC02', + id: 'incident-1', + title: 'CK-1', pushedDate: '2020-04-27T10:59:46.202Z', url: 'https://siem-kibana.atlassian.net/browse/CK-1', }); @@ -169,7 +169,7 @@ describe('api', () => { await api.pushToService({ externalService, mapping, params }); expect(externalService.createComment).toHaveBeenCalledTimes(2); expect(externalService.createComment).toHaveBeenNthCalledWith(1, { - incidentId: 'incident-2', + incidentId: 'incident-1', comment: { commentId: 'case-comment-1', version: 'WzU3LDFd', @@ -189,7 +189,7 @@ describe('api', () => { }); expect(externalService.createComment).toHaveBeenNthCalledWith(2, { - incidentId: 'incident-2', + incidentId: 'incident-1', comment: { commentId: 'case-comment-2', version: 'WlK3LDFd', 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 5c86ade03dda21..43d3a1d5b0a286 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 @@ -11,41 +11,55 @@ import { MapRecord, } from '../case/types'; -const createMock = (): jest.Mocked => ({ - getIncident: jest.fn().mockImplementation(() => - Promise.resolve({ - id: '1', - key: 'CK-1', - summary: 'title from jira', - description: 'description from jira', - created: '2020-04-27T10:59:46.202Z', - updated: '2020-04-27T10:59:46.202Z', - }) - ), - createIncident: jest.fn().mockImplementation(() => - Promise.resolve({ - id: '1', - title: 'CK-1', - pushedDate: '2020-04-27T10:59:46.202Z', - url: 'https://siem-kibana.atlassian.net/browse/CK-1', - }) - ), - updateIncident: jest.fn().mockImplementation(() => +const createMock = (): jest.Mocked => { + const service = { + getIncident: jest.fn().mockImplementation(() => + Promise.resolve({ + id: 'incident-1', + key: 'CK-1', + summary: 'title from jira', + description: 'description from jira', + created: '2020-04-27T10:59:46.202Z', + updated: '2020-04-27T10:59:46.202Z', + }) + ), + createIncident: jest.fn().mockImplementation(() => + Promise.resolve({ + id: 'incident-1', + title: 'CK-1', + pushedDate: '2020-04-27T10:59:46.202Z', + url: 'https://siem-kibana.atlassian.net/browse/CK-1', + }) + ), + updateIncident: jest.fn().mockImplementation(() => + Promise.resolve({ + id: 'incident-1', + title: 'CK-1', + pushedDate: '2020-04-27T10:59:46.202Z', + url: 'https://siem-kibana.atlassian.net/browse/CK-1', + }) + ), + createComment: jest.fn(), + }; + + service.createComment.mockImplementationOnce(() => Promise.resolve({ - id: 'incident-2', - title: 'INC02', + commentId: 'case-comment-1', pushedDate: '2020-04-27T10:59:46.202Z', - url: 'https://siem-kibana.atlassian.net/browse/CK-1', + externalCommentId: '1', }) - ), - createComment: jest.fn().mockImplementation(() => + ); + + service.createComment.mockImplementationOnce(() => Promise.resolve({ - commentId: 'comment-1', + commentId: 'case-comment-2', pushedDate: '2020-04-27T10:59:46.202Z', - externalCommentId: '1', + externalCommentId: '2', }) - ), -}); + ); + + return service; +}; const externalServiceMock = { create: createMock, diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts index e95771a5d51c70..76a3a9ab2cb8d9 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts @@ -11,11 +11,12 @@ import { ExternalService } from '../case/types'; describe('api', () => { let externalService: jest.Mocked; - beforeAll(() => { + beforeEach(() => { externalService = externalServiceMock.create(); + jest.clearAllMocks(); }); - beforeEach(() => { + afterEach(() => { jest.clearAllMocks(); }); 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 7cc8caea259058..6cbfcb9a8766a4 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 @@ -11,36 +11,48 @@ import { MapRecord, } from '../case/types'; -const createMock = (): jest.Mocked => ({ - getIncident: jest.fn().mockImplementation(() => - Promise.resolve({ - short_description: 'title from servicenow', - description: 'description from servicenow', - }) - ), - createIncident: jest.fn().mockImplementation(() => - Promise.resolve({ - id: 'incident-1', - title: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }) - ), - updateIncident: jest.fn().mockImplementation(() => +const createMock = (): jest.Mocked => { + const service = { + getIncident: jest.fn().mockImplementation(() => + Promise.resolve({ + short_description: 'title from servicenow', + description: 'description from servicenow', + }) + ), + createIncident: jest.fn().mockImplementation(() => + Promise.resolve({ + id: 'incident-1', + title: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', + }) + ), + updateIncident: jest.fn().mockImplementation(() => + Promise.resolve({ + id: 'incident-2', + title: 'INC02', + pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', + }) + ), + createComment: jest.fn(), + }; + + service.createComment.mockImplementationOnce(() => Promise.resolve({ - id: 'incident-2', - title: 'INC02', + commentId: 'case-comment-1', pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', }) - ), - createComment: jest.fn().mockImplementation(() => + ); + + service.createComment.mockImplementationOnce(() => Promise.resolve({ - commentId: 'comment-1', + commentId: 'case-comment-2', pushedDate: '2020-03-10T12:24:20.000Z', }) - ), -}); + ); + return service; +}; const externalServiceMock = { create: createMock, diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/jira_simulation.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/jira_simulation.ts similarity index 100% rename from x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/jira_simulation.ts rename to x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/jira_simulation.ts diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts index c6a6db1ec451a9..fd4b5ff2c1e665 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts @@ -11,7 +11,7 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getExternalServiceSimulatorPath, ExternalServiceSimulator, -} from '../../../../common/fixtures/plugins/actions'; +} from '../../../../common/fixtures/plugins/actions_simulators'; const mapping = [ { @@ -325,8 +325,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { actionId: simulatedActionId, status: 'error', retry: false, - message: - 'error validating action params: [subAction]: expected at least one defined value but got [undefined]', + message: `error validating action params: Cannot read property 'Symbol(Symbol.iterator)' of undefined`, }); }); }); @@ -344,7 +343,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [subAction]: types that failed validation:\n- [subAction.0]: expected value to equal [getIncident]\n- [subAction.1]: expected value to equal [pushToService]\n- [subAction.2]: expected value to equal [handshake]', + '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]', }); }); }); @@ -362,7 +361,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [subActionParams.caseId]: 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.caseId]: expected value of type [string] but got [undefined]', }); }); }); @@ -380,7 +379,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [subActionParams.caseId]: 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.caseId]: expected value of type [string] but got [undefined]', }); }); }); @@ -403,7 +402,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [subActionParams.title]: 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]', }); }); }); @@ -427,7 +426,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [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.createdAt]: expected value of type [string] but got [undefined]', }); }); }); @@ -455,7 +454,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [subActionParams.comments.0.commentId]: 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.comments.0.commentId]: expected value of type [string] but got [undefined]', }); }); }); @@ -483,7 +482,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [subActionParams.comments.0.comment]: 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.comments.0.comment]: expected value of type [string] but got [undefined]', }); }); }); @@ -511,7 +510,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [subActionParams.comments.0.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.comments.0.createdAt]: expected value of type [string] but got [undefined]', }); }); }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts index 38c66a9ae8d83d..69d54384f2984c 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts @@ -296,8 +296,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { actionId: simulatedActionId, status: 'error', retry: false, - message: - 'error validating action params: [subAction]: expected at least one defined value but got [undefined]', + message: `error validating action params: Cannot read property 'Symbol(Symbol.iterator)' of undefined`, }); }); }); @@ -315,7 +314,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [subAction]: types that failed validation:\n- [subAction.0]: expected value to equal [getIncident]\n- [subAction.1]: expected value to equal [pushToService]\n- [subAction.2]: expected value to equal [handshake]', + '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]', }); }); }); @@ -333,7 +332,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [subActionParams.caseId]: 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.caseId]: expected value of type [string] but got [undefined]', }); }); }); @@ -351,7 +350,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [subActionParams.caseId]: 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.caseId]: expected value of type [string] but got [undefined]', }); }); }); @@ -374,7 +373,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [subActionParams.title]: 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]', }); }); }); @@ -398,7 +397,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [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.createdAt]: expected value of type [string] but got [undefined]', }); }); }); @@ -426,7 +425,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [subActionParams.comments.0.commentId]: 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.comments.0.commentId]: expected value of type [string] but got [undefined]', }); }); }); @@ -454,7 +453,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [subActionParams.comments.0.comment]: 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.comments.0.comment]: expected value of type [string] but got [undefined]', }); }); }); @@ -482,7 +481,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [subActionParams.comments.0.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.comments.0.createdAt]: expected value of type [string] but got [undefined]', }); }); }); From 102826e3cdd410954c82cadcadf038e1dda56513 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 30 Apr 2020 22:22:22 +0300 Subject: [PATCH 33/33] Change undefined values to nullable --- .../builtin_action_types/case/schema.ts | 9 +++---- .../builtin_action_types/case/utils.test.ts | 14 ---------- .../builtin_action_types/jira/api.test.ts | 4 --- .../server/builtin_action_types/jira/mocks.ts | 2 -- .../servicenow/api.test.ts | 4 --- .../builtin_action_types/servicenow/mocks.ts | 2 -- x-pack/plugins/case/common/api/cases/case.ts | 26 ++++++++----------- .../siem/public/containers/case/mock.ts | 1 + .../actions/builtin_action_types/jira.ts | 6 ++--- .../builtin_action_types/servicenow.ts | 6 ++--- 10 files changed, 22 insertions(+), 52 deletions(-) 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 04ebf8666f6c69..33b2ad6d186843 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 @@ -41,8 +41,8 @@ export const ExternalIncidentServiceSecretConfigurationSchema = schema.object( ); export const UserSchema = schema.object({ - fullName: schema.oneOf([schema.nullable(schema.string()), schema.maybe(schema.string())]), - username: schema.oneOf([schema.nullable(schema.string()), schema.maybe(schema.string())]), + fullName: schema.nullable(schema.string()), + username: schema.nullable(schema.string()), }); const EntityInformation = { @@ -57,7 +57,6 @@ export const EntityInformationSchema = schema.object(EntityInformation); export const CommentSchema = schema.object({ commentId: schema.string(), comment: schema.string(), - version: schema.maybe(schema.string()), ...EntityInformation, }); @@ -70,8 +69,8 @@ export const ExecutorSubActionSchema = schema.oneOf([ export const ExecutorSubActionPushParamsSchema = schema.object({ caseId: schema.string(), title: schema.string(), - description: schema.maybe(schema.string()), - comments: schema.maybe(schema.arrayOf(CommentSchema)), + description: schema.nullable(schema.string()), + comments: schema.nullable(schema.arrayOf(CommentSchema)), externalId: schema.nullable(schema.string()), ...EntityInformation, }); 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 1faa4f4163a3ad..1e8cc3eda20e55 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 @@ -78,7 +78,6 @@ const fullParams: PushToServiceApiParams = { comments: [ { commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', comment: 'first comment', createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: 'Elastic User', username: 'elastic' }, @@ -87,7 +86,6 @@ const fullParams: PushToServiceApiParams = { }, { commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', comment: 'second comment', createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: 'Elastic User', username: 'elastic' }, @@ -335,7 +333,6 @@ describe('transformComments', () => { const comments: Comment[] = [ { commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', comment: 'first comment', createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: 'Elastic User', username: 'elastic' }, @@ -347,7 +344,6 @@ describe('transformComments', () => { expect(res).toEqual([ { commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', comment: 'first comment (created at 2020-03-13T08:34:53.450Z by Elastic User)', createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: 'Elastic User', username: 'elastic' }, @@ -361,7 +357,6 @@ describe('transformComments', () => { const comments: Comment[] = [ { commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', comment: 'first comment', createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: 'Elastic User', username: 'elastic' }, @@ -376,7 +371,6 @@ describe('transformComments', () => { expect(res).toEqual([ { commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', comment: 'first comment (updated at 2020-03-15T08:34:53.450Z by Another User)', createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: 'Elastic User', username: 'elastic' }, @@ -393,7 +387,6 @@ describe('transformComments', () => { const comments: Comment[] = [ { commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', comment: 'first comment', createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: 'Elastic User', username: 'elastic' }, @@ -405,7 +398,6 @@ describe('transformComments', () => { expect(res).toEqual([ { commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', comment: 'first comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: 'Elastic User', username: 'elastic' }, @@ -419,7 +411,6 @@ describe('transformComments', () => { const comments: Comment[] = [ { commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', comment: 'first comment', createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: '', username: 'elastic' }, @@ -431,7 +422,6 @@ describe('transformComments', () => { expect(res).toEqual([ { commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', comment: 'first comment (added at 2020-03-13T08:34:53.450Z by elastic)', createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: '', username: 'elastic' }, @@ -445,7 +435,6 @@ describe('transformComments', () => { const comments: Comment[] = [ { commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', comment: 'first comment', createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: 'Elastic', username: 'elastic' }, @@ -457,7 +446,6 @@ describe('transformComments', () => { expect(res).toEqual([ { commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', comment: 'first comment (added at 2020-04-13T08:34:53.450Z by Elastic2)', createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: 'Elastic', username: 'elastic' }, @@ -471,7 +459,6 @@ describe('transformComments', () => { const comments: Comment[] = [ { commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', comment: 'first comment', createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: 'Elastic', username: 'elastic' }, @@ -483,7 +470,6 @@ describe('transformComments', () => { expect(res).toEqual([ { commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', comment: 'first comment (added at 2020-04-13T08:34:53.450Z by elastic2)', createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: 'Elastic', username: 'elastic' }, diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts index 755e0660171b88..bcfb82077d2861 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts @@ -77,7 +77,6 @@ describe('api', () => { incidentId: 'incident-1', comment: { commentId: 'case-comment-1', - version: 'WzU3LDFd', comment: 'A comment (added at 2020-04-27T10:59:46.202Z by Elastic User)', createdAt: '2020-04-27T10:59:46.202Z', createdBy: { @@ -97,7 +96,6 @@ describe('api', () => { incidentId: 'incident-1', comment: { commentId: 'case-comment-2', - version: 'WlK3LDFd', comment: 'Another comment (added at 2020-04-27T10:59:46.202Z by Elastic User)', createdAt: '2020-04-27T10:59:46.202Z', createdBy: { @@ -172,7 +170,6 @@ describe('api', () => { incidentId: 'incident-1', comment: { commentId: 'case-comment-1', - version: 'WzU3LDFd', comment: 'A comment (added at 2020-04-27T10:59:46.202Z by Elastic User)', createdAt: '2020-04-27T10:59:46.202Z', createdBy: { @@ -192,7 +189,6 @@ describe('api', () => { incidentId: 'incident-1', comment: { commentId: 'case-comment-2', - version: 'WlK3LDFd', comment: 'Another comment (added at 2020-04-27T10:59:46.202Z by Elastic User)', createdAt: '2020-04-27T10:59:46.202Z', createdBy: { 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 43d3a1d5b0a286..3ae0e9db36de0c 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 @@ -99,7 +99,6 @@ const executorParams: ExecutorSubActionPushParams = { comments: [ { commentId: 'case-comment-1', - version: 'WzU3LDFd', comment: 'A comment', createdAt: '2020-04-27T10:59:46.202Z', createdBy: { fullName: 'Elastic User', username: 'elastic' }, @@ -108,7 +107,6 @@ const executorParams: ExecutorSubActionPushParams = { }, { commentId: 'case-comment-2', - version: 'WlK3LDFd', comment: 'Another comment', createdAt: '2020-04-27T10:59:46.202Z', createdBy: { fullName: 'Elastic User', username: 'elastic' }, diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts index 76a3a9ab2cb8d9..86a83188412712 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts @@ -79,7 +79,6 @@ describe('api', () => { incidentId: 'incident-1', comment: { commentId: 'case-comment-1', - version: 'WzU3LDFd', comment: 'A comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', createdAt: '2020-03-13T08:34:53.450Z', createdBy: { @@ -99,7 +98,6 @@ describe('api', () => { incidentId: 'incident-1', comment: { commentId: 'case-comment-2', - version: 'WlK3LDFd', comment: 'Another comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', createdAt: '2020-03-13T08:34:53.450Z', createdBy: { @@ -175,7 +173,6 @@ describe('api', () => { incidentId: 'incident-2', comment: { commentId: 'case-comment-1', - version: 'WzU3LDFd', comment: 'A comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', createdAt: '2020-03-13T08:34:53.450Z', createdBy: { @@ -195,7 +192,6 @@ describe('api', () => { incidentId: 'incident-2', comment: { commentId: 'case-comment-2', - version: 'WlK3LDFd', comment: 'Another comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', createdAt: '2020-03-13T08:34:53.450Z', createdBy: { 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 6cbfcb9a8766a4..37228380910b3d 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 @@ -92,7 +92,6 @@ const executorParams: ExecutorSubActionPushParams = { comments: [ { commentId: 'case-comment-1', - version: 'WzU3LDFd', comment: 'A comment', createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: 'Elastic User', username: 'elastic' }, @@ -101,7 +100,6 @@ const executorParams: ExecutorSubActionPushParams = { }, { commentId: 'case-comment-2', - version: 'WlK3LDFd', comment: 'Another comment', createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: 'Elastic User', username: 'elastic' }, diff --git a/x-pack/plugins/case/common/api/cases/case.ts b/x-pack/plugins/case/common/api/cases/case.ts index a1ed3a957d68c7..d1bcae549805e8 100644 --- a/x-pack/plugins/case/common/api/cases/case.ts +++ b/x-pack/plugins/case/common/api/cases/case.ts @@ -127,21 +127,17 @@ export const ServiceConnectorCommentParamsRt = rt.type({ updatedBy: rt.union([ServiceConnectorUserParams, rt.null]), }); -export const ServiceConnectorCaseParamsRt = rt.intersection([ - rt.type({ - caseId: rt.string, - createdAt: rt.string, - createdBy: ServiceConnectorUserParams, - externalId: rt.union([rt.string, rt.null]), - title: rt.string, - updatedAt: rt.union([rt.string, rt.null]), - updatedBy: rt.union([ServiceConnectorUserParams, rt.null]), - }), - rt.partial({ - description: rt.string, - comments: rt.array(ServiceConnectorCommentParamsRt), - }), -]); +export const ServiceConnectorCaseParamsRt = rt.type({ + caseId: rt.string, + createdAt: rt.string, + createdBy: ServiceConnectorUserParams, + externalId: rt.union([rt.string, rt.null]), + title: rt.string, + updatedAt: rt.union([rt.string, rt.null]), + updatedBy: rt.union([ServiceConnectorUserParams, rt.null]), + description: rt.union([rt.string, rt.null]), + comments: rt.union([rt.array(ServiceConnectorCommentParamsRt), rt.null]), +}); export const ServiceConnectorCaseResponseRt = rt.intersection([ rt.type({ diff --git a/x-pack/plugins/siem/public/containers/case/mock.ts b/x-pack/plugins/siem/public/containers/case/mock.ts index a8f7aa4b944759..a3a8db2c40950c 100644 --- a/x-pack/plugins/siem/public/containers/case/mock.ts +++ b/x-pack/plugins/siem/public/containers/case/mock.ts @@ -135,6 +135,7 @@ export const casePushParams = { updatedAt: basicCreatedAt, updatedBy: elasticUser, description: 'nice', + comments: null, }; export const actionTypeExecutorResult = { actionId: 'string', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts index fd4b5ff2c1e665..ed63d25d86aca6 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts @@ -454,7 +454,7 @@ export default function jiraTest({ 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.0.commentId]: 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.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]', }); }); }); @@ -482,7 +482,7 @@ export default function jiraTest({ 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.0.comment]: 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.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]', }); }); }); @@ -510,7 +510,7 @@ export default function jiraTest({ 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.0.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.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]', }); }); }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts index 69d54384f2984c..04cd06999f4321 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts @@ -425,7 +425,7 @@ export default function servicenowTest({ 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.0.commentId]: 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.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]', }); }); }); @@ -453,7 +453,7 @@ export default function servicenowTest({ 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.0.comment]: 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.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]', }); }); }); @@ -481,7 +481,7 @@ export default function servicenowTest({ 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.0.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.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]', }); }); });