From 2fed291b6643b65efcf1cfaa8d8f31823dee9de5 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 12 Apr 2022 15:17:43 -0600 Subject: [PATCH 01/97] bootstrap cases webhook --- .../builtin_action_types/cases_webhook.ts | 325 +++++++++++++ .../server/builtin_action_types/index.ts | 4 + x-pack/plugins/actions/server/index.ts | 2 + .../cases_webhook/index.ts | 8 + .../cases_webhook/translations.ts | 64 +++ .../cases_webhook/webhook.test.tsx | 186 ++++++++ .../cases_webhook/webhook.tsx | 102 ++++ .../cases_webhook/webhook_connectors.test.tsx | 138 ++++++ .../cases_webhook/webhook_connectors.tsx | 440 ++++++++++++++++++ .../cases_webhook/webhook_params.test.tsx | 52 +++ .../cases_webhook/webhook_params.tsx | 53 +++ .../components/builtin_action_types/index.ts | 2 + .../components/builtin_action_types/types.ts | 21 + 13 files changed, 1397 insertions(+) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/cases_webhook.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/index.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_params.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_params.tsx diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook.ts new file mode 100644 index 00000000000000..55fb4ebc692de6 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook.ts @@ -0,0 +1,325 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { curry, isString } from 'lodash'; +import axios, { AxiosError, AxiosResponse } from 'axios'; +import { schema, TypeOf } from '@kbn/config-schema'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { map, getOrElse } from 'fp-ts/lib/Option'; +import { getRetryAfterIntervalFromHeaders } from './lib/http_rersponse_retry_header'; +import { nullableType } from './lib/nullable'; +import { isOk, promiseResult, Result } from './lib/result_type'; +import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types'; +import { ActionsConfigurationUtilities } from '../actions_config'; +import { Logger } from '../../../../../src/core/server'; +import { request } from './lib/axios_utils'; +import { renderMustacheString } from '../lib/mustache_renderer'; + +// config definition +export enum CasesWebhookMethods { + POST = 'post', + PUT = 'put', +} + +export type CasesWebhookActionType = ActionType< + ActionTypeConfigType, + ActionTypeSecretsType, + ActionParamsType, + unknown +>; +export type CasesWebhookActionTypeExecutorOptions = ActionTypeExecutorOptions< + ActionTypeConfigType, + ActionTypeSecretsType, + ActionParamsType +>; + +const HeadersSchema = schema.recordOf(schema.string(), schema.string()); +const configSchemaProps = { + url: schema.string(), + method: schema.oneOf( + [schema.literal(CasesWebhookMethods.POST), schema.literal(CasesWebhookMethods.PUT)], + { + defaultValue: CasesWebhookMethods.POST, + } + ), + headers: nullableType(HeadersSchema), + hasAuth: schema.boolean({ defaultValue: true }), +}; +const ConfigSchema = schema.object(configSchemaProps); +export type ActionTypeConfigType = TypeOf; + +// secrets definition +export type ActionTypeSecretsType = TypeOf; +const secretSchemaProps = { + user: schema.nullable(schema.string()), + password: schema.nullable(schema.string()), +}; +const SecretsSchema = schema.object(secretSchemaProps, { + validate: (secrets) => { + // user and password must be set together (or not at all) + if (!secrets.password && !secrets.user) return; + if (secrets.password && secrets.user) return; + return i18n.translate('xpack.actions.builtin.casesWebhook.invalidUsernamePassword', { + defaultMessage: 'both user and password must be specified', + }); + }, +}); + +// params definition +export type ActionParamsType = TypeOf; +const ParamsSchema = schema.object({ + body: schema.maybe(schema.string()), +}); + +export const ActionTypeId = '.cases-webhook'; +// action type definition +export function getActionType({ + logger, + configurationUtilities, +}: { + logger: Logger; + configurationUtilities: ActionsConfigurationUtilities; +}): CasesWebhookActionType { + return { + id: ActionTypeId, + minimumLicenseRequired: 'gold', + name: i18n.translate('xpack.actions.builtin.casesWebhookTitle', { + defaultMessage: 'Cases Webhook', + }), + validate: { + config: schema.object(configSchemaProps, { + validate: curry(validateActionTypeConfig)(configurationUtilities), + }), + secrets: SecretsSchema, + params: ParamsSchema, + }, + renderParameterTemplates, + executor: curry(executor)({ logger, configurationUtilities }), + }; +} + +function renderParameterTemplates( + params: ActionParamsType, + variables: Record +): ActionParamsType { + if (!params.body) return params; + return { + body: renderMustacheString(params.body, variables, 'json'), + }; +} + +function validateActionTypeConfig( + configurationUtilities: ActionsConfigurationUtilities, + configObject: ActionTypeConfigType +) { + const configuredUrl = configObject.url; + try { + new URL(configuredUrl); + } catch (err) { + return i18n.translate( + 'xpack.actions.builtin.casesWebhook.casesWebhookConfigurationErrorNoHostname', + { + defaultMessage: 'error configuring cases webhook action: unable to parse url: {err}', + values: { + err, + }, + } + ); + } + + try { + configurationUtilities.ensureUriAllowed(configuredUrl); + } catch (allowListError) { + return i18n.translate('xpack.actions.builtin.casesWebhook.casesWebhookConfigurationError', { + defaultMessage: 'error configuring cases webhook action: {message}', + values: { + message: allowListError.message, + }, + }); + } +} + +// action executor +export async function executor( + { + logger, + configurationUtilities, + }: { logger: Logger; configurationUtilities: ActionsConfigurationUtilities }, + execOptions: CasesWebhookActionTypeExecutorOptions +): Promise> { + const actionId = execOptions.actionId; + const { method, url, headers = {}, hasAuth } = execOptions.config; + const { body: data } = execOptions.params; + + const secrets: ActionTypeSecretsType = execOptions.secrets; + const basicAuth = + hasAuth && isString(secrets.user) && isString(secrets.password) + ? { auth: { username: secrets.user, password: secrets.password } } + : {}; + + const axiosInstance = axios.create(); + + const result: Result = await promiseResult( + request({ + axios: axiosInstance, + method, + url, + logger, + ...basicAuth, + headers, + data, + configurationUtilities, + }) + ); + + if (isOk(result)) { + const { + value: { status, statusText }, + } = result; + logger.debug( + `response from cases webhook action "${actionId}": [HTTP ${status}] ${statusText}` + ); + + return successResult(actionId, data); + } else { + const { error } = result; + if (error.response) { + const { + status, + statusText, + headers: responseHeaders, + data: { message: responseMessage }, + } = error.response; + const responseMessageAsSuffix = responseMessage ? `: ${responseMessage}` : ''; + const message = `[${status}] ${statusText}${responseMessageAsSuffix}`; + logger.error(`error on ${actionId} cases webhook event: ${message}`); + // The request was made and the server responded with a status code + // that falls out of the range of 2xx + // special handling for 5xx + if (status >= 500) { + return retryResult(actionId, message); + } + + // special handling for rate limiting + if (status === 429) { + return pipe( + getRetryAfterIntervalFromHeaders(responseHeaders), + map((retry) => retryResultSeconds(actionId, message, retry)), + getOrElse(() => retryResult(actionId, message)) + ); + } + return errorResultInvalid(actionId, message); + } else if (error.code) { + const message = `[${error.code}] ${error.message}`; + logger.error(`error on ${actionId} cases webhook event: ${message}`); + return errorResultRequestFailed(actionId, message); + } else if (error.isAxiosError) { + const message = `${error.message}`; + logger.error(`error on ${actionId} cases webhook event: ${message}`); + return errorResultRequestFailed(actionId, message); + } + + logger.error(`error on ${actionId} cases webhook action: unexpected error`); + return errorResultUnexpectedError(actionId); + } +} + +// Action Executor Result w/ internationalisation +function successResult(actionId: string, data: unknown): ActionTypeExecutorResult { + return { status: 'ok', data, actionId }; +} + +function errorResultInvalid( + actionId: string, + serviceMessage: string +): ActionTypeExecutorResult { + const errMessage = i18n.translate( + 'xpack.actions.builtin.casesWebhook.invalidResponseErrorMessage', + { + defaultMessage: 'error calling cases webhook, invalid response', + } + ); + return { + status: 'error', + message: errMessage, + actionId, + serviceMessage, + }; +} + +function errorResultRequestFailed( + actionId: string, + serviceMessage: string +): ActionTypeExecutorResult { + const errMessage = i18n.translate( + 'xpack.actions.builtin.casesWebhook.requestFailedErrorMessage', + { + defaultMessage: 'error calling cases webhook, request failed', + } + ); + return { + status: 'error', + message: errMessage, + actionId, + serviceMessage, + }; +} + +function errorResultUnexpectedError(actionId: string): ActionTypeExecutorResult { + const errMessage = i18n.translate('xpack.actions.builtin.casesWebhook.unreachableErrorMessage', { + defaultMessage: 'error calling cases webhook, unexpected error', + }); + return { + status: 'error', + message: errMessage, + actionId, + }; +} + +function retryResult(actionId: string, serviceMessage: string): ActionTypeExecutorResult { + const errMessage = i18n.translate( + 'xpack.actions.builtin.casesWebhook.invalidResponseRetryLaterErrorMessage', + { + defaultMessage: 'error calling cases webhook, retry later', + } + ); + return { + status: 'error', + message: errMessage, + retry: true, + actionId, + serviceMessage, + }; +} + +function retryResultSeconds( + actionId: string, + serviceMessage: string, + + retryAfter: number +): ActionTypeExecutorResult { + const retryEpoch = Date.now() + retryAfter * 1000; + const retry = new Date(retryEpoch); + const retryString = retry.toISOString(); + const errMessage = i18n.translate( + 'xpack.actions.builtin.casesWebhook.invalidResponseRetryDateErrorMessage', + { + defaultMessage: 'error calling cases webhook, retry at {retryString}', + values: { + retryString, + }, + } + ); + return { + status: 'error', + message: errMessage, + retry, + actionId, + serviceMessage, + }; +} 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 5c9c8e784af7e2..7d7486fd5643ff 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/index.ts @@ -16,6 +16,7 @@ import { getActionType as getSwimlaneActionType } from './swimlane'; import { getActionType as getServerLogActionType } from './server_log'; import { getActionType as getSlackActionType } from './slack'; import { getActionType as getWebhookActionType } from './webhook'; +import { getActionType as getCasesWebhookActionType } from './cases_webhook'; import { getActionType as getXmattersActionType } from './xmatters'; import { getServiceNowITSMActionType, @@ -36,6 +37,8 @@ export { ActionTypeId as ServerLogActionTypeId } from './server_log'; export type { ActionParamsType as SlackActionParams } from './slack'; export { ActionTypeId as SlackActionTypeId } from './slack'; export type { ActionParamsType as WebhookActionParams } from './webhook'; +export type { ActionParamsType as CasesWebhookActionParams } from './cases_webhook'; +export { ActionTypeId as CasesWebhookActionTypeId } from './cases_webhook'; export { ActionTypeId as WebhookActionTypeId } from './webhook'; export type { ActionParamsType as XmattersActionParams } from './xmatters'; export { ActionTypeId as XmattersActionTypeId } from './xmatters'; @@ -72,6 +75,7 @@ export function registerBuiltInActionTypes({ actionTypeRegistry.register(getServerLogActionType({ logger })); actionTypeRegistry.register(getSlackActionType({ logger, configurationUtilities })); actionTypeRegistry.register(getWebhookActionType({ logger, configurationUtilities })); + actionTypeRegistry.register(getCasesWebhookActionType({ logger, configurationUtilities })); actionTypeRegistry.register(getXmattersActionType({ logger, configurationUtilities })); actionTypeRegistry.register(getServiceNowITSMActionType({ logger, configurationUtilities })); actionTypeRegistry.register(getServiceNowSIRActionType({ logger, configurationUtilities })); diff --git a/x-pack/plugins/actions/server/index.ts b/x-pack/plugins/actions/server/index.ts index 9af6db47d076c2..0405d9135961b2 100644 --- a/x-pack/plugins/actions/server/index.ts +++ b/x-pack/plugins/actions/server/index.ts @@ -26,6 +26,8 @@ export type { } from './types'; export type { + CasesWebhookActionTypeId, + CasesWebhookActionParams, EmailActionTypeId, EmailActionParams, IndexActionTypeId, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/index.ts new file mode 100644 index 00000000000000..63e1475a115fd6 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { getActionType as getCasesWebhookActionType } from './webhook'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts new file mode 100644 index 00000000000000..9ed750bb3ebd00 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const URL_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.error.requiredUrlText', + { + defaultMessage: 'URL is required.', + } +); + +export const URL_INVALID = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.error.invalidUrlTextField', + { + defaultMessage: 'URL is invalid.', + } +); + +export const METHOD_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.sections.addAction.casesWebhookAction.error.requiredMethodText', + { + defaultMessage: 'Method is required.', + } +); + +export const USERNAME_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.sections.addAction.casesWebhookAction.error.requiredAuthUserNameText', + { + defaultMessage: 'Username is required.', + } +); + +export const PASSWORD_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.sections.addAction.casesWebhookAction.error.requiredAuthPasswordText', + { + defaultMessage: 'Password is required.', + } +); + +export const PASSWORD_REQUIRED_FOR_USER = i18n.translate( + 'xpack.triggersActionsUI.sections.addAction.casesWebhookAction.error.requiredPasswordText', + { + defaultMessage: 'Password is required when username is used.', + } +); + +export const USERNAME_REQUIRED_FOR_PASSWORD = i18n.translate( + 'xpack.triggersActionsUI.sections.addAction.casesWebhookAction.error.requiredUserText', + { + defaultMessage: 'Username is required when password is used.', + } +); + +export const BODY_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredWebhookBodyText', + { + defaultMessage: 'Body is required.', + } +); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.test.tsx new file mode 100644 index 00000000000000..3e42e7965c5bde --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.test.tsx @@ -0,0 +1,186 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TypeRegistry } from '../../../type_registry'; +import { registerBuiltInActionTypes } from '.././index'; +import { ActionTypeModel } from '../../../../types'; +import { WebhookActionConnector } from '../types'; + +const ACTION_TYPE_ID = '.webhook'; +let actionTypeModel: ActionTypeModel; + +beforeAll(() => { + const actionTypeRegistry = new TypeRegistry(); + registerBuiltInActionTypes({ actionTypeRegistry }); + const getResult = actionTypeRegistry.get(ACTION_TYPE_ID); + if (getResult !== null) { + actionTypeModel = getResult; + } +}); + +describe('actionTypeRegistry.get() works', () => { + test('action type static data is as expected', () => { + expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID); + expect(actionTypeModel.iconClass).toEqual('logoWebhook'); + }); +}); + +describe('webhook connector validation', () => { + test('connector validation succeeds when hasAuth is true and connector config is valid', async () => { + const actionConnector = { + secrets: { + user: 'user', + password: 'pass', + }, + id: 'test', + actionTypeId: '.webhook', + name: 'webhook', + isPreconfigured: false, + config: { + method: 'PUT', + url: 'http://test.com', + headers: { 'content-type': 'text' }, + hasAuth: true, + }, + } as WebhookActionConnector; + + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ + config: { + errors: { + url: [], + method: [], + }, + }, + secrets: { + errors: { + user: [], + password: [], + }, + }, + }); + }); + + test('connector validation succeeds when hasAuth is false and connector config is valid', async () => { + const actionConnector = { + secrets: { + user: '', + password: '', + }, + id: 'test', + actionTypeId: '.webhook', + name: 'webhook', + isPreconfigured: false, + config: { + method: 'PUT', + url: 'http://test.com', + headers: { 'content-type': 'text' }, + hasAuth: false, + }, + } as WebhookActionConnector; + + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ + config: { + errors: { + url: [], + method: [], + }, + }, + secrets: { + errors: { + user: [], + password: [], + }, + }, + }); + }); + + test('connector validation fails when connector config is not valid', async () => { + const actionConnector = { + secrets: { + user: 'user', + }, + id: 'test', + actionTypeId: '.webhook', + name: 'webhook', + config: { + method: 'PUT', + hasAuth: true, + }, + } as WebhookActionConnector; + + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ + config: { + errors: { + url: ['URL is required.'], + method: [], + }, + }, + secrets: { + errors: { + user: [], + password: ['Password is required when username is used.'], + }, + }, + }); + }); + + test('connector validation fails when url in config is not valid', async () => { + const actionConnector = { + secrets: { + user: 'user', + password: 'pass', + }, + id: 'test', + actionTypeId: '.webhook', + name: 'webhook', + config: { + method: 'PUT', + url: 'invalid.url', + hasAuth: true, + }, + } as WebhookActionConnector; + + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ + config: { + errors: { + url: ['URL is invalid.'], + method: [], + }, + }, + secrets: { + errors: { + user: [], + password: [], + }, + }, + }); + }); +}); + +describe('webhook action params validation', () => { + test('action params validation succeeds when action params is valid', async () => { + const actionParams = { + body: 'message {test}', + }; + + expect(await actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { body: [] }, + }); + }); + + test('params validation fails when body is not valid', async () => { + const actionParams = { + body: '', + }; + + expect(await actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { + body: ['Body is required.'], + }, + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx new file mode 100644 index 00000000000000..0d14b91d08516a --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { lazy } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + ActionTypeModel, + GenericValidationResult, + ConnectorValidationResult, +} from '../../../../types'; +import { + CasesWebhookActionParams, + CasesWebhookConfig, + CasesWebhookSecrets, + CasesWebhookActionConnector, +} from '../types'; +import { isValidUrl } from '../../../lib/value_validators'; + +export function getActionType(): ActionTypeModel< + CasesWebhookConfig, + CasesWebhookSecrets, + CasesWebhookActionParams +> { + return { + id: '.cases-webhook', + // TODO: Steph/cases webhook get an icon + iconClass: 'indexManagementApp', + selectMessage: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.selectMessageText', + { + defaultMessage: 'Send a request to a Case Management web service.', + } + ), + actionTypeTitle: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.actionTypeTitle', + { + defaultMessage: 'Cases Webhook data', + } + ), + validateConnector: async ( + action: CasesWebhookActionConnector + ): Promise< + ConnectorValidationResult, CasesWebhookSecrets> + > => { + const translations = await import('./translations'); + const configErrors = { + url: new Array(), + method: new Array(), + }; + const secretsErrors = { + user: new Array(), + password: new Array(), + }; + const validationResult = { + config: { errors: configErrors }, + secrets: { errors: secretsErrors }, + }; + if (!action.config.url) { + configErrors.url.push(translations.URL_REQUIRED); + } + if (action.config.url && !isValidUrl(action.config.url)) { + configErrors.url = [...configErrors.url, translations.URL_INVALID]; + } + if (!action.config.method) { + configErrors.method.push(translations.METHOD_REQUIRED); + } + if (action.config.hasAuth && !action.secrets.user && !action.secrets.password) { + secretsErrors.user.push(translations.USERNAME_REQUIRED); + } + if (action.config.hasAuth && !action.secrets.user && !action.secrets.password) { + secretsErrors.password.push(translations.PASSWORD_REQUIRED); + } + if (action.secrets.user && !action.secrets.password) { + secretsErrors.password.push(translations.PASSWORD_REQUIRED_FOR_USER); + } + if (!action.secrets.user && action.secrets.password) { + secretsErrors.user.push(translations.USERNAME_REQUIRED_FOR_PASSWORD); + } + return validationResult; + }, + validateParams: async ( + actionParams: CasesWebhookActionParams + ): Promise> => { + const translations = await import('./translations'); + const errors = { + body: new Array(), + }; + const validationResult = { errors }; + validationResult.errors = errors; + if (!actionParams.body?.length) { + errors.body.push(translations.BODY_REQUIRED); + } + return validationResult; + }, + actionConnectorFields: lazy(() => import('./webhook_connectors')), + actionParamsFields: lazy(() => import('./webhook_params')), + }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx new file mode 100644 index 00000000000000..2579d8dd1a93b4 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx @@ -0,0 +1,138 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { mountWithIntl } from '@kbn/test-jest-helpers'; +import { WebhookActionConnector } from '../types'; +import WebhookActionConnectorFields from './webhook_connectors'; + +describe('WebhookActionConnectorFields renders', () => { + test('all connector fields is rendered', () => { + const actionConnector = { + secrets: { + user: 'user', + password: 'pass', + }, + id: 'test', + actionTypeId: '.webhook', + isPreconfigured: false, + name: 'webhook', + config: { + method: 'PUT', + url: 'http:\\test', + headers: { 'content-type': 'text' }, + hasAuth: true, + }, + } as WebhookActionConnector; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + readOnly={false} + setCallbacks={() => {}} + isEdit={false} + /> + ); + expect(wrapper.find('[data-test-subj="webhookViewHeadersSwitch"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="webhookHeaderText"]').length > 0).toBeTruthy(); + wrapper.find('[data-test-subj="webhookViewHeadersSwitch"]').first().simulate('click'); + expect(wrapper.find('[data-test-subj="webhookMethodSelect"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="webhookUrlText"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="webhookUserInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="webhookPasswordInput"]').length > 0).toBeTruthy(); + }); + + test('should display a message on create to remember credentials', () => { + const actionConnector = { + secrets: {}, + actionTypeId: '.webhook', + isPreconfigured: false, + config: { + hasAuth: true, + }, + } as WebhookActionConnector; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + readOnly={false} + setCallbacks={() => {}} + isEdit={false} + /> + ); + expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0); + expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toEqual(0); + }); + + test('should display a message on edit to re-enter credentials', () => { + const actionConnector = { + secrets: { + user: 'user', + password: 'pass', + }, + id: 'test', + actionTypeId: '.webhook', + isPreconfigured: false, + name: 'webhook', + config: { + method: 'PUT', + url: 'http:\\test', + headers: { 'content-type': 'text' }, + hasAuth: true, + }, + } as WebhookActionConnector; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + readOnly={false} + setCallbacks={() => {}} + isEdit={false} + /> + ); + expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0); + expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0); + }); + + test('should display a message for missing secrets after import', () => { + const actionConnector = { + secrets: { + user: 'user', + password: 'pass', + }, + id: 'test', + actionTypeId: '.webhook', + isPreconfigured: false, + isMissingSecrets: true, + name: 'webhook', + config: { + method: 'PUT', + url: 'http:\\test', + headers: { 'content-type': 'text' }, + hasAuth: true, + }, + } as WebhookActionConnector; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + readOnly={false} + setCallbacks={() => {}} + isEdit={false} + /> + ); + expect(wrapper.find('[data-test-subj="missingSecretsMessage"]').length).toBeGreaterThan(0); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx new file mode 100644 index 00000000000000..f86e50509de611 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx @@ -0,0 +1,440 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import { + EuiFieldPassword, + EuiFieldText, + EuiFormRow, + EuiSelect, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiButtonIcon, + EuiDescriptionList, + EuiDescriptionListDescription, + EuiDescriptionListTitle, + EuiTitle, + EuiSwitch, + EuiButtonEmpty, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ActionConnectorFieldsProps } from '../../../../types'; +import { CasesWebhookActionConnector } from '../types'; +import { getEncryptedFieldNotifyLabel } from '../../get_encrypted_field_notify_label'; + +const HTTP_VERBS = ['post', 'put']; + +const CasesWebhookActionConnectorFields: React.FunctionComponent< + ActionConnectorFieldsProps +> = ({ action, editActionConfig, editActionSecrets, errors, readOnly }) => { + const { user, password } = action.secrets; + const { method, url, headers, hasAuth } = action.config; + + const [httpHeaderKey, setHttpHeaderKey] = useState(''); + const [httpHeaderValue, setHttpHeaderValue] = useState(''); + const [hasHeaders, setHasHeaders] = useState(false); + + useEffect(() => { + if (!action.id) { + editActionConfig('hasAuth', true); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + if (!method) { + editActionConfig('method', 'post'); // set method to POST by default + } + + const headerErrors = { + keyHeader: new Array(), + valueHeader: new Array(), + }; + if (!httpHeaderKey && httpHeaderValue) { + headerErrors.keyHeader.push( + i18n.translate( + 'xpack.triggersActionsUI.sections.addAction.casesWebhookAction.error.requiredHeaderKeyText', + { + defaultMessage: 'Key is required.', + } + ) + ); + } + if (httpHeaderKey && !httpHeaderValue) { + headerErrors.valueHeader.push( + i18n.translate( + 'xpack.triggersActionsUI.sections.addAction.casesWebhookAction.error.requiredHeaderValueText', + { + defaultMessage: 'Value is required.', + } + ) + ); + } + const hasHeaderErrors: boolean = + (headerErrors.keyHeader !== undefined && + headerErrors.valueHeader !== undefined && + headerErrors.keyHeader.length > 0) || + headerErrors.valueHeader.length > 0; + + function addHeader() { + if (headers && !!Object.keys(headers).find((key) => key === httpHeaderKey)) { + return; + } + const updatedHeaders = headers + ? { ...headers, [httpHeaderKey]: httpHeaderValue } + : { [httpHeaderKey]: httpHeaderValue }; + editActionConfig('headers', updatedHeaders); + setHttpHeaderKey(''); + setHttpHeaderValue(''); + } + + function viewHeaders() { + setHasHeaders(!hasHeaders); + if (!hasHeaders && !headers) { + editActionConfig('headers', {}); + } + } + + function removeHeader(keyToRemove: string) { + const updatedHeaders = Object.keys(headers) + .filter((key) => key !== keyToRemove) + .reduce((headerToRemove: Record, key: string) => { + headerToRemove[key] = headers[key]; + return headerToRemove; + }, {}); + editActionConfig('headers', updatedHeaders); + } + + let headerControl; + if (hasHeaders) { + headerControl = ( + <> + +
+ +
+
+ + + + + { + setHttpHeaderKey(e.target.value); + }} + /> + + + + + { + setHttpHeaderValue(e.target.value); + }} + /> + + + + + addHeader()} + > + + + + + + + ); + } + + const headersList = Object.keys(headers || {}).map((key: string) => { + return ( + + + removeHeader(key)} + /> + + + + {key} + {headers[key]} + + + + ); + }); + + const isUrlInvalid: boolean = + errors.url !== undefined && errors.url.length > 0 && url !== undefined; + const isPasswordInvalid: boolean = + password !== undefined && errors.password !== undefined && errors.password.length > 0; + const isUserInvalid: boolean = + user !== undefined && errors.user !== undefined && errors.user.length > 0; + + return ( + <> + + + + ({ text: verb.toUpperCase(), value: verb }))} + onChange={(e) => { + editActionConfig('method', e.target.value); + }} + /> + + + + + { + editActionConfig('url', e.target.value); + }} + onBlur={() => { + if (!url) { + editActionConfig('url', ''); + } + }} + /> + + + + + + + +

+ +

+
+ + { + editActionConfig('hasAuth', e.target.checked); + if (!e.target.checked) { + editActionSecrets('user', null); + editActionSecrets('password', null); + } + }} + /> +
+
+ {hasAuth ? ( + <> + {getEncryptedFieldNotifyLabel( + !action.id, + 2, + action.isMissingSecrets ?? false, + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.reenterValuesLabel', + { + defaultMessage: + 'Username and password are encrypted. Please reenter values for these fields.', + } + ) + )} + + + + { + editActionSecrets('user', e.target.value); + }} + onBlur={() => { + if (!user) { + editActionSecrets('user', ''); + } + }} + /> + + + + + { + editActionSecrets('password', e.target.value); + }} + onBlur={() => { + if (!password) { + editActionSecrets('password', ''); + } + }} + /> + + + + + ) : null} + + viewHeaders()} + /> + + +
+ {Object.keys(headers || {}).length > 0 ? ( + <> + + +
+ +
+
+ + {headersList} + + ) : null} + + {hasHeaders && headerControl} + +
+ + ); +}; + +// eslint-disable-next-line import/no-default-export +export { CasesWebhookActionConnectorFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_params.test.tsx new file mode 100644 index 00000000000000..064d21b50e463e --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_params.test.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { mountWithIntl } from '@kbn/test-jest-helpers'; +import WebhookParamsFields from './webhook_params'; +import { MockCodeEditor } from '../../../code_editor.mock'; + +const kibanaReactPath = '../../../../../../../../src/plugins/kibana_react/public'; + +jest.mock(kibanaReactPath, () => { + const original = jest.requireActual(kibanaReactPath); + return { + ...original, + CodeEditor: (props: any) => { + return ; + }, + }; +}); + +describe('WebhookParamsFields renders', () => { + test('all params fields is rendered', () => { + const actionParams = { + body: 'test message', + }; + + const wrapper = mountWithIntl( + {}} + index={0} + messageVariables={[ + { + name: 'myVar', + description: 'My variable description', + useWithTripleBracesInTemplates: true, + }, + ]} + /> + ); + expect(wrapper.find('[data-test-subj="bodyJsonEditor"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="bodyJsonEditor"]').first().prop('value')).toStrictEqual( + 'test message' + ); + expect(wrapper.find('[data-test-subj="bodyAddVariableButton"]').length > 0).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_params.tsx new file mode 100644 index 00000000000000..d7f296fc808d20 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_params.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { ActionParamsProps } from '../../../../types'; +import { WebhookActionParams } from '../types'; +import { JsonEditorWithMessageVariables } from '../../json_editor_with_message_variables'; + +const WebhookParamsFields: React.FunctionComponent> = ({ + actionParams, + editAction, + index, + messageVariables, + errors, +}) => { + const { body } = actionParams; + return ( + { + editAction('body', json, index); + }} + onBlur={() => { + if (!body) { + editAction('body', '', index); + } + }} + /> + ); +}; + +// eslint-disable-next-line import/no-default-export +export { WebhookParamsFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts index 6817631e2150a3..a4c7447ac8a398 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts @@ -11,6 +11,7 @@ import { getEmailActionType } from './email'; import { getIndexActionType } from './es_index'; import { getPagerDutyActionType } from './pagerduty'; import { getSwimlaneActionType } from './swimlane'; +import { getCasesWebhookActionType } from './cases_webhook'; import { getWebhookActionType } from './webhook'; import { getXmattersActionType } from './xmatters'; import { TypeRegistry } from '../../type_registry'; @@ -35,6 +36,7 @@ export function registerBuiltInActionTypes({ actionTypeRegistry.register(getIndexActionType()); actionTypeRegistry.register(getPagerDutyActionType()); actionTypeRegistry.register(getSwimlaneActionType()); + actionTypeRegistry.register(getCasesWebhookActionType()); actionTypeRegistry.register(getWebhookActionType()); actionTypeRegistry.register(getXmattersActionType()); actionTypeRegistry.register(getServiceNowITSMActionType()); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts index 1d0c58ffb8f213..0aaa384d1a0b35 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts @@ -72,6 +72,10 @@ export interface WebhookActionParams { body?: string; } +export interface CasesWebhookActionParams { + body?: string; +} + export interface EmailConfig { from: string; host: string; @@ -132,6 +136,23 @@ export interface WebhookSecrets { export type WebhookActionConnector = UserConfiguredActionConnector; +export interface CasesWebhookConfig { + method: string; + url: string; + headers: Record; + hasAuth: boolean; +} + +export interface CasesWebhookSecrets { + user: string; + password: string; +} + +export type CasesWebhookActionConnector = UserConfiguredActionConnector< + CasesWebhookConfig, + CasesWebhookSecrets +>; + export enum XmattersSeverityOptions { CRITICAL = 'critical', HIGH = 'high', From 78907b6913ab05ff12cca2bc4bf01c0d6f345bf5 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 20 Apr 2022 07:57:07 -0600 Subject: [PATCH 02/97] wip --- .../common/api/connectors/cases_webhook.ts | 16 +++++++++++ .../cases/common/api/connectors/index.ts | 14 +++++++++- x-pack/plugins/cases/common/constants.ts | 1 + .../cases/server/client/configure/client.ts | 2 +- .../client/configure/create_mappings.ts | 1 + .../server/connectors/cases_webook/format.ts | 24 ++++++++++++++++ .../server/connectors/cases_webook/index.ts | 15 ++++++++++ .../server/connectors/cases_webook/mapping.ts | 28 +++++++++++++++++++ .../server/connectors/cases_webook/types.ts | 17 +++++++++++ .../cases/server/connectors/factory.ts | 1 + 10 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/cases/common/api/connectors/cases_webhook.ts create mode 100644 x-pack/plugins/cases/server/connectors/cases_webook/format.ts create mode 100644 x-pack/plugins/cases/server/connectors/cases_webook/index.ts create mode 100644 x-pack/plugins/cases/server/connectors/cases_webook/mapping.ts create mode 100644 x-pack/plugins/cases/server/connectors/cases_webook/types.ts diff --git a/x-pack/plugins/cases/common/api/connectors/cases_webhook.ts b/x-pack/plugins/cases/common/api/connectors/cases_webhook.ts new file mode 100644 index 00000000000000..e83c9b8b9b9cbf --- /dev/null +++ b/x-pack/plugins/cases/common/api/connectors/cases_webhook.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as rt from 'io-ts'; + +// New fields should also be added at: x-pack/plugins/cases/server/connectors/case/schema.ts +export const CasesWebhookFieldsRT = rt.record( + rt.string, + rt.union([rt.string, rt.array(rt.string), rt.undefined, rt.null]) +); + +export type CasesWebhookFieldsType = rt.TypeOf; diff --git a/x-pack/plugins/cases/common/api/connectors/index.ts b/x-pack/plugins/cases/common/api/connectors/index.ts index bb1892525f8e05..f579755f1d8738 100644 --- a/x-pack/plugins/cases/common/api/connectors/index.ts +++ b/x-pack/plugins/cases/common/api/connectors/index.ts @@ -8,12 +8,14 @@ import * as rt from 'io-ts'; import { ActionResult, ActionType } from '@kbn/actions-plugin/common'; +import { CasesWebhookFieldsRT } from './cases_webhook'; import { JiraFieldsRT } from './jira'; import { ResilientFieldsRT } from './resilient'; import { ServiceNowITSMFieldsRT } from './servicenow_itsm'; import { ServiceNowSIRFieldsRT } from './servicenow_sir'; import { SwimlaneFieldsRT } from './swimlane'; +export * from './cases_webhook'; export * from './jira'; export * from './servicenow_itsm'; export * from './servicenow_sir'; @@ -25,6 +27,7 @@ export type ActionConnector = ActionResult; export type ActionTypeConnector = ActionType; export const ConnectorFieldsRt = rt.union([ + CasesWebhookFieldsRT, JiraFieldsRT, ResilientFieldsRT, ServiceNowITSMFieldsRT, @@ -33,6 +36,7 @@ export const ConnectorFieldsRt = rt.union([ ]); export enum ConnectorTypes { + casesWebhook = '.cases-webhook', jira = '.jira', none = '.none', resilient = '.resilient', @@ -43,9 +47,14 @@ export enum ConnectorTypes { export const connectorTypes = Object.values(ConnectorTypes); +const ConnectorCasesWebhookTypeFieldsRt = rt.type({ + type: rt.literal(ConnectorTypes.casesWebhook), + fields: rt.union([CasesWebhookFieldsRT, rt.null]), +}); + const ConnectorJiraTypeFieldsRt = rt.type({ type: rt.literal(ConnectorTypes.jira), - fields: rt.union([JiraFieldsRT, rt.null]), + fields: rt.union([CasesWebhookFieldsRT, rt.null]), }); const ConnectorResilientTypeFieldsRt = rt.type({ @@ -76,6 +85,7 @@ const ConnectorNoneTypeFieldsRt = rt.type({ export const NONE_CONNECTOR_ID: string = 'none'; export const ConnectorTypeFieldsRt = rt.union([ + ConnectorCasesWebhookTypeFieldsRt, ConnectorJiraTypeFieldsRt, ConnectorNoneTypeFieldsRt, ConnectorResilientTypeFieldsRt, @@ -88,6 +98,7 @@ export const ConnectorTypeFieldsRt = rt.union([ * This type represents the connector's format when it is encoded within a user action. */ export const CaseUserActionConnectorRt = rt.union([ + rt.intersection([ConnectorCasesWebhookTypeFieldsRt, rt.type({ name: rt.string })]), rt.intersection([ConnectorJiraTypeFieldsRt, rt.type({ name: rt.string })]), rt.intersection([ConnectorNoneTypeFieldsRt, rt.type({ name: rt.string })]), rt.intersection([ConnectorResilientTypeFieldsRt, rt.type({ name: rt.string })]), @@ -106,6 +117,7 @@ export const CaseConnectorRt = rt.intersection([ export type CaseUserActionConnector = rt.TypeOf; export type CaseConnector = rt.TypeOf; export type ConnectorTypeFields = rt.TypeOf; +export type ConnectorCasesWebhookTypeFields = rt.TypeOf; export type ConnectorJiraTypeFields = rt.TypeOf; export type ConnectorResilientTypeFields = rt.TypeOf; export type ConnectorSwimlaneTypeFields = rt.TypeOf; diff --git a/x-pack/plugins/cases/common/constants.ts b/x-pack/plugins/cases/common/constants.ts index 29a8029dda0636..62f195bdf41a50 100644 --- a/x-pack/plugins/cases/common/constants.ts +++ b/x-pack/plugins/cases/common/constants.ts @@ -88,6 +88,7 @@ export const ACTION_TYPES_URL = `${ACTION_URL}/connector_types` as const; export const CONNECTORS_URL = `${ACTION_URL}/connectors` as const; export const SUPPORTED_CONNECTORS = [ + `${ConnectorTypes.casesWebhook}`, `${ConnectorTypes.serviceNowITSM}`, `${ConnectorTypes.serviceNowSIR}`, `${ConnectorTypes.jira}`, diff --git a/x-pack/plugins/cases/server/client/configure/client.ts b/x-pack/plugins/cases/server/client/configure/client.ts index c4b07019627e4a..df63a53c2f4325 100644 --- a/x-pack/plugins/cases/server/client/configure/client.ts +++ b/x-pack/plugins/cases/server/client/configure/client.ts @@ -394,7 +394,7 @@ async function create( const creationDate = new Date().toISOString(); let mappings: ConnectorMappingsAttributes[] = []; - + console.log('configuration', configuration); try { mappings = await casesClientInternal.configuration.createMappings({ connector: configuration.connector, diff --git a/x-pack/plugins/cases/server/client/configure/create_mappings.ts b/x-pack/plugins/cases/server/client/configure/create_mappings.ts index 52f00453ef0eca..3c9aa1bda9324f 100644 --- a/x-pack/plugins/cases/server/client/configure/create_mappings.ts +++ b/x-pack/plugins/cases/server/client/configure/create_mappings.ts @@ -20,6 +20,7 @@ export const createMappings = async ( try { const mappings = casesConnectors.get(connector.type)?.getMapping() ?? []; + console.log('mappings', mappings); const theMapping = await connectorMappingsService.post({ unsecuredSavedObjectsClient, diff --git a/x-pack/plugins/cases/server/connectors/cases_webook/format.ts b/x-pack/plugins/cases/server/connectors/cases_webook/format.ts new file mode 100644 index 00000000000000..3508c19d75eb78 --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/cases_webook/format.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ConnectorCasesWebhookTypeFields } from '../../../common/api'; +import { Format } from './types'; + +export const format: Format = (theCase, alerts) => { + const { + priority = null, + issueType = null, + parent = null, + } = (theCase.connector.fields as ConnectorCasesWebhookTypeFields['fields']) ?? {}; + return { + priority, + // CasesWebook do not allows empty spaces on labels. We replace white spaces with hyphens + labels: theCase.tags.map((tag) => tag.replace(/\s+/g, '-')), + issueType, + parent, + }; +}; diff --git a/x-pack/plugins/cases/server/connectors/cases_webook/index.ts b/x-pack/plugins/cases/server/connectors/cases_webook/index.ts new file mode 100644 index 00000000000000..961e7648d0cef7 --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/cases_webook/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getMapping } from './mapping'; +import { format } from './format'; +import { CasesWebhookCaseConnector } from './types'; + +export const getCaseConnector = (): CasesWebhookCaseConnector => ({ + getMapping, + format, +}); diff --git a/x-pack/plugins/cases/server/connectors/cases_webook/mapping.ts b/x-pack/plugins/cases/server/connectors/cases_webook/mapping.ts new file mode 100644 index 00000000000000..8f8a914b4e091a --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/cases_webook/mapping.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { GetMapping } from './types'; + +export const getMapping: GetMapping = () => { + return [ + { + source: 'title', + target: 'summary', + action_type: 'overwrite', + }, + { + source: 'description', + target: 'description', + action_type: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + action_type: 'append', + }, + ]; +}; diff --git a/x-pack/plugins/cases/server/connectors/cases_webook/types.ts b/x-pack/plugins/cases/server/connectors/cases_webook/types.ts new file mode 100644 index 00000000000000..0a0e42c7f29703 --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/cases_webook/types.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CasesWebhookFieldsType } from '../../../common/api'; +import { ICasesConnector } from '../types'; + +interface ExternalServiceFormatterParams extends CasesWebhookFieldsType { + labels: string[]; +} + +export type CasesWebhookCaseConnector = ICasesConnector; +export type Format = ICasesConnector['format']; +export type GetMapping = ICasesConnector['getMapping']; diff --git a/x-pack/plugins/cases/server/connectors/factory.ts b/x-pack/plugins/cases/server/connectors/factory.ts index 40a6702f11b0f6..a4506ef1293436 100644 --- a/x-pack/plugins/cases/server/connectors/factory.ts +++ b/x-pack/plugins/cases/server/connectors/factory.ts @@ -13,6 +13,7 @@ import { getServiceNowITSMCaseConnector, getServiceNowSIRCaseConnector } from '. import { getCaseConnector as getSwimlaneCaseConnector } from './swimlane'; const mapping: Record = { + [ConnectorTypes.casesWebhook]: getJiraCaseConnector(), [ConnectorTypes.jira]: getJiraCaseConnector(), [ConnectorTypes.serviceNowITSM]: getServiceNowITSMCaseConnector(), [ConnectorTypes.serviceNowSIR]: getServiceNowSIRCaseConnector(), From 094ff9bb1f54f1f3834a2653cbdb6f0b990fdfcc Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 4 May 2022 16:08:22 -0600 Subject: [PATCH 03/97] wip' --- .../index.ts} | 63 ++- .../cases_webhook/schema.ts | 26 + .../cases_webhook/service.ts | 503 ++++++++++++++++++ .../cases_webhook/translations.ts | 12 + .../cases_webhook/types.ts | 50 ++ .../server/builtin_action_types/jira/index.ts | 6 +- .../cases_webhook/translations.ts | 6 + .../cases_webhook/webhook.tsx | 14 +- .../cases_webhook/webhook_connectors.test.tsx | 22 +- .../cases_webhook/webhook_connectors.tsx | 60 ++- .../cases_webhook/webhook_params.tsx | 96 ++-- .../components/builtin_action_types/types.ts | 4 +- 12 files changed, 786 insertions(+), 76 deletions(-) rename x-pack/plugins/actions/server/builtin_action_types/{cases_webhook.ts => cases_webhook/index.ts} (87%) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/cases_webhook/translations.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts similarity index 87% rename from x-pack/plugins/actions/server/builtin_action_types/cases_webhook.ts rename to x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts index 55fb4ebc692de6..43fa89888c6c7a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts @@ -11,14 +11,15 @@ import axios, { AxiosError, AxiosResponse } from 'axios'; import { schema, TypeOf } from '@kbn/config-schema'; import { pipe } from 'fp-ts/lib/pipeable'; import { map, getOrElse } from 'fp-ts/lib/Option'; -import { getRetryAfterIntervalFromHeaders } from './lib/http_rersponse_retry_header'; -import { nullableType } from './lib/nullable'; -import { isOk, promiseResult, Result } from './lib/result_type'; -import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types'; -import { ActionsConfigurationUtilities } from '../actions_config'; -import { Logger } from '../../../../../src/core/server'; -import { request } from './lib/axios_utils'; -import { renderMustacheString } from '../lib/mustache_renderer'; +import { Logger } from '@kbn/core/server'; +import { getRetryAfterIntervalFromHeaders } from '../lib/http_rersponse_retry_header'; +import { nullableType } from '../lib/nullable'; +import { isOk, promiseResult, Result } from '../lib/result_type'; +import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../../types'; +import { ActionsConfigurationUtilities } from '../../actions_config'; +import { request } from '../lib/axios_utils'; +import { renderMustacheString } from '../../lib/mustache_renderer'; +import { createExternalService } from './service'; // config definition export enum CasesWebhookMethods { @@ -47,6 +48,7 @@ const configSchemaProps = { defaultValue: CasesWebhookMethods.POST, } ), + incident: schema.string(), // stringified object headers: nullableType(HeadersSchema), hasAuth: schema.boolean({ defaultValue: true }), }; @@ -73,7 +75,8 @@ const SecretsSchema = schema.object(secretSchemaProps, { // params definition export type ActionParamsType = TypeOf; const ParamsSchema = schema.object({ - body: schema.maybe(schema.string()), + summary: schema.string(), + description: schema.string(), }); export const ActionTypeId = '.cases-webhook'; @@ -107,9 +110,11 @@ function renderParameterTemplates( params: ActionParamsType, variables: Record ): ActionParamsType { - if (!params.body) return params; + if (!params.summary) return params; + if (!params.description) return params; return { - body: renderMustacheString(params.body, variables, 'json'), + summary: renderMustacheString(params.summary, variables, 'json'), + description: renderMustacheString(params.description, variables, 'json'), }; } @@ -153,30 +158,24 @@ export async function executor( execOptions: CasesWebhookActionTypeExecutorOptions ): Promise> { const actionId = execOptions.actionId; - const { method, url, headers = {}, hasAuth } = execOptions.config; - const { body: data } = execOptions.params; + const { summary, description } = execOptions.params; - const secrets: ActionTypeSecretsType = execOptions.secrets; - const basicAuth = - hasAuth && isString(secrets.user) && isString(secrets.password) - ? { auth: { username: secrets.user, password: secrets.password } } - : {}; - - const axiosInstance = axios.create(); - - const result: Result = await promiseResult( - request({ - axios: axiosInstance, - method, - url, - logger, - ...basicAuth, - headers, - data, - configurationUtilities, - }) + const externalService = createExternalService( + { + config: execOptions.config, + secrets: execOptions.secrets, + }, + logger, + configurationUtilities ); + console.log('call createIncident'); + const result = await externalService.createIncident({ + summary, + description, + }); + console.log('result', result); + if (isOk(result)) { const { value: { status, statusText }, diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts new file mode 100644 index 00000000000000..e6dcb942569663 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; + +export const ExternalIncidentServiceConfiguration = { + url: schema.string(), + incident: schema.string(), // JSON.stringified object +}; + +export const ExternalIncidentServiceConfigurationSchema = schema.object( + ExternalIncidentServiceConfiguration +); + +export const ExternalIncidentServiceSecretConfiguration = { + user: schema.string(), + password: schema.string(), +}; + +export const ExternalIncidentServiceSecretConfigurationSchema = schema.object( + ExternalIncidentServiceSecretConfiguration +); diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts new file mode 100644 index 00000000000000..87c94a42d00cdf --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts @@ -0,0 +1,503 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import axios from 'axios'; + +import { Logger } from '@kbn/core/server'; +import { + CreateIncidentParams, + ExternalServiceCredentials, + ResponseError, + ExternalServiceIncidentResponse, + ExternalService, + CasesWebhookPublicConfigurationType, + CasesWebhookSecretConfigurationType, +} from './types'; + +import * as i18n from './translations'; +import { request, getErrorMessage, throwIfResponseIsNotValid } from '../lib/axios_utils'; +import { ActionsConfigurationUtilities } from '../../actions_config'; + +const VERSION = '2'; +const BASE_URL = `rest/api/${VERSION}`; + +const VIEW_INCIDENT_URL = `browse`; + +export const createExternalService = ( + { config, secrets }: ExternalServiceCredentials, + logger: Logger, + configurationUtilities: ActionsConfigurationUtilities +): ExternalService => { + const { url, incident } = config as CasesWebhookPublicConfigurationType; + const { password, user } = secrets as CasesWebhookSecretConfigurationType; + console.log('eh eh eh', { + bool: !url || !password || !user, + config, + url, + password, + user, + }); + if (!url || !password || !user) { + throw Error(`[Action]${i18n.NAME}: Wrong configuration.`); + } + + const incidentUrl = url.endsWith('/') ? url.slice(0, -1) : url; + // const incidentUrl = `${urlWithoutTrailingSlash}/${BASE_URL}/issue`; + + const axiosInstance = axios.create({ + auth: { username: user, password }, + }); + // + const getIncidentViewURL = (key: string) => { + return `https://siem-kibana.atlassian.net/browse/${key}`; + }; + const createErrorMessage = (errorResponse: ResponseError | null | undefined): string => { + if (errorResponse == null) { + return 'unknown: errorResponse was null'; + } + + const { errorMessages, errors } = errorResponse; + + if (errors == null) { + return 'unknown: errorResponse.errors was null'; + } + + if (Array.isArray(errorMessages) && errorMessages.length > 0) { + return `${errorMessages.join(', ')}`; + } + + return Object.entries(errors).reduce((errorMessage, [, value]) => { + const msg = errorMessage.length > 0 ? `${errorMessage} ${value}` : value; + return msg; + }, ''); + }; + // + // const hasSupportForNewAPI = (capabilities: { capabilities?: {} }) => + // createMetaCapabilities.every((c) => Object.keys(capabilities?.capabilities ?? {}).includes(c)); + // + // const normalizeIssueTypes = (issueTypes: Array<{ id: string; name: string }>) => + // issueTypes.map((type) => ({ id: type.id, name: type.name })); + // + // const normalizeFields = (fields: { + // [key: string]: { + // allowedValues?: Array<{}>; + // defaultValue?: {}; + // name: string; + // required: boolean; + // schema: FieldSchema; + // }; + // }) => + // Object.keys(fields ?? {}).reduce( + // (fieldsAcc, fieldKey) => ({ + // ...fieldsAcc, + // [fieldKey]: { + // required: fields[fieldKey]?.required, + // allowedValues: fields[fieldKey]?.allowedValues ?? [], + // defaultValue: fields[fieldKey]?.defaultValue ?? {}, + // schema: fields[fieldKey]?.schema, + // name: fields[fieldKey]?.name, + // }, + // }), + // {} + // ); + // + // const normalizeSearchResults = ( + // issues: Array<{ id: string; key: string; fields: { summary: string } }> + // ) => + // issues.map((issue) => ({ id: issue.id, key: issue.key, title: issue.fields?.summary ?? null })); + // + // const normalizeIssue = (issue: { id: string; key: string; fields: { summary: string } }) => ({ + // id: issue.id, + // key: issue.key, + // title: issue.fields?.summary ?? null, + // }); + // + const getIncident = async (id: string) => { + try { + const res = await request({ + axios: axiosInstance, + url: `${incidentUrl}/${id}`, + logger, + configurationUtilities, + }); + + throwIfResponseIsNotValid({ + res, + requiredAttributesToBeInTheResponse: ['id', 'key'], + }); + + const { fields, id: incidentId, key } = res.data; + + return { id: incidentId, key, created: fields.created, updated: fields.updated, ...fields }; + } catch (error) { + throw new Error( + getErrorMessage( + i18n.NAME, + `Unable to get incident with id ${id}. Error: ${ + error.message + } Reason: ${createErrorMessage(error.response?.data)}` + ) + ); + } + }; + + const replaceSumDesc = (sum: string, desc: string) => { + let str = incident; // incident is stringified object + str = str.replace('$SUM', sum); + str = str.replace('$DESC', desc); + return JSON.parse(str); + }; + + const createIncident = async ({ + summary, + description, + }: CreateIncidentParams): Promise => { + const data = replaceSumDesc(summary, description); + console.log('its happening!!', data); + try { + const res = await request({ + axios: axiosInstance, + url: `${incidentUrl}`, + logger, + method: 'post', + data, + configurationUtilities, + }); + console.log('it happened!!!', res); + + throwIfResponseIsNotValid({ + res, + requiredAttributesToBeInTheResponse: ['id'], + }); + + const updatedIncident = await getIncident(res.data.id); + + console.log('updatedIncident!!!', updatedIncident); + return { + title: updatedIncident.key, + id: updatedIncident.id, + pushedDate: new Date(updatedIncident.created).toISOString(), + url: getIncidentViewURL(updatedIncident.key), + }; + } catch (error) { + console.log('ERROR', error.response); + throw new Error( + getErrorMessage( + i18n.NAME, + `Unable to create incident. Error: ${error.message}. Reason: ${createErrorMessage( + error.response?.data + )}` + ) + ); + } + }; + + // const updateIncident = async ({ + // incidentId, + // incident, + // }: UpdateIncidentParams): Promise => { + // const incidentWithoutNullValues = Object.entries(incident).reduce( + // (obj, [key, value]) => (value != null ? { ...obj, [key]: value } : obj), + // {} as Incident + // ); + // + // const fields = createFields(projectKey, incidentWithoutNullValues); + // + // try { + // const res = await request({ + // axios: axiosInstance, + // method: 'put', + // url: `${incidentUrl}/${incidentId}`, + // logger, + // data: { fields }, + // configurationUtilities, + // }); + // + // throwIfResponseIsNotValid({ + // res, + // }); + // + // const updatedIncident = await getIncident(incidentId as string); + // + // return { + // title: updatedIncident.key, + // id: updatedIncident.id, + // pushedDate: new Date(updatedIncident.updated).toISOString(), + // url: getIncidentViewURL(updatedIncident.key), + // }; + // } catch (error) { + // throw new Error( + // getErrorMessage( + // i18n.NAME, + // `Unable to update incident with id ${incidentId}. Error: ${ + // error.message + // }. Reason: ${createErrorMessage(error.response?.data)}` + // ) + // ); + // } + // }; + // + // const createComment = async ({ + // incidentId, + // comment, + // }: CreateCommentParams): Promise => { + // try { + // const res = await request({ + // axios: axiosInstance, + // method: 'post', + // url: getCommentsURL(incidentId), + // logger, + // data: { body: comment.comment }, + // configurationUtilities, + // }); + // + // throwIfResponseIsNotValid({ + // res, + // requiredAttributesToBeInTheResponse: ['id', 'created'], + // }); + // + // 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 + // }. Reason: ${createErrorMessage(error.response?.data)}` + // ) + // ); + // } + // }; + + // const getCapabilities = async () => { + // try { + // const res = await request({ + // axios: axiosInstance, + // method: 'get', + // url: capabilitiesUrl, + // logger, + // configurationUtilities, + // }); + // + // throwIfResponseIsNotValid({ + // res, + // requiredAttributesToBeInTheResponse: ['capabilities'], + // }); + // + // return { ...res.data }; + // } catch (error) { + // throw new Error( + // getErrorMessage( + // i18n.NAME, + // `Unable to get capabilities. Error: ${error.message}. Reason: ${createErrorMessage( + // error.response?.data + // )}` + // ) + // ); + // } + // }; + // + // const getIssueTypes = async () => { + // const capabilitiesResponse = await getCapabilities(); + // const supportsNewAPI = hasSupportForNewAPI(capabilitiesResponse); + // try { + // if (!supportsNewAPI) { + // const res = await request({ + // axios: axiosInstance, + // method: 'get', + // url: getIssueTypesOldAPIURL, + // logger, + // configurationUtilities, + // }); + // + // throwIfResponseIsNotValid({ + // res, + // }); + // + // const issueTypes = res.data.projects[0]?.issuetypes ?? []; + // return normalizeIssueTypes(issueTypes); + // } else { + // const res = await request({ + // axios: axiosInstance, + // method: 'get', + // url: getIssueTypesUrl, + // logger, + // configurationUtilities, + // }); + // + // throwIfResponseIsNotValid({ + // res, + // }); + // + // const issueTypes = res.data.values; + // return normalizeIssueTypes(issueTypes); + // } + // } catch (error) { + // throw new Error( + // getErrorMessage( + // i18n.NAME, + // `Unable to get issue types. Error: ${error.message}. Reason: ${createErrorMessage( + // error.response?.data + // )}` + // ) + // ); + // } + // }; + // + // const getFieldsByIssueType = async (issueTypeId: string) => { + // const capabilitiesResponse = await getCapabilities(); + // const supportsNewAPI = hasSupportForNewAPI(capabilitiesResponse); + // try { + // if (!supportsNewAPI) { + // const res = await request({ + // axios: axiosInstance, + // method: 'get', + // url: createGetIssueTypeFieldsUrl(getIssueTypeFieldsOldAPIURL, issueTypeId), + // logger, + // configurationUtilities, + // }); + // + // throwIfResponseIsNotValid({ + // res, + // }); + // + // const fields = res.data.projects[0]?.issuetypes[0]?.fields || {}; + // return normalizeFields(fields); + // } else { + // const res = await request({ + // axios: axiosInstance, + // method: 'get', + // url: createGetIssueTypeFieldsUrl(getIssueTypeFieldsUrl, issueTypeId), + // logger, + // configurationUtilities, + // }); + // + // throwIfResponseIsNotValid({ + // res, + // }); + // + // const fields = res.data.values.reduce( + // (acc: { [x: string]: {} }, value: { fieldId: string }) => ({ + // ...acc, + // [value.fieldId]: { ...value }, + // }), + // {} + // ); + // return normalizeFields(fields); + // } + // } catch (error) { + // throw new Error( + // getErrorMessage( + // i18n.NAME, + // `Unable to get fields. Error: ${error.message}. Reason: ${createErrorMessage( + // error.response?.data + // )}` + // ) + // ); + // } + // }; + // + // const getFields = async () => { + // try { + // const issueTypes = await getIssueTypes(); + // const fieldsPerIssueType = await Promise.all( + // issueTypes.map((issueType) => getFieldsByIssueType(issueType.id)) + // ); + // return fieldsPerIssueType.reduce((acc: GetCommonFieldsResponse, fieldTypesByIssue) => { + // const currentListOfFields = Object.keys(acc); + // return currentListOfFields.length === 0 + // ? fieldTypesByIssue + // : currentListOfFields.reduce( + // (add: GetCommonFieldsResponse, field) => + // Object.keys(fieldTypesByIssue).includes(field) + // ? { ...add, [field]: acc[field] } + // : add, + // {} + // ); + // }, {}); + // } catch (error) { + // // errors that happen here would be thrown in the contained async calls + // throw error; + // } + // }; + // + // const getIssues = async (title: string) => { + // const query = `${searchUrl}?jql=${encodeURIComponent( + // `project="${projectKey}" and summary ~"${title}"` + // )}`; + // + // try { + // const res = await request({ + // axios: axiosInstance, + // method: 'get', + // url: query, + // logger, + // configurationUtilities, + // }); + // + // throwIfResponseIsNotValid({ + // res, + // }); + // + // return normalizeSearchResults(res.data?.issues ?? []); + // } catch (error) { + // throw new Error( + // getErrorMessage( + // i18n.NAME, + // `Unable to get issues. Error: ${error.message}. Reason: ${createErrorMessage( + // error.response?.data + // )}` + // ) + // ); + // } + // }; + // + // const getIssue = async (id: string) => { + // const getIssueUrl = `${incidentUrl}/${id}`; + // try { + // const res = await request({ + // axios: axiosInstance, + // method: 'get', + // url: getIssueUrl, + // logger, + // configurationUtilities, + // }); + // + // throwIfResponseIsNotValid({ + // res, + // }); + // + // return normalizeIssue(res.data ?? {}); + // } catch (error) { + // throw new Error( + // getErrorMessage( + // i18n.NAME, + // `Unable to get issue with id ${id}. Error: ${error.message}. Reason: ${createErrorMessage( + // error.response?.data + // )}` + // ) + // ); + // } + // }; + + return { + // getFields, + // getIncident, + createIncident, + // updateIncident, + // createComment, + // getCapabilities, + // getIssueTypes, + // getFieldsByIssueType, + // getIssues, + // getIssue, + }; +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/translations.ts new file mode 100644 index 00000000000000..52bab5c0faa127 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/translations.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + + import { i18n } from '@kbn/i18n'; + +export const NAME = i18n.translate('xpack.actions.builtin.cases.casesWebhookTitle', { + defaultMessage: 'Cases Webhook', +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts new file mode 100644 index 00000000000000..9cddc866928056 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TypeOf } from '@kbn/config-schema'; +import { ActionsConfigurationUtilities } from '../../actions_config'; +import { + ExternalIncidentServiceConfigurationSchema, + ExternalIncidentServiceSecretConfigurationSchema, +} from './schema'; + +export interface ExternalServiceCredentials { + config: Record; + secrets: Record; +} + +export interface ExternalServiceValidation { + config: (configurationUtilities: ActionsConfigurationUtilities, configObject: any) => void; + secrets: (configurationUtilities: ActionsConfigurationUtilities, secrets: any) => void; +} + +export interface CreateIncidentParams { + summary: string; + description: string; +} + +export interface ExternalServiceIncidentResponse { + id: string; + title: string; + url: string; + pushedDate: string; +} + +export interface ExternalService { + createIncident: (params: CreateIncidentParams) => Promise; +} +export interface ResponseError { + errorMessages: string[] | null | undefined; + errors: { [k: string]: string } | null | undefined; +} + +export type CasesWebhookPublicConfigurationType = TypeOf< + typeof ExternalIncidentServiceConfigurationSchema +>; +export type CasesWebhookSecretConfigurationType = TypeOf< + typeof ExternalIncidentServiceSecretConfigurationSchema +>; 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 af94c79e463097..07131deae50a71 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 @@ -126,7 +126,11 @@ async function executor( } if (subAction === 'pushToService') { const pushToServiceParams = subActionParams as ExecutorSubActionPushParams; - + console.log('api push to service', { + externalService, + params: pushToServiceParams, + logger, + }); data = await api.pushToService({ externalService, params: pushToServiceParams, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts index 9ed750bb3ebd00..5a1c66e73ba674 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts @@ -13,6 +13,12 @@ export const URL_REQUIRED = i18n.translate( defaultMessage: 'URL is required.', } ); +export const INCIDENT_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.error.requiredIncidentText', + { + defaultMessage: 'Incident object is required.', + } +); export const URL_INVALID = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.error.invalidUrlTextField', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx index 0d14b91d08516a..5d52fefa3cf9f9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx @@ -50,6 +50,7 @@ export function getActionType(): ActionTypeModel< const configErrors = { url: new Array(), method: new Array(), + incident: new Array(), }; const secretsErrors = { user: new Array(), @@ -65,6 +66,9 @@ export function getActionType(): ActionTypeModel< if (action.config.url && !isValidUrl(action.config.url)) { configErrors.url = [...configErrors.url, translations.URL_INVALID]; } + if (!action.config.incident) { + configErrors.incident.push(translations.INCIDENT_REQUIRED); + } if (!action.config.method) { configErrors.method.push(translations.METHOD_REQUIRED); } @@ -87,12 +91,16 @@ export function getActionType(): ActionTypeModel< ): Promise> => { const translations = await import('./translations'); const errors = { - body: new Array(), + summary: new Array(), + description: new Array(), }; const validationResult = { errors }; validationResult.errors = errors; - if (!actionParams.body?.length) { - errors.body.push(translations.BODY_REQUIRED); + if (!actionParams.summary?.length) { + errors.summary.push(translations.BODY_REQUIRED); + } + if (!actionParams.description?.length) { + errors.description.push(translations.BODY_REQUIRED); } return validationResult; }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx index 2579d8dd1a93b4..962a8090102ba9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test-jest-helpers'; -import { WebhookActionConnector } from '../types'; +import { CasesWebhookActionConnector, WebhookActionConnector } from '../types'; import WebhookActionConnectorFields from './webhook_connectors'; describe('WebhookActionConnectorFields renders', () => { @@ -18,16 +18,19 @@ describe('WebhookActionConnectorFields renders', () => { password: 'pass', }, id: 'test', - actionTypeId: '.webhook', + actionTypeId: '.webhook-cases', isPreconfigured: false, - name: 'webhook', + isDeprecated: false, + name: 'cases webhook', config: { method: 'PUT', url: 'http:\\test', + incident: + '{"fields":{"summary":"$SUM","description":"$DESC","project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', headers: { 'content-type': 'text' }, hasAuth: true, }, - } as WebhookActionConnector; + } as CasesWebhookActionConnector; const wrapper = mountWithIntl( { test('should display a message on create to remember credentials', () => { const actionConnector = { secrets: {}, - actionTypeId: '.webhook', + actionTypeId: '.webhook-cases', isPreconfigured: false, + isDeprecated: false, config: { hasAuth: true, }, @@ -60,7 +64,7 @@ describe('WebhookActionConnectorFields renders', () => { const wrapper = mountWithIntl( {}} editActionSecrets={() => {}} readOnly={false} @@ -81,10 +85,13 @@ describe('WebhookActionConnectorFields renders', () => { id: 'test', actionTypeId: '.webhook', isPreconfigured: false, + isDeprecated: false, name: 'webhook', config: { method: 'PUT', url: 'http:\\test', + incident: + '{"fields":{"summary":"$SUM","description":"$DESC","project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', headers: { 'content-type': 'text' }, hasAuth: true, }, @@ -114,10 +121,13 @@ describe('WebhookActionConnectorFields renders', () => { actionTypeId: '.webhook', isPreconfigured: false, isMissingSecrets: true, + isDeprecated: false, name: 'webhook', config: { method: 'PUT', url: 'http:\\test', + incident: + '{"fields":{"summary":"$SUM","description":"$DESC","project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', headers: { 'content-type': 'text' }, hasAuth: true, }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx index f86e50509de611..2df13235f28dcd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx @@ -28,6 +28,7 @@ import { i18n } from '@kbn/i18n'; import { ActionConnectorFieldsProps } from '../../../../types'; import { CasesWebhookActionConnector } from '../types'; import { getEncryptedFieldNotifyLabel } from '../../get_encrypted_field_notify_label'; +import { JsonEditorWithMessageVariables } from '../../json_editor_with_message_variables'; const HTTP_VERBS = ['post', 'put']; @@ -35,7 +36,8 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< ActionConnectorFieldsProps > = ({ action, editActionConfig, editActionSecrets, errors, readOnly }) => { const { user, password } = action.secrets; - const { method, url, headers, hasAuth } = action.config; + const { method, url, incident, headers, hasAuth } = action.config; + console.log('incident here', incident); const [httpHeaderKey, setHttpHeaderKey] = useState(''); const [httpHeaderValue, setHttpHeaderValue] = useState(''); @@ -230,6 +232,9 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< const isUserInvalid: boolean = user !== undefined && errors.user !== undefined && errors.user.length > 0; + const isIncidentInvalid: boolean = + errors.url !== undefined && errors.url.length > 0 && url !== undefined; + return ( <> @@ -238,7 +243,7 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< label={i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.methodTextFieldLabel', { - defaultMessage: 'Method', + defaultMessage: 'Create Incident Method', } )} > @@ -263,7 +268,7 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< label={i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.urlTextFieldLabel', { - defaultMessage: 'URL', + defaultMessage: 'Incident URL', } )} > @@ -286,6 +291,55 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< + + + + { + editActionConfig('incident', json); + }} + onBlur={() => { + if (!incident) { + editActionConfig('incident', ''); + } + }} + /> + + + diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_params.tsx index d7f296fc808d20..d07cde382a78dc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_params.tsx @@ -5,47 +5,83 @@ * 2.0. */ -import React from 'react'; +import React, { useCallback, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; +import { EuiFormRow } from '@elastic/eui'; import { ActionParamsProps } from '../../../../types'; -import { WebhookActionParams } from '../types'; -import { JsonEditorWithMessageVariables } from '../../json_editor_with_message_variables'; +import { CasesWebhookActionParams } from '../types'; +import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables'; +import { TextFieldWithMessageVariables } from '../../text_field_with_message_variables'; -const WebhookParamsFields: React.FunctionComponent> = ({ +const WebhookParamsFields: React.FunctionComponent> = ({ actionParams, editAction, index, messageVariables, errors, }) => { - const { body } = actionParams; + const { summary, description } = actionParams; + const incident = useMemo( + () => ({ + summary, + description, + }), + [summary, description] + ); + const editSubActionProperty = useCallback( + (key: string, value: any) => { + if (key === 'summary') { + editAction('summary', value, index); + } + if (key === 'description') { + editAction('description', value, index); + } + // console.log('edit action', { ...incident, [key]: value }, index); + // return editAction('subActionParams', { ...incident, [key]: value }, index); + }, + [editAction, index] + ); return ( - { - editAction('body', json, index); - }} - onBlur={() => { - if (!body) { - editAction('body', '', index); + <> + 0 && + incident.summary !== undefined } - }} - /> + label={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhook.summaryFieldLabel', + { + defaultMessage: 'Summary (required)', + } + )} + > + + + + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts index 0aaa384d1a0b35..7ec7933f04dbac 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts @@ -73,7 +73,8 @@ export interface WebhookActionParams { } export interface CasesWebhookActionParams { - body?: string; + summary: string; + description: string; } export interface EmailConfig { @@ -139,6 +140,7 @@ export type WebhookActionConnector = UserConfiguredActionConnector; hasAuth: boolean; } From ff959a46ef4583759767ec2ad4555119b0e67faa Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Fri, 6 May 2022 12:57:40 -0600 Subject: [PATCH 04/97] wip --- .../cases_webhook/index.ts | 102 +++++++++--------- .../cases_webhook/service.ts | 82 +++++++++----- .../cases_webhook/types.ts | 3 +- .../builtin_action_types/jira/service.ts | 11 +- .../servicenow/service.ts | 9 +- 5 files changed, 128 insertions(+), 79 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts index 43fa89888c6c7a..56aa02e2ad35c7 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts @@ -161,6 +161,7 @@ export async function executor( const { summary, description } = execOptions.params; const externalService = createExternalService( + actionId, { config: execOptions.config, secrets: execOptions.secrets, @@ -175,57 +176,58 @@ export async function executor( description, }); console.log('result', result); + return result; - if (isOk(result)) { - const { - value: { status, statusText }, - } = result; - logger.debug( - `response from cases webhook action "${actionId}": [HTTP ${status}] ${statusText}` - ); - - return successResult(actionId, data); - } else { - const { error } = result; - if (error.response) { - const { - status, - statusText, - headers: responseHeaders, - data: { message: responseMessage }, - } = error.response; - const responseMessageAsSuffix = responseMessage ? `: ${responseMessage}` : ''; - const message = `[${status}] ${statusText}${responseMessageAsSuffix}`; - logger.error(`error on ${actionId} cases webhook event: ${message}`); - // The request was made and the server responded with a status code - // that falls out of the range of 2xx - // special handling for 5xx - if (status >= 500) { - return retryResult(actionId, message); - } - - // special handling for rate limiting - if (status === 429) { - return pipe( - getRetryAfterIntervalFromHeaders(responseHeaders), - map((retry) => retryResultSeconds(actionId, message, retry)), - getOrElse(() => retryResult(actionId, message)) - ); - } - return errorResultInvalid(actionId, message); - } else if (error.code) { - const message = `[${error.code}] ${error.message}`; - logger.error(`error on ${actionId} cases webhook event: ${message}`); - return errorResultRequestFailed(actionId, message); - } else if (error.isAxiosError) { - const message = `${error.message}`; - logger.error(`error on ${actionId} cases webhook event: ${message}`); - return errorResultRequestFailed(actionId, message); - } - - logger.error(`error on ${actionId} cases webhook action: unexpected error`); - return errorResultUnexpectedError(actionId); - } + // if (isOk(result)) { + // // const { + // // value: { status, statusText }, + // // } = result; + // // logger.debug( + // // `response from cases webhook action "${actionId}": [HTTP ${status}] ${statusText}` + // // ); + // // + // // return successResult(actionId, data); + // } else { + // const { error } = result; + // if (error.response) { + // const { + // status, + // statusText, + // headers: responseHeaders, + // data: { message: responseMessage }, + // } = error.response; + // const responseMessageAsSuffix = responseMessage ? `: ${responseMessage}` : ''; + // const message = `[${status}] ${statusText}${responseMessageAsSuffix}`; + // logger.error(`error on ${actionId} cases webhook event: ${message}`); + // // The request was made and the server responded with a status code + // // that falls out of the range of 2xx + // // special handling for 5xx + // if (status >= 500) { + // return retryResult(actionId, message); + // } + // + // // special handling for rate limiting + // if (status === 429) { + // return pipe( + // getRetryAfterIntervalFromHeaders(responseHeaders), + // map((retry) => retryResultSeconds(actionId, message, retry)), + // getOrElse(() => retryResult(actionId, message)) + // ); + // } + // return errorResultInvalid(actionId, message); + // } else if (error.code) { + // const message = `[${error.code}] ${error.message}`; + // logger.error(`error on ${actionId} cases webhook event: ${message}`); + // return errorResultRequestFailed(actionId, message); + // } else if (error.isAxiosError) { + // const message = `${error.message}`; + // logger.error(`error on ${actionId} cases webhook event: ${message}`); + // return errorResultRequestFailed(actionId, message); + // } + // + // logger.error(`error on ${actionId} cases webhook action: unexpected error`); + // return errorResultUnexpectedError(actionId); + // } } // Action Executor Result w/ internationalisation diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts index 87c94a42d00cdf..4ea00405ecf0d2 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts @@ -5,9 +5,13 @@ * 2.0. */ -import axios from 'axios'; +import axios, { AxiosError, AxiosResponse } from 'axios'; import { Logger } from '@kbn/core/server'; +import { pipe } from 'fp-ts/pipeable'; +import { getOrElse, map } from 'fp-ts/Option'; +import { ActionTypeExecutorResult } from '../../../common'; +import { isOk, promiseResult, Result } from '../lib/result_type'; import { CreateIncidentParams, ExternalServiceCredentials, @@ -21,6 +25,7 @@ import { import * as i18n from './translations'; import { request, getErrorMessage, throwIfResponseIsNotValid } from '../lib/axios_utils'; import { ActionsConfigurationUtilities } from '../../actions_config'; +import { getRetryAfterIntervalFromHeaders } from '../lib/http_rersponse_retry_header'; const VERSION = '2'; const BASE_URL = `rest/api/${VERSION}`; @@ -28,6 +33,7 @@ const BASE_URL = `rest/api/${VERSION}`; const VIEW_INCIDENT_URL = `browse`; export const createExternalService = ( + actionId: string, { config, secrets }: ExternalServiceCredentials, logger: Logger, configurationUtilities: ActionsConfigurationUtilities @@ -151,38 +157,62 @@ export const createExternalService = ( str = str.replace('$DESC', desc); return JSON.parse(str); }; - + // Action Executor Result w/ internationalisation + function successResult(actionId: string, data: unknown): ActionTypeExecutorResult { + return { status: 'ok', data, actionId }; + } const createIncident = async ({ summary, description, - }: CreateIncidentParams): Promise => { + }: CreateIncidentParams): Promise => { const data = replaceSumDesc(summary, description); - console.log('its happening!!', data); + console.log('cases webhook args!!', { + axios: axiosInstance, + url: `${incidentUrl}`, + logger, + method: 'post', + data, + configurationUtilities, + }); try { - const res = await request({ - axios: axiosInstance, - url: `${incidentUrl}`, - logger, - method: 'post', - data, - configurationUtilities, - }); - console.log('it happened!!!', res); - - throwIfResponseIsNotValid({ - res, - requiredAttributesToBeInTheResponse: ['id'], - }); + const result: Result = await promiseResult( + request({ + axios: axiosInstance, + url: `${incidentUrl}`, + logger, + method: 'post', + data, + configurationUtilities, + }) + ); + console.log('it happened!!!', result); - const updatedIncident = await getIncident(res.data.id); + if (isOk(result)) { + const { + value: { status, statusText, data: data2 }, + } = result; + console.log('DATA', data2); + logger.debug(`response from webhook action "${actionId}": [HTTP ${status}] ${statusText}`); - console.log('updatedIncident!!!', updatedIncident); - return { - title: updatedIncident.key, - id: updatedIncident.id, - pushedDate: new Date(updatedIncident.created).toISOString(), - url: getIncidentViewURL(updatedIncident.key), - }; + return successResult(actionId, data); + } else { + const { error } = result; + if (error.response) { + const { + status, + statusText, + headers: responseHeaders, + data: { message: responseMessage }, + } = error.response; + const responseMessageAsSuffix = responseMessage ? `: ${responseMessage}` : ''; + const message = `[${status}] ${statusText}${responseMessageAsSuffix}`; + logger.error(`error on ${actionId} webhook event: ${message}`); + // The request was made and the server responded with a status code + // that falls out of the range of 2xx + // special handling for 5xx + return { actionId, message }; + } + } } catch (error) { console.log('ERROR', error.response); throw new Error( diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts index 9cddc866928056..ad49a089ab7c1c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts @@ -6,6 +6,7 @@ */ import { TypeOf } from '@kbn/config-schema'; +import { ActionTypeExecutorResult } from '../../../common'; import { ActionsConfigurationUtilities } from '../../actions_config'; import { ExternalIncidentServiceConfigurationSchema, @@ -35,7 +36,7 @@ export interface ExternalServiceIncidentResponse { } export interface ExternalService { - createIncident: (params: CreateIncidentParams) => Promise; + createIncident: (params: CreateIncidentParams) => Promise; } export interface ResponseError { errorMessages: string[] | null | undefined; 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 5f5147c67e1049..5ab8eb0e0952e5 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 @@ -222,7 +222,16 @@ export const createExternalService = ( ...incident, issueType, }); - + console.log('jira args', { + axios: axiosInstance, + url: `${incidentUrl}`, + logger, + method: 'post', + data: { + fields, + }, + configurationUtilities, + }); try { const res = await request({ axios: axiosInstance, 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 5a1b1f604cb818..0d40fc61595cfb 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 @@ -181,7 +181,14 @@ export const createExternalService: ServiceFactory = ({ const createIncident = async ({ incident }: ExternalServiceParamsCreate) => { try { await checkIfApplicationIsInstalled(); - + console.log('SN args', { + axios: axiosInstance, + url: getCreateIncidentUrl(), + logger, + method: 'post', + data: prepareIncident(useTableApi, incident), + configurationUtilities, + }); const res = await request({ axios: axiosInstance, url: getCreateIncidentUrl(), From 85e3d973379e682b69a3bef8ec789cf080de0b4f Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 6 May 2022 19:55:49 +0000 Subject: [PATCH 05/97] [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' --- .../server/builtin_action_types/cases_webhook/translations.ts | 2 +- .../builtin_action_types/cases_webhook/webhook.test.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/translations.ts index 52bab5c0faa127..85d7bc545ee1bf 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/translations.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/translations.ts @@ -5,7 +5,7 @@ * 2.0. */ - import { i18n } from '@kbn/i18n'; +import { i18n } from '@kbn/i18n'; export const NAME = i18n.translate('xpack.actions.builtin.cases.casesWebhookTitle', { defaultMessage: 'Cases Webhook', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.test.tsx index 3e42e7965c5bde..8a17b1c0b206a6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.test.tsx @@ -6,7 +6,7 @@ */ import { TypeRegistry } from '../../../type_registry'; -import { registerBuiltInActionTypes } from '.././index'; +import { registerBuiltInActionTypes } from '..'; import { ActionTypeModel } from '../../../../types'; import { WebhookActionConnector } from '../types'; From 9b6bf56a0f9e42992d8b0fbd5426088a6f0a4daa Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 16 May 2022 11:55:18 -0600 Subject: [PATCH 06/97] make work in cases --- .../cases_webhook/index.ts | 34 +++++------ .../cases_webhook/schema.ts | 13 ++++ .../cases_webhook/types.ts | 8 ++- .../common/api/connectors/cases_webhook.ts | 4 ++ .../public/components/all_cases/columns.tsx | 23 +++++--- .../connectors/cases_webhook/case_fields.tsx | 48 +++++++++++++++ .../connectors/cases_webhook/index.ts | 25 ++++++++ .../connectors/cases_webhook/translations.ts | 42 +++++++++++++ .../connectors/cases_webhook/validator.ts | 39 ++++++++++++ .../public/components/connectors/index.ts | 3 + .../public/components/user_actions/pushed.tsx | 16 +++-- .../plugins/cases/server/client/cases/push.ts | 59 +++++++++++-------- .../cases/server/client/configure/client.ts | 2 +- .../client/configure/create_mappings.ts | 1 - .../cases/server/connectors/factory.ts | 3 +- .../cases/server/services/cases/transform.ts | 7 +++ .../server/services/user_actions/index.ts | 7 +++ 17 files changed, 274 insertions(+), 60 deletions(-) create mode 100644 x-pack/plugins/cases/public/components/connectors/cases_webhook/case_fields.tsx create mode 100644 x-pack/plugins/cases/public/components/connectors/cases_webhook/index.ts create mode 100644 x-pack/plugins/cases/public/components/connectors/cases_webhook/translations.ts create mode 100644 x-pack/plugins/cases/public/components/connectors/cases_webhook/validator.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts index 56aa02e2ad35c7..e1345627d502c8 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts @@ -12,6 +12,12 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { pipe } from 'fp-ts/lib/pipeable'; import { map, getOrElse } from 'fp-ts/lib/Option'; import { Logger } from '@kbn/core/server'; +import { + CasesWebhookExecutorResultData, + CasesWebhookPublicConfigurationType, + CasesWebhookSecretConfigurationType, + ExecutorParams, +} from './types'; import { getRetryAfterIntervalFromHeaders } from '../lib/http_rersponse_retry_header'; import { nullableType } from '../lib/nullable'; import { isOk, promiseResult, Result } from '../lib/result_type'; @@ -20,6 +26,7 @@ import { ActionsConfigurationUtilities } from '../../actions_config'; import { request } from '../lib/axios_utils'; import { renderMustacheString } from '../../lib/mustache_renderer'; import { createExternalService } from './service'; +import { ExecutorParamsSchema } from './schema'; // config definition export enum CasesWebhookMethods { @@ -58,8 +65,8 @@ export type ActionTypeConfigType = TypeOf; // secrets definition export type ActionTypeSecretsType = TypeOf; const secretSchemaProps = { - user: schema.nullable(schema.string()), - password: schema.nullable(schema.string()), + user: schema.string(), + password: schema.string(), }; const SecretsSchema = schema.object(secretSchemaProps, { validate: (secrets) => { @@ -87,7 +94,12 @@ export function getActionType({ }: { logger: Logger; configurationUtilities: ActionsConfigurationUtilities; -}): CasesWebhookActionType { +}): ActionType< + CasesWebhookPublicConfigurationType, + CasesWebhookSecretConfigurationType, + ExecutorParams, + CasesWebhookExecutorResultData +> { return { id: ActionTypeId, minimumLicenseRequired: 'gold', @@ -99,25 +111,12 @@ export function getActionType({ validate: curry(validateActionTypeConfig)(configurationUtilities), }), secrets: SecretsSchema, - params: ParamsSchema, + params: ExecutorParamsSchema, }, - renderParameterTemplates, executor: curry(executor)({ logger, configurationUtilities }), }; } -function renderParameterTemplates( - params: ActionParamsType, - variables: Record -): ActionParamsType { - if (!params.summary) return params; - if (!params.description) return params; - return { - summary: renderMustacheString(params.summary, variables, 'json'), - description: renderMustacheString(params.description, variables, 'json'), - }; -} - function validateActionTypeConfig( configurationUtilities: ActionsConfigurationUtilities, configObject: ActionTypeConfigType @@ -160,6 +159,7 @@ export async function executor( const actionId = execOptions.actionId; const { summary, description } = execOptions.params; + console.log('call createExternalService'); const externalService = createExternalService( actionId, { diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts index e6dcb942569663..36118c31ea888e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts @@ -6,6 +6,12 @@ */ import { schema } from '@kbn/config-schema'; +import { + ExecutorSubActionCommonFieldsParamsSchema, + ExecutorSubActionGetIncidentParamsSchema, + ExecutorSubActionHandshakeParamsSchema, + ExecutorSubActionPushParamsSchema, +} from '../jira/schema'; export const ExternalIncidentServiceConfiguration = { url: schema.string(), @@ -24,3 +30,10 @@ export const ExternalIncidentServiceSecretConfiguration = { export const ExternalIncidentServiceSecretConfigurationSchema = schema.object( ExternalIncidentServiceSecretConfiguration ); + +export const ExecutorParamsSchema = schema.oneOf([ + schema.object({ + subAction: schema.literal('pushToService'), + subActionParams: ExecutorSubActionPushParamsSchema, + }), +]); diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts index ad49a089ab7c1c..62277cd219c591 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts @@ -6,6 +6,8 @@ */ import { TypeOf } from '@kbn/config-schema'; +import { PushToServiceResponse } from '../jira/types'; +import { ExecutorParamsSchema } from '../jira/schema'; import { ActionTypeExecutorResult } from '../../../common'; import { ActionsConfigurationUtilities } from '../../actions_config'; import { @@ -35,9 +37,13 @@ export interface ExternalServiceIncidentResponse { pushedDate: string; } +export type ExecutorParams = TypeOf; + export interface ExternalService { - createIncident: (params: CreateIncidentParams) => Promise; + createIncident: (params: CreateIncidentParams) => Promise; } +export type CasesWebhookExecutorResultData = ExternalServiceIncidentResponse; + export interface ResponseError { errorMessages: string[] | null | undefined; errors: { [k: string]: string } | null | undefined; diff --git a/x-pack/plugins/cases/common/api/connectors/cases_webhook.ts b/x-pack/plugins/cases/common/api/connectors/cases_webhook.ts index e83c9b8b9b9cbf..5a597adedb0d5e 100644 --- a/x-pack/plugins/cases/common/api/connectors/cases_webhook.ts +++ b/x-pack/plugins/cases/common/api/connectors/cases_webhook.ts @@ -14,3 +14,7 @@ export const CasesWebhookFieldsRT = rt.record( ); export type CasesWebhookFieldsType = rt.TypeOf; + +export enum CasesWebhookConnectorType { + Cases = 'cases', +} diff --git a/x-pack/plugins/cases/public/components/all_cases/columns.tsx b/x-pack/plugins/cases/public/components/all_cases/columns.tsx index c895dfdc11f3f8..fb5315ee658af5 100644 --- a/x-pack/plugins/cases/public/components/all_cases/columns.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/columns.tsx @@ -440,15 +440,20 @@ export const ExternalServiceColumn: React.FC = ({ theCase, connectors }) /> )} - - {theCase.externalService?.externalTitle} - + {theCase.externalService?.externalUrl !== 'replace_cases_webhook' ? ( + + {theCase.externalService?.externalTitle} + + ) : ( + `${theCase.externalService?.connectorName} incident` + )} + {hasDataToPush ? renderStringField(i18n.REQUIRES_UPDATE, `case-table-column-external-requiresUpdate`) : renderStringField(i18n.UP_TO_DATE, `case-table-column-external-upToDate`)} diff --git a/x-pack/plugins/cases/public/components/connectors/cases_webhook/case_fields.tsx b/x-pack/plugins/cases/public/components/connectors/cases_webhook/case_fields.tsx new file mode 100644 index 00000000000000..b9eefdd8869447 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/cases_webhook/case_fields.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import { EuiCallOut } from '@elastic/eui'; +import * as i18n from './translations'; + +import { ConnectorTypes, CasesWebhookFieldsType } from '../../../../common/api'; +import { ConnectorFieldsProps } from '../types'; +import { ConnectorCard } from '../card'; +import { connectorValidator } from './validator'; + +const CasesWebhookComponent: React.FunctionComponent< + ConnectorFieldsProps +> = ({ connector, isEdit = true }) => { + const showMappingWarning = useMemo(() => connectorValidator(connector) != null, [connector]); + + return ( + <> + {!isEdit && ( + + )} + {showMappingWarning && ( + + {i18n.EMPTY_MAPPING_WARNING_DESC} + + )} + + ); +}; +CasesWebhookComponent.displayName = 'CasesWebhook'; + +// eslint-disable-next-line import/no-default-export +export { CasesWebhookComponent as default }; diff --git a/x-pack/plugins/cases/public/components/connectors/cases_webhook/index.ts b/x-pack/plugins/cases/public/components/connectors/cases_webhook/index.ts new file mode 100644 index 00000000000000..76ea641fc7e4a7 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/cases_webhook/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { lazy } from 'react'; + +import { CaseConnector } from '../types'; +import { ConnectorTypes, CasesWebhookFieldsType } from '../../../../common/api'; +import * as i18n from './translations'; + +export const getCaseConnector = (): CaseConnector => { + return { + id: ConnectorTypes.casesWebhook, + fieldsComponent: lazy(() => import('./case_fields')), + }; +}; + +export const fieldLabels = { + caseId: i18n.CASE_ID_LABEL, + caseName: i18n.CASE_NAME_LABEL, + severity: i18n.SEVERITY_LABEL, +}; diff --git a/x-pack/plugins/cases/public/components/connectors/cases_webhook/translations.ts b/x-pack/plugins/cases/public/components/connectors/cases_webhook/translations.ts new file mode 100644 index 00000000000000..7dba4c676cbc28 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/cases_webhook/translations.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const ALERT_SOURCE_LABEL = i18n.translate( + 'xpack.cases.connectors.casesWebhook.alertSourceLabel', + { + defaultMessage: 'Alert Source', + } +); + +export const CASE_ID_LABEL = i18n.translate('xpack.cases.connectors.casesWebhook.caseIdLabel', { + defaultMessage: 'Case Id', +}); + +export const CASE_NAME_LABEL = i18n.translate('xpack.cases.connectors.casesWebhook.caseNameLabel', { + defaultMessage: 'Case Name', +}); + +export const SEVERITY_LABEL = i18n.translate('xpack.cases.connectors.casesWebhook.severityLabel', { + defaultMessage: 'Severity', +}); + +export const EMPTY_MAPPING_WARNING_TITLE = i18n.translate( + 'xpack.cases.connectors.casesWebhook.emptyMappingWarningTitle', + { + defaultMessage: 'This connector has missing field mappings', + } +); + +export const EMPTY_MAPPING_WARNING_DESC = i18n.translate( + 'xpack.cases.connectors.casesWebhook.emptyMappingWarningDesc', + { + defaultMessage: + 'This connector cannot be selected because it is missing the required case field mappings. You can edit this connector to add required field mappings or select a connector of type Cases.', + } +); diff --git a/x-pack/plugins/cases/public/components/connectors/cases_webhook/validator.ts b/x-pack/plugins/cases/public/components/connectors/cases_webhook/validator.ts new file mode 100644 index 00000000000000..6424fb58adf2d9 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/cases_webhook/validator.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CasesWebhookConnectorType } from '../../../../common/api'; +import { ValidationConfig } from '../../../common/shared_imports'; +import { CaseActionConnector } from '../../types'; + +const casesRequiredFields = [ + 'caseIdConfig', + 'caseNameConfig', + 'descriptionConfig', + 'commentsConfig', +]; + +export const isAnyRequiredFieldNotSet = (mapping: Record | undefined) => + casesRequiredFields.some((field) => mapping?.[field] == null); + +/** + * The user can use either a connector of type cases or all. + * If the connector is of type all we should check if all + * required field have been configured. + */ + +export const connectorValidator = ( + connector: CaseActionConnector +): ReturnType => { + const { + config: { mappings, connectorType }, + } = connector; + // if (connectorType !== CasesWebhookConnectorType.Cases || isAnyRequiredFieldNotSet(mappings)) { + // return { + // message: 'Invalid connector', + // }; + // } +}; diff --git a/x-pack/plugins/cases/public/components/connectors/index.ts b/x-pack/plugins/cases/public/components/connectors/index.ts index 03d18976c40fd3..6f405bc45fc3b8 100644 --- a/x-pack/plugins/cases/public/components/connectors/index.ts +++ b/x-pack/plugins/cases/public/components/connectors/index.ts @@ -10,8 +10,10 @@ import { createCaseConnectorsRegistry } from './connectors_registry'; import { getCaseConnector as getJiraCaseConnector } from './jira'; import { getCaseConnector as getSwimlaneCaseConnector } from './swimlane'; import { getCaseConnector as getResilientCaseConnector } from './resilient'; +import { getCaseConnector as getCasesWebhookCaseConnector } from './cases_webhook'; import { getServiceNowITSMCaseConnector, getServiceNowSIRCaseConnector } from './servicenow'; import { + CasesWebhookFieldsType, JiraFieldsType, ServiceNowITSMFieldsType, ServiceNowSIRFieldsType, @@ -41,6 +43,7 @@ class CaseConnectors { ); this.caseConnectorsRegistry.register(getServiceNowSIRCaseConnector()); this.caseConnectorsRegistry.register(getSwimlaneCaseConnector()); + this.caseConnectorsRegistry.register(getCasesWebhookCaseConnector()); } registry(): CaseConnectorsRegistry { diff --git a/x-pack/plugins/cases/public/components/user_actions/pushed.tsx b/x-pack/plugins/cases/public/components/user_actions/pushed.tsx index e02bde992b6517..13522f2bbbcacc 100644 --- a/x-pack/plugins/cases/public/components/user_actions/pushed.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/pushed.tsx @@ -47,11 +47,17 @@ const getLabelTitle = (action: UserActionResponse, firstPush: externalService?.connectorName }`} - - - {externalService?.externalTitle} - - + {externalService?.externalUrl !== 'replace_cases_webhook' && ( + + + {externalService?.externalTitle} + + + )} ); }; diff --git a/x-pack/plugins/cases/server/client/cases/push.ts b/x-pack/plugins/cases/server/client/cases/push.ts index 42fe71f902a72d..300a88cd401859 100644 --- a/x-pack/plugins/cases/server/client/cases/push.ts +++ b/x-pack/plugins/cases/server/client/cases/push.ts @@ -20,6 +20,8 @@ import { OWNER_FIELD, CommentType, CommentRequestAlertType, + ConnectorTypes, + NONE_CONNECTOR_ID, } from '../../../common/api'; import { CASE_COMMENT_SAVED_OBJECT } from '../../../common/constants'; @@ -135,7 +137,6 @@ export const push = async ( const alertsInfo = getAlertInfoFromComments(theCase?.comments); const alerts = await getAlerts(alertsInfo, clientArgs); - const getMappingsResponse = await casesClientInternal.configuration.getMappings({ connector: theCase.connector, }); @@ -274,30 +275,38 @@ export const push = async ( /* End of update case with push information */ - return CaseResponseRt.encode( - flattenCaseSavedObject({ - savedObject: { - ...myCase, - ...updatedCase, - attributes: { ...myCase.attributes, ...updatedCase?.attributes }, - references: myCase.references, - }, - comments: comments.saved_objects.map((origComment) => { - const updatedComment = updatedComments.saved_objects.find((c) => c.id === origComment.id); - return { - ...origComment, - ...updatedComment, - attributes: { - ...origComment.attributes, - ...updatedComment?.attributes, - ...getCommentContextFromAttributes(origComment.attributes), - }, - version: updatedComment?.version ?? origComment.version, - references: origComment?.references ?? [], - }; - }), - }) - ); + const flattedCase = flattenCaseSavedObject({ + savedObject: { + ...myCase, + ...updatedCase, + attributes: { ...myCase.attributes, ...updatedCase?.attributes }, + references: myCase.references, + }, + comments: comments.saved_objects.map((origComment) => { + const updatedComment = updatedComments.saved_objects.find((c) => c.id === origComment.id); + return { + ...origComment, + ...updatedComment, + attributes: { + ...origComment.attributes, + ...updatedComment?.attributes, + ...getCommentContextFromAttributes(origComment.attributes), + }, + version: updatedComment?.version ?? origComment.version, + references: origComment?.references ?? [], + }; + }), + }); + if (flattedCase.connector.type === ConnectorTypes.casesWebhook) { + flattedCase.external_service = { + ...flattedCase.external_service, + external_id: 'replace_cases_webhook', + external_title: 'replace_cases_webhook', + external_url: 'replace_cases_webhook', + }; + } + + return CaseResponseRt.encode(flattedCase); } catch (error) { throw createCaseError({ message: `Failed to push case: ${error}`, error, logger }); } diff --git a/x-pack/plugins/cases/server/client/configure/client.ts b/x-pack/plugins/cases/server/client/configure/client.ts index baa68c23340cb7..9bb6b833162647 100644 --- a/x-pack/plugins/cases/server/client/configure/client.ts +++ b/x-pack/plugins/cases/server/client/configure/client.ts @@ -392,7 +392,7 @@ async function create( const creationDate = new Date().toISOString(); let mappings: ConnectorMappingsAttributes[] = []; - console.log('configuration', configuration); + try { mappings = await casesClientInternal.configuration.createMappings({ connector: configuration.connector, diff --git a/x-pack/plugins/cases/server/client/configure/create_mappings.ts b/x-pack/plugins/cases/server/client/configure/create_mappings.ts index 3c9aa1bda9324f..52f00453ef0eca 100644 --- a/x-pack/plugins/cases/server/client/configure/create_mappings.ts +++ b/x-pack/plugins/cases/server/client/configure/create_mappings.ts @@ -20,7 +20,6 @@ export const createMappings = async ( try { const mappings = casesConnectors.get(connector.type)?.getMapping() ?? []; - console.log('mappings', mappings); const theMapping = await connectorMappingsService.post({ unsecuredSavedObjectsClient, diff --git a/x-pack/plugins/cases/server/connectors/factory.ts b/x-pack/plugins/cases/server/connectors/factory.ts index a4506ef1293436..40035fb984863d 100644 --- a/x-pack/plugins/cases/server/connectors/factory.ts +++ b/x-pack/plugins/cases/server/connectors/factory.ts @@ -9,11 +9,12 @@ import { ConnectorTypes } from '../../common/api'; import { ICasesConnector, CasesConnectorsMap } from './types'; import { getCaseConnector as getJiraCaseConnector } from './jira'; import { getCaseConnector as getResilientCaseConnector } from './resilient'; +import { getCaseConnector as getCasesWebhookCaseConnector } from './cases_webook'; import { getServiceNowITSMCaseConnector, getServiceNowSIRCaseConnector } from './servicenow'; import { getCaseConnector as getSwimlaneCaseConnector } from './swimlane'; const mapping: Record = { - [ConnectorTypes.casesWebhook]: getJiraCaseConnector(), + [ConnectorTypes.casesWebhook]: getCasesWebhookCaseConnector(), [ConnectorTypes.jira]: getJiraCaseConnector(), [ConnectorTypes.serviceNowITSM]: getServiceNowITSMCaseConnector(), [ConnectorTypes.serviceNowSIR]: getServiceNowSIRCaseConnector(), diff --git a/x-pack/plugins/cases/server/services/cases/transform.ts b/x-pack/plugins/cases/server/services/cases/transform.ts index c013475bd2e9c6..d48dabd4426183 100644 --- a/x-pack/plugins/cases/server/services/cases/transform.ts +++ b/x-pack/plugins/cases/server/services/cases/transform.ts @@ -201,5 +201,12 @@ function transformESExternalService( return { ...externalService, connector_id: connectorIdRef?.id ?? NONE_CONNECTOR_ID, + ...(externalService.external_url == null + ? { + external_id: 'replace_cases_webhook', + external_title: 'replace_cases_webhook', + external_url: 'replace_cases_webhook', + } + : {}), }; } diff --git a/x-pack/plugins/cases/server/services/user_actions/index.ts b/x-pack/plugins/cases/server/services/user_actions/index.ts index 7739437f9e6078..c02fb7833379bd 100644 --- a/x-pack/plugins/cases/server/services/user_actions/index.ts +++ b/x-pack/plugins/cases/server/services/user_actions/index.ts @@ -586,6 +586,13 @@ const addReferenceIdToPayload = ( externalService: { ...userActionAttributes.payload.externalService, connector_id: connectorId ?? NONE_CONNECTOR_ID, + ...(userActionAttributes.payload.externalService.external_url == null + ? { + external_id: 'replace_cases_webhook', + external_title: 'replace_cases_webhook', + external_url: 'replace_cases_webhook', + } + : {}), }, }; } From 0cd8127a6511914076f14c724d94d90af43e7c2e Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 16 May 2022 18:46:19 +0000 Subject: [PATCH 07/97] [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' --- .../server/builtin_action_types/cases_webhook/index.ts | 9 +-------- .../server/builtin_action_types/cases_webhook/schema.ts | 7 +------ .../server/builtin_action_types/cases_webhook/service.ts | 4 ---- .../server/builtin_action_types/cases_webhook/types.ts | 2 -- .../components/connectors/cases_webhook/validator.ts | 1 - x-pack/plugins/cases/server/client/cases/push.ts | 1 - 6 files changed, 2 insertions(+), 22 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts index e1345627d502c8..cb6e94cfa6093b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts @@ -6,11 +6,8 @@ */ import { i18n } from '@kbn/i18n'; -import { curry, isString } from 'lodash'; -import axios, { AxiosError, AxiosResponse } from 'axios'; +import { curry } from 'lodash'; import { schema, TypeOf } from '@kbn/config-schema'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { map, getOrElse } from 'fp-ts/lib/Option'; import { Logger } from '@kbn/core/server'; import { CasesWebhookExecutorResultData, @@ -18,13 +15,9 @@ import { CasesWebhookSecretConfigurationType, ExecutorParams, } from './types'; -import { getRetryAfterIntervalFromHeaders } from '../lib/http_rersponse_retry_header'; import { nullableType } from '../lib/nullable'; -import { isOk, promiseResult, Result } from '../lib/result_type'; import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../../types'; import { ActionsConfigurationUtilities } from '../../actions_config'; -import { request } from '../lib/axios_utils'; -import { renderMustacheString } from '../../lib/mustache_renderer'; import { createExternalService } from './service'; import { ExecutorParamsSchema } from './schema'; diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts index 36118c31ea888e..42fd967b785393 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts @@ -6,12 +6,7 @@ */ import { schema } from '@kbn/config-schema'; -import { - ExecutorSubActionCommonFieldsParamsSchema, - ExecutorSubActionGetIncidentParamsSchema, - ExecutorSubActionHandshakeParamsSchema, - ExecutorSubActionPushParamsSchema, -} from '../jira/schema'; +import { ExecutorSubActionPushParamsSchema } from '../jira/schema'; export const ExternalIncidentServiceConfiguration = { url: schema.string(), diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts index 4ea00405ecf0d2..9f0c118f92966f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts @@ -8,15 +8,12 @@ import axios, { AxiosError, AxiosResponse } from 'axios'; import { Logger } from '@kbn/core/server'; -import { pipe } from 'fp-ts/pipeable'; -import { getOrElse, map } from 'fp-ts/Option'; import { ActionTypeExecutorResult } from '../../../common'; import { isOk, promiseResult, Result } from '../lib/result_type'; import { CreateIncidentParams, ExternalServiceCredentials, ResponseError, - ExternalServiceIncidentResponse, ExternalService, CasesWebhookPublicConfigurationType, CasesWebhookSecretConfigurationType, @@ -25,7 +22,6 @@ import { import * as i18n from './translations'; import { request, getErrorMessage, throwIfResponseIsNotValid } from '../lib/axios_utils'; import { ActionsConfigurationUtilities } from '../../actions_config'; -import { getRetryAfterIntervalFromHeaders } from '../lib/http_rersponse_retry_header'; const VERSION = '2'; const BASE_URL = `rest/api/${VERSION}`; diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts index 62277cd219c591..68b6b6dcde2d0e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts @@ -6,9 +6,7 @@ */ import { TypeOf } from '@kbn/config-schema'; -import { PushToServiceResponse } from '../jira/types'; import { ExecutorParamsSchema } from '../jira/schema'; -import { ActionTypeExecutorResult } from '../../../common'; import { ActionsConfigurationUtilities } from '../../actions_config'; import { ExternalIncidentServiceConfigurationSchema, diff --git a/x-pack/plugins/cases/public/components/connectors/cases_webhook/validator.ts b/x-pack/plugins/cases/public/components/connectors/cases_webhook/validator.ts index 6424fb58adf2d9..f8a132803ae37f 100644 --- a/x-pack/plugins/cases/public/components/connectors/cases_webhook/validator.ts +++ b/x-pack/plugins/cases/public/components/connectors/cases_webhook/validator.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { CasesWebhookConnectorType } from '../../../../common/api'; import { ValidationConfig } from '../../../common/shared_imports'; import { CaseActionConnector } from '../../types'; diff --git a/x-pack/plugins/cases/server/client/cases/push.ts b/x-pack/plugins/cases/server/client/cases/push.ts index 300a88cd401859..4c0c514af4801e 100644 --- a/x-pack/plugins/cases/server/client/cases/push.ts +++ b/x-pack/plugins/cases/server/client/cases/push.ts @@ -21,7 +21,6 @@ import { CommentType, CommentRequestAlertType, ConnectorTypes, - NONE_CONNECTOR_ID, } from '../../../common/api'; import { CASE_COMMENT_SAVED_OBJECT } from '../../../common/constants'; From 959e689d733e143426fb1f6bda129bbc4aa2b2b4 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 18 May 2022 12:39:34 -0600 Subject: [PATCH 08/97] werk better --- .../cases_webhook/index.ts | 53 ++++++++++--------- .../cases_webhook/schema.ts | 25 ++++++++- .../cases_webhook/service.ts | 34 +++--------- .../cases_webhook/types.ts | 4 +- .../builtin_action_types/jira/service.ts | 9 +--- .../cases_webhook/webhook.tsx | 23 +++++--- .../cases_webhook/webhook_params.tsx | 52 ++++++++++++------ .../components/builtin_action_types/types.ts | 5 +- 8 files changed, 115 insertions(+), 90 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts index cb6e94cfa6093b..b2033b7d2f40d7 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts @@ -73,11 +73,7 @@ const SecretsSchema = schema.object(secretSchemaProps, { }); // params definition -export type ActionParamsType = TypeOf; -const ParamsSchema = schema.object({ - summary: schema.string(), - description: schema.string(), -}); +export type ActionParamsType = TypeOf; export const ActionTypeId = '.cases-webhook'; // action type definition @@ -149,27 +145,32 @@ export async function executor( }: { logger: Logger; configurationUtilities: ActionsConfigurationUtilities }, execOptions: CasesWebhookActionTypeExecutorOptions ): Promise> { - const actionId = execOptions.actionId; - const { summary, description } = execOptions.params; - - console.log('call createExternalService'); - const externalService = createExternalService( - actionId, - { - config: execOptions.config, - secrets: execOptions.secrets, - }, - logger, - configurationUtilities - ); - - console.log('call createIncident'); - const result = await externalService.createIncident({ - summary, - description, - }); - console.log('result', result); - return result; + try { + const actionId = execOptions.actionId; + const { subAction, subActionParams } = execOptions.params; + const externalService = createExternalService( + actionId, + { + config: execOptions.config, + secrets: execOptions.secrets, + }, + logger, + configurationUtilities + ); + if (subAction === 'pushToService') { + const { summary, description } = subActionParams.incident; + const result = await externalService.createIncident({ + summary, + description: description || '', + }); + console.log('result', result); + return result; // successResult(actionId, result); + } + return errorResultRequestFailed(actionId, 'wow what a massive fail'); + } catch (err) { + console.log('err', err); + return err; + } // if (isOk(result)) { // // const { diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts index 42fd967b785393..1121067af27c5c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts @@ -6,7 +6,6 @@ */ import { schema } from '@kbn/config-schema'; -import { ExecutorSubActionPushParamsSchema } from '../jira/schema'; export const ExternalIncidentServiceConfiguration = { url: schema.string(), @@ -26,6 +25,30 @@ export const ExternalIncidentServiceSecretConfigurationSchema = schema.object( ExternalIncidentServiceSecretConfiguration ); +export const ExecutorSubActionPushParamsSchema = schema.object({ + incident: schema.object({ + summary: schema.string(), + description: schema.nullable(schema.string()), + labels: schema.nullable( + schema.arrayOf( + schema.string({ + validate: (label) => + // Matches any space, tab or newline character. + label.match(/\s/g) ? `The label ${label} cannot contain spaces` : undefined, + }) + ) + ), + }), + comments: schema.nullable( + schema.arrayOf( + schema.object({ + comment: schema.string(), + commentId: schema.string(), + }) + ) + ), +}); + export const ExecutorParamsSchema = schema.oneOf([ schema.object({ subAction: schema.literal('pushToService'), diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts index 9f0c118f92966f..6f0a9c530f8777 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts @@ -23,11 +23,6 @@ import * as i18n from './translations'; import { request, getErrorMessage, throwIfResponseIsNotValid } from '../lib/axios_utils'; import { ActionsConfigurationUtilities } from '../../actions_config'; -const VERSION = '2'; -const BASE_URL = `rest/api/${VERSION}`; - -const VIEW_INCIDENT_URL = `browse`; - export const createExternalService = ( actionId: string, { config, secrets }: ExternalServiceCredentials, @@ -36,27 +31,16 @@ export const createExternalService = ( ): ExternalService => { const { url, incident } = config as CasesWebhookPublicConfigurationType; const { password, user } = secrets as CasesWebhookSecretConfigurationType; - console.log('eh eh eh', { - bool: !url || !password || !user, - config, - url, - password, - user, - }); if (!url || !password || !user) { throw Error(`[Action]${i18n.NAME}: Wrong configuration.`); } const incidentUrl = url.endsWith('/') ? url.slice(0, -1) : url; - // const incidentUrl = `${urlWithoutTrailingSlash}/${BASE_URL}/issue`; const axiosInstance = axios.create({ auth: { username: user, password }, }); - // - const getIncidentViewURL = (key: string) => { - return `https://siem-kibana.atlassian.net/browse/${key}`; - }; + const createErrorMessage = (errorResponse: ResponseError | null | undefined): string => { if (errorResponse == null) { return 'unknown: errorResponse was null'; @@ -153,22 +137,14 @@ export const createExternalService = ( str = str.replace('$DESC', desc); return JSON.parse(str); }; - // Action Executor Result w/ internationalisation - function successResult(actionId: string, data: unknown): ActionTypeExecutorResult { - return { status: 'ok', data, actionId }; - } + const createIncident = async ({ summary, description, }: CreateIncidentParams): Promise => { const data = replaceSumDesc(summary, description); - console.log('cases webhook args!!', { - axios: axiosInstance, - url: `${incidentUrl}`, - logger, - method: 'post', + console.log('cases webhook data!!', { data, - configurationUtilities, }); try { const result: Result = await promiseResult( @@ -527,3 +503,7 @@ export const createExternalService = ( // getIssue, }; }; +// Action Executor Result w/ internationalisation +function successResult(actionId: string, data: unknown): ActionTypeExecutorResult { + return { status: 'ok', data, actionId }; +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts index 68b6b6dcde2d0e..1416d05953d05d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts @@ -6,9 +6,10 @@ */ import { TypeOf } from '@kbn/config-schema'; -import { ExecutorParamsSchema } from '../jira/schema'; import { ActionsConfigurationUtilities } from '../../actions_config'; import { + ExecutorParamsSchema, + ExecutorSubActionPushParamsSchema, ExternalIncidentServiceConfigurationSchema, ExternalIncidentServiceSecretConfigurationSchema, } from './schema'; @@ -36,6 +37,7 @@ export interface ExternalServiceIncidentResponse { } export type ExecutorParams = TypeOf; +export type ExecutorSubActionPushParams = TypeOf; export interface ExternalService { createIncident: (params: CreateIncidentParams) => Promise; 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 5ab8eb0e0952e5..4d98ebfda5bc49 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 @@ -223,14 +223,7 @@ export const createExternalService = ( issueType, }); console.log('jira args', { - axios: axiosInstance, - url: `${incidentUrl}`, - logger, - method: 'post', - data: { - fields, - }, - configurationUtilities, + fields, }); try { const res = await request({ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx index 5d52fefa3cf9f9..44cb6cf87684e5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx @@ -88,19 +88,26 @@ export function getActionType(): ActionTypeModel< }, validateParams: async ( actionParams: CasesWebhookActionParams - ): Promise> => { + ): Promise> => { const translations = await import('./translations'); const errors = { - summary: new Array(), - description: new Array(), + 'subActionParams.incident.summary': new Array(), + 'subActionParams.incident.description': new Array(), }; const validationResult = { errors }; - validationResult.errors = errors; - if (!actionParams.summary?.length) { - errors.summary.push(translations.BODY_REQUIRED); + if ( + actionParams.subActionParams && + actionParams.subActionParams.incident && + !actionParams.subActionParams.incident.summary?.length + ) { + errors['subActionParams.incident.summary'].push(translations.BODY_REQUIRED); } - if (!actionParams.description?.length) { - errors.description.push(translations.BODY_REQUIRED); + if ( + actionParams.subActionParams && + actionParams.subActionParams.incident && + !actionParams.subActionParams.incident.description?.length + ) { + errors['subActionParams.incident.description'].push(translations.BODY_REQUIRED); } return validationResult; }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_params.tsx index d07cde382a78dc..e0349e63a6e9b4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_params.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFormRow } from '@elastic/eui'; import { ActionParamsProps } from '../../../../types'; @@ -20,27 +20,45 @@ const WebhookParamsFields: React.FunctionComponent { - const { summary, description } = actionParams; - const incident = useMemo( - () => ({ - summary, - description, - }), - [summary, description] + const { incident } = useMemo( + () => + actionParams.subActionParams ?? + ({ + incident: {}, + comments: [], + } as unknown as CasesWebhookActionParams['subActionParams']), + [actionParams.subActionParams] ); const editSubActionProperty = useCallback( (key: string, value: any) => { - if (key === 'summary') { - editAction('summary', value, index); - } - if (key === 'description') { - editAction('description', value, index); - } - // console.log('edit action', { ...incident, [key]: value }, index); - // return editAction('subActionParams', { ...incident, [key]: value }, index); + return editAction( + 'subActionParams', + { + incident: { ...incident, [key]: value }, + comments: [], + }, + index + ); }, - [editAction, index] + [editAction, incident, index] ); + useEffect(() => { + if (!actionParams.subAction) { + editAction('subAction', 'pushToService', index); + } + if (!actionParams.subActionParams) { + editAction( + 'subActionParams', + { + incident: {}, + comments: [], + }, + index + ); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [actionParams]); + return ( <> Date: Wed, 18 May 2022 13:11:15 -0600 Subject: [PATCH 09/97] add api file --- .../builtin_action_types/cases_webhook/api.ts | 61 ++++++++++++++++++ .../cases_webhook/index.ts | 64 ++++++++++++------- .../cases_webhook/schema.ts | 1 + .../cases_webhook/service.ts | 14 ++-- .../cases_webhook/types.ts | 21 +++++- 5 files changed, 126 insertions(+), 35 deletions(-) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/cases_webhook/api.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/api.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/api.ts new file mode 100644 index 00000000000000..e782f83f9531a0 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/api.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + ExternalServiceApi, + Incident, + PushToServiceApiHandlerArgs, + PushToServiceResponse, +} from './types'; + +const pushToServiceHandler = async ({ + externalService, + params, +}: PushToServiceApiHandlerArgs): Promise => { + // const { comments } = params; + const { externalId, ...rest } = params.incident; + const incident: Incident = rest; + const res: PushToServiceResponse = await externalService.createIncident({ + incident, + }); + + // if (externalId != null) { + // res = await externalService.updateIncident({ + // incidentId: externalId, + // incident, + // }); + // } else { + // res = await externalService.createIncident({ + // incident, + // }); + // } + + // if (comments && Array.isArray(comments) && comments.length > 0) { + // res.comments = []; + // for (const currentComment of comments) { + // if (!currentComment.comment) { + // continue; + // } + // const comment = await externalService.createComment({ + // incidentId: res.id, + // comment: currentComment, + // }); + // res.comments = [ + // ...(res.comments ?? []), + // { + // commentId: comment.commentId, + // pushedDate: comment.pushedDate, + // }, + // ]; + // } + // } + + return res; +}; +export const api: ExternalServiceApi = { + pushToService: pushToServiceHandler, +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts index b2033b7d2f40d7..727f06e7a72d6d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts @@ -14,12 +14,14 @@ import { CasesWebhookPublicConfigurationType, CasesWebhookSecretConfigurationType, ExecutorParams, + ExecutorSubActionPushParams, } from './types'; import { nullableType } from '../lib/nullable'; import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../../types'; import { ActionsConfigurationUtilities } from '../../actions_config'; import { createExternalService } from './service'; import { ExecutorParamsSchema } from './schema'; +import { api } from './api'; // config definition export enum CasesWebhookMethods { @@ -75,6 +77,8 @@ const SecretsSchema = schema.object(secretSchemaProps, { // params definition export type ActionParamsType = TypeOf; +const supportedSubActions: string[] = ['pushToService']; + export const ActionTypeId = '.cases-webhook'; // action type definition export function getActionType({ @@ -145,33 +149,45 @@ export async function executor( }: { logger: Logger; configurationUtilities: ActionsConfigurationUtilities }, execOptions: CasesWebhookActionTypeExecutorOptions ): Promise> { - try { - const actionId = execOptions.actionId; - const { subAction, subActionParams } = execOptions.params; - const externalService = createExternalService( - actionId, - { - config: execOptions.config, - secrets: execOptions.secrets, - }, + const actionId = execOptions.actionId; + const { subAction, subActionParams } = execOptions.params; + let data: CasesWebhookExecutorResultData | null = null; + + const externalService = createExternalService( + actionId, + { + config: execOptions.config, + secrets: execOptions.secrets, + }, + logger, + configurationUtilities + ); + + if (!api[subAction]) { + const errorMessage = `[Action][ExternalService] Unsupported subAction type ${subAction}.`; + logger.error(errorMessage); + throw new Error(errorMessage); + } + + if (!supportedSubActions.includes(subAction)) { + const errorMessage = `[Action][ExternalService] subAction ${subAction} not implemented.`; + logger.error(errorMessage); + throw new Error(errorMessage); + } + + if (subAction === 'pushToService') { + const pushToServiceParams = subActionParams as ExecutorSubActionPushParams; + data = await api.pushToService({ + externalService, + params: pushToServiceParams, logger, - configurationUtilities - ); - if (subAction === 'pushToService') { - const { summary, description } = subActionParams.incident; - const result = await externalService.createIncident({ - summary, - description: description || '', - }); - console.log('result', result); - return result; // successResult(actionId, result); - } - return errorResultRequestFailed(actionId, 'wow what a massive fail'); - } catch (err) { - console.log('err', err); - return err; + }); + + logger.debug(`response push to service for incident id: ${data.id}`); } + return { status: 'ok', data: data ?? {}, actionId }; + // if (isOk(result)) { // // const { // // value: { status, statusText }, diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts index 1121067af27c5c..4cbae0b6205e97 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts @@ -29,6 +29,7 @@ export const ExecutorSubActionPushParamsSchema = schema.object({ incident: schema.object({ summary: schema.string(), description: schema.nullable(schema.string()), + externalId: schema.nullable(schema.string()), labels: schema.nullable( schema.arrayOf( schema.string({ diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts index 6f0a9c530f8777..2a777eea326fd1 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts @@ -29,7 +29,7 @@ export const createExternalService = ( logger: Logger, configurationUtilities: ActionsConfigurationUtilities ): ExternalService => { - const { url, incident } = config as CasesWebhookPublicConfigurationType; + const { url, incident: incidentStringified } = config as CasesWebhookPublicConfigurationType; const { password, user } = secrets as CasesWebhookSecretConfigurationType; if (!url || !password || !user) { throw Error(`[Action]${i18n.NAME}: Wrong configuration.`); @@ -132,17 +132,15 @@ export const createExternalService = ( }; const replaceSumDesc = (sum: string, desc: string) => { - let str = incident; // incident is stringified object + let str = incidentStringified; // incident is stringified object str = str.replace('$SUM', sum); str = str.replace('$DESC', desc); return JSON.parse(str); }; - const createIncident = async ({ - summary, - description, - }: CreateIncidentParams): Promise => { - const data = replaceSumDesc(summary, description); + const createIncident = async ({ incident }: CreateIncidentParams): Promise => { + const { summary, description } = incident; + const data = replaceSumDesc(summary, description ?? ''); console.log('cases webhook data!!', { data, }); @@ -166,7 +164,7 @@ export const createExternalService = ( console.log('DATA', data2); logger.debug(`response from webhook action "${actionId}": [HTTP ${status}] ${statusText}`); - return successResult(actionId, data); + return data; // successResult(actionId, data); } else { const { error } = result; if (error.response) { diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts index 1416d05953d05d..4c391c33d0c230 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts @@ -6,6 +6,7 @@ */ import { TypeOf } from '@kbn/config-schema'; +import { Logger } from '@kbn/core/server'; import { ActionsConfigurationUtilities } from '../../actions_config'; import { ExecutorParamsSchema, @@ -25,8 +26,7 @@ export interface ExternalServiceValidation { } export interface CreateIncidentParams { - summary: string; - description: string; + incident: Incident; } export interface ExternalServiceIncidentResponse { @@ -35,13 +35,28 @@ export interface ExternalServiceIncidentResponse { url: string; pushedDate: string; } +export type Incident = Omit; export type ExecutorParams = TypeOf; export type ExecutorSubActionPushParams = TypeOf; - +export type PushToServiceApiParams = ExecutorSubActionPushParams; export interface ExternalService { createIncident: (params: CreateIncidentParams) => Promise; } + +export interface ExternalServiceApiHandlerArgs { + externalService: ExternalService; +} +export interface PushToServiceApiHandlerArgs extends ExternalServiceApiHandlerArgs { + params: PushToServiceApiParams; + logger: Logger; +} +export type PushToServiceResponse = ExternalServiceIncidentResponse; + +export interface ExternalServiceApi { + pushToService: (args: PushToServiceApiHandlerArgs) => Promise; +} + export type CasesWebhookExecutorResultData = ExternalServiceIncidentResponse; export interface ResponseError { From 823c1fa5f01da4415e94cf33bf230cf518c26b5e Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 19 May 2022 07:58:56 -0600 Subject: [PATCH 10/97] type cleanup --- .../cases_webhook/index.ts | 260 ++---------------- .../cases_webhook/schema.ts | 14 +- .../cases_webhook/service.ts | 4 +- .../cases_webhook/types.ts | 42 ++- .../cases_webhook/validators.ts | 59 ++++ .../builtin_action_types/jira/schema.ts | 4 +- .../cases_webhook/webhook_connectors.tsx | 4 +- .../components/builtin_action_types/types.ts | 2 +- 8 files changed, 128 insertions(+), 261 deletions(-) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts index 727f06e7a72d6d..cfc3aa26f65939 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts @@ -7,75 +7,26 @@ import { i18n } from '@kbn/i18n'; import { curry } from 'lodash'; -import { schema, TypeOf } from '@kbn/config-schema'; +import { schema } from '@kbn/config-schema'; import { Logger } from '@kbn/core/server'; import { + CasesWebhookActionParamsType, CasesWebhookExecutorResultData, CasesWebhookPublicConfigurationType, CasesWebhookSecretConfigurationType, ExecutorParams, ExecutorSubActionPushParams, } from './types'; -import { nullableType } from '../lib/nullable'; import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../../types'; import { ActionsConfigurationUtilities } from '../../actions_config'; import { createExternalService } from './service'; -import { ExecutorParamsSchema } from './schema'; +import { + ExecutorParamsSchema, + ExternalIncidentServiceConfiguration, + ExternalIncidentServiceSecretConfiguration, +} from './schema'; import { api } from './api'; - -// config definition -export enum CasesWebhookMethods { - POST = 'post', - PUT = 'put', -} - -export type CasesWebhookActionType = ActionType< - ActionTypeConfigType, - ActionTypeSecretsType, - ActionParamsType, - unknown ->; -export type CasesWebhookActionTypeExecutorOptions = ActionTypeExecutorOptions< - ActionTypeConfigType, - ActionTypeSecretsType, - ActionParamsType ->; - -const HeadersSchema = schema.recordOf(schema.string(), schema.string()); -const configSchemaProps = { - url: schema.string(), - method: schema.oneOf( - [schema.literal(CasesWebhookMethods.POST), schema.literal(CasesWebhookMethods.PUT)], - { - defaultValue: CasesWebhookMethods.POST, - } - ), - incident: schema.string(), // stringified object - headers: nullableType(HeadersSchema), - hasAuth: schema.boolean({ defaultValue: true }), -}; -const ConfigSchema = schema.object(configSchemaProps); -export type ActionTypeConfigType = TypeOf; - -// secrets definition -export type ActionTypeSecretsType = TypeOf; -const secretSchemaProps = { - user: schema.string(), - password: schema.string(), -}; -const SecretsSchema = schema.object(secretSchemaProps, { - validate: (secrets) => { - // user and password must be set together (or not at all) - if (!secrets.password && !secrets.user) return; - if (secrets.password && secrets.user) return; - return i18n.translate('xpack.actions.builtin.casesWebhook.invalidUsernamePassword', { - defaultMessage: 'both user and password must be specified', - }); - }, -}); - -// params definition -export type ActionParamsType = TypeOf; +import { validate } from './validators'; const supportedSubActions: string[] = ['pushToService']; @@ -100,58 +51,33 @@ export function getActionType({ defaultMessage: 'Cases Webhook', }), validate: { - config: schema.object(configSchemaProps, { - validate: curry(validateActionTypeConfig)(configurationUtilities), + config: schema.object(ExternalIncidentServiceConfiguration, { + validate: curry(validate.config)(configurationUtilities), + }), + secrets: schema.object(ExternalIncidentServiceSecretConfiguration, { + validate: curry(validate.secrets), }), - secrets: SecretsSchema, params: ExecutorParamsSchema, }, executor: curry(executor)({ logger, configurationUtilities }), }; } -function validateActionTypeConfig( - configurationUtilities: ActionsConfigurationUtilities, - configObject: ActionTypeConfigType -) { - const configuredUrl = configObject.url; - try { - new URL(configuredUrl); - } catch (err) { - return i18n.translate( - 'xpack.actions.builtin.casesWebhook.casesWebhookConfigurationErrorNoHostname', - { - defaultMessage: 'error configuring cases webhook action: unable to parse url: {err}', - values: { - err, - }, - } - ); - } - - try { - configurationUtilities.ensureUriAllowed(configuredUrl); - } catch (allowListError) { - return i18n.translate('xpack.actions.builtin.casesWebhook.casesWebhookConfigurationError', { - defaultMessage: 'error configuring cases webhook action: {message}', - values: { - message: allowListError.message, - }, - }); - } -} - // action executor export async function executor( { logger, configurationUtilities, }: { logger: Logger; configurationUtilities: ActionsConfigurationUtilities }, - execOptions: CasesWebhookActionTypeExecutorOptions -): Promise> { + execOptions: ActionTypeExecutorOptions< + CasesWebhookPublicConfigurationType, + CasesWebhookSecretConfigurationType, + CasesWebhookActionParamsType + > +): Promise> { const actionId = execOptions.actionId; const { subAction, subActionParams } = execOptions.params; - let data: CasesWebhookExecutorResultData | null = null; + let data: CasesWebhookExecutorResultData | undefined; const externalService = createExternalService( actionId, @@ -186,151 +112,5 @@ export async function executor( logger.debug(`response push to service for incident id: ${data.id}`); } - return { status: 'ok', data: data ?? {}, actionId }; - - // if (isOk(result)) { - // // const { - // // value: { status, statusText }, - // // } = result; - // // logger.debug( - // // `response from cases webhook action "${actionId}": [HTTP ${status}] ${statusText}` - // // ); - // // - // // return successResult(actionId, data); - // } else { - // const { error } = result; - // if (error.response) { - // const { - // status, - // statusText, - // headers: responseHeaders, - // data: { message: responseMessage }, - // } = error.response; - // const responseMessageAsSuffix = responseMessage ? `: ${responseMessage}` : ''; - // const message = `[${status}] ${statusText}${responseMessageAsSuffix}`; - // logger.error(`error on ${actionId} cases webhook event: ${message}`); - // // The request was made and the server responded with a status code - // // that falls out of the range of 2xx - // // special handling for 5xx - // if (status >= 500) { - // return retryResult(actionId, message); - // } - // - // // special handling for rate limiting - // if (status === 429) { - // return pipe( - // getRetryAfterIntervalFromHeaders(responseHeaders), - // map((retry) => retryResultSeconds(actionId, message, retry)), - // getOrElse(() => retryResult(actionId, message)) - // ); - // } - // return errorResultInvalid(actionId, message); - // } else if (error.code) { - // const message = `[${error.code}] ${error.message}`; - // logger.error(`error on ${actionId} cases webhook event: ${message}`); - // return errorResultRequestFailed(actionId, message); - // } else if (error.isAxiosError) { - // const message = `${error.message}`; - // logger.error(`error on ${actionId} cases webhook event: ${message}`); - // return errorResultRequestFailed(actionId, message); - // } - // - // logger.error(`error on ${actionId} cases webhook action: unexpected error`); - // return errorResultUnexpectedError(actionId); - // } -} - -// Action Executor Result w/ internationalisation -function successResult(actionId: string, data: unknown): ActionTypeExecutorResult { return { status: 'ok', data, actionId }; } - -function errorResultInvalid( - actionId: string, - serviceMessage: string -): ActionTypeExecutorResult { - const errMessage = i18n.translate( - 'xpack.actions.builtin.casesWebhook.invalidResponseErrorMessage', - { - defaultMessage: 'error calling cases webhook, invalid response', - } - ); - return { - status: 'error', - message: errMessage, - actionId, - serviceMessage, - }; -} - -function errorResultRequestFailed( - actionId: string, - serviceMessage: string -): ActionTypeExecutorResult { - const errMessage = i18n.translate( - 'xpack.actions.builtin.casesWebhook.requestFailedErrorMessage', - { - defaultMessage: 'error calling cases webhook, request failed', - } - ); - return { - status: 'error', - message: errMessage, - actionId, - serviceMessage, - }; -} - -function errorResultUnexpectedError(actionId: string): ActionTypeExecutorResult { - const errMessage = i18n.translate('xpack.actions.builtin.casesWebhook.unreachableErrorMessage', { - defaultMessage: 'error calling cases webhook, unexpected error', - }); - return { - status: 'error', - message: errMessage, - actionId, - }; -} - -function retryResult(actionId: string, serviceMessage: string): ActionTypeExecutorResult { - const errMessage = i18n.translate( - 'xpack.actions.builtin.casesWebhook.invalidResponseRetryLaterErrorMessage', - { - defaultMessage: 'error calling cases webhook, retry later', - } - ); - return { - status: 'error', - message: errMessage, - retry: true, - actionId, - serviceMessage, - }; -} - -function retryResultSeconds( - actionId: string, - serviceMessage: string, - - retryAfter: number -): ActionTypeExecutorResult { - const retryEpoch = Date.now() + retryAfter * 1000; - const retry = new Date(retryEpoch); - const retryString = retry.toISOString(); - const errMessage = i18n.translate( - 'xpack.actions.builtin.casesWebhook.invalidResponseRetryDateErrorMessage', - { - defaultMessage: 'error calling cases webhook, retry at {retryString}', - values: { - retryString, - }, - } - ); - return { - status: 'error', - message: errMessage, - retry, - actionId, - serviceMessage, - }; -} diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts index 4cbae0b6205e97..e23515d2388cd2 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts @@ -6,10 +6,22 @@ */ import { schema } from '@kbn/config-schema'; +import { CasesWebhookMethods } from './types'; +import { nullableType } from '../lib/nullable'; + +const HeadersSchema = schema.recordOf(schema.string(), schema.string()); export const ExternalIncidentServiceConfiguration = { url: schema.string(), - incident: schema.string(), // JSON.stringified object + method: schema.oneOf( + [schema.literal(CasesWebhookMethods.POST), schema.literal(CasesWebhookMethods.PUT)], + { + defaultValue: CasesWebhookMethods.POST, + } + ), + incidentJson: schema.string(), // stringified object + headers: nullableType(HeadersSchema), + hasAuth: schema.boolean({ defaultValue: true }), }; export const ExternalIncidentServiceConfigurationSchema = schema.object( diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts index 2a777eea326fd1..3bd122f124eb7c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts @@ -29,7 +29,7 @@ export const createExternalService = ( logger: Logger, configurationUtilities: ActionsConfigurationUtilities ): ExternalService => { - const { url, incident: incidentStringified } = config as CasesWebhookPublicConfigurationType; + const { url, incidentJson } = config as CasesWebhookPublicConfigurationType; const { password, user } = secrets as CasesWebhookSecretConfigurationType; if (!url || !password || !user) { throw Error(`[Action]${i18n.NAME}: Wrong configuration.`); @@ -132,7 +132,7 @@ export const createExternalService = ( }; const replaceSumDesc = (sum: string, desc: string) => { - let str = incidentStringified; // incident is stringified object + let str = incidentJson; // incident is stringified object str = str.replace('$SUM', sum); str = str.replace('$DESC', desc); return JSON.parse(str); diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts index 4c391c33d0c230..4b64c65a7a3bc6 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts @@ -15,18 +15,34 @@ import { ExternalIncidentServiceSecretConfigurationSchema, } from './schema'; +// config definition +export const enum CasesWebhookMethods { + POST = 'post', + PUT = 'put', +} + +// config +export type CasesWebhookPublicConfigurationType = TypeOf< + typeof ExternalIncidentServiceConfigurationSchema +>; +// secrets +export type CasesWebhookSecretConfigurationType = TypeOf< + typeof ExternalIncidentServiceSecretConfigurationSchema +>; +// params +export type CasesWebhookActionParamsType = TypeOf; + export interface ExternalServiceCredentials { config: Record; secrets: Record; } export interface ExternalServiceValidation { - config: (configurationUtilities: ActionsConfigurationUtilities, configObject: any) => void; - secrets: (configurationUtilities: ActionsConfigurationUtilities, secrets: any) => void; -} - -export interface CreateIncidentParams { - incident: Incident; + config: ( + configurationUtilities: ActionsConfigurationUtilities, + configObject: CasesWebhookPublicConfigurationType + ) => void; + secrets: (secrets: CasesWebhookSecretConfigurationType) => void; } export interface ExternalServiceIncidentResponse { @@ -40,13 +56,20 @@ export type Incident = Omit; export type ExecutorSubActionPushParams = TypeOf; export type PushToServiceApiParams = ExecutorSubActionPushParams; + +// incident service export interface ExternalService { createIncident: (params: CreateIncidentParams) => Promise; } +export interface CreateIncidentParams { + incident: Incident; +} export interface ExternalServiceApiHandlerArgs { externalService: ExternalService; } + +// incident api export interface PushToServiceApiHandlerArgs extends ExternalServiceApiHandlerArgs { params: PushToServiceApiParams; logger: Logger; @@ -63,10 +86,3 @@ export interface ResponseError { errorMessages: string[] | null | undefined; errors: { [k: string]: string } | null | undefined; } - -export type CasesWebhookPublicConfigurationType = TypeOf< - typeof ExternalIncidentServiceConfigurationSchema ->; -export type CasesWebhookSecretConfigurationType = TypeOf< - typeof ExternalIncidentServiceSecretConfigurationSchema ->; diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts new file mode 100644 index 00000000000000..80de70cf63c90a --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { ActionsConfigurationUtilities } from '../../actions_config'; +import { + CasesWebhookPublicConfigurationType, + CasesWebhookSecretConfigurationType, + ExternalServiceValidation, +} from './types'; + +const validateConfig = ( + configurationUtilities: ActionsConfigurationUtilities, + configObject: CasesWebhookPublicConfigurationType +) => { + const configuredUrl = configObject.url; + try { + new URL(configuredUrl); + } catch (err) { + return i18n.translate( + 'xpack.actions.builtin.casesWebhook.casesWebhookConfigurationErrorNoHostname', + { + defaultMessage: 'error configuring cases webhook action: unable to parse url: {err}', + values: { + err, + }, + } + ); + } + + try { + configurationUtilities.ensureUriAllowed(configuredUrl); + } catch (allowListError) { + return i18n.translate('xpack.actions.builtin.casesWebhook.casesWebhookConfigurationError', { + defaultMessage: 'error configuring cases webhook action: {message}', + values: { + message: allowListError.message, + }, + }); + } +}; + +export const validateSecrets = (secrets: CasesWebhookSecretConfigurationType) => { + // user and password must be set together (or not at all) + if (!secrets.password && !secrets.user) return; + if (secrets.password && secrets.user) return; + return i18n.translate('xpack.actions.builtin.casesWebhook.invalidUsernamePassword', { + defaultMessage: 'both user and password must be specified', + }); +}; + +export const validate: ExternalServiceValidation = { + config: validateConfig, + secrets: validateSecrets, +}; 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 eb2f540deaa9ad..60877b2c0ee62b 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 @@ -17,8 +17,8 @@ export const ExternalIncidentServiceConfigurationSchema = schema.object( ); export const ExternalIncidentServiceSecretConfiguration = { - email: schema.string(), - apiToken: schema.string(), + user: schema.string(), + password: schema.string(), }; export const ExternalIncidentServiceSecretConfigurationSchema = schema.object( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx index 2df13235f28dcd..08771699193868 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx @@ -36,8 +36,8 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< ActionConnectorFieldsProps > = ({ action, editActionConfig, editActionSecrets, errors, readOnly }) => { const { user, password } = action.secrets; - const { method, url, incident, headers, hasAuth } = action.config; - console.log('incident here', incident); + const { method, url, incidentJson, headers, hasAuth } = action.config; + console.log('incident here', incidentJson); const [httpHeaderKey, setHttpHeaderKey] = useState(''); const [httpHeaderValue, setHttpHeaderValue] = useState(''); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts index facb81a55475bc..d6c18b691ab49c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts @@ -141,7 +141,7 @@ export type WebhookActionConnector = UserConfiguredActionConnector; hasAuth: boolean; } From ef6e12e0f55c91a82df4a2981d427aba7eb7f108 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 19 May 2022 08:18:36 -0600 Subject: [PATCH 11/97] fix types more --- .../cases_webhook/types.ts | 34 +++++++++++++++++++ .../cases_webhook/webhook.tsx | 8 ++--- .../cases_webhook/webhook_connectors.tsx | 29 ++++++++-------- .../cases_webhook/webhook_params.tsx | 2 +- .../components/builtin_action_types/types.ts | 24 ------------- 5 files changed, 54 insertions(+), 43 deletions(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/types.ts diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/types.ts new file mode 100644 index 00000000000000..807a1190f7a8fe --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/types.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* eslint-disable @kbn/eslint/no-restricted-paths */ + +import { ExecutorSubActionPushParams } from '@kbn/actions-plugin/server/builtin_action_types/cases_webhook/types'; +import { UserConfiguredActionConnector } from '../../../../types'; + +export interface CasesWebhookActionParams { + subAction: string; + subActionParams: ExecutorSubActionPushParams; +} + +export interface CasesWebhookConfig { + method: string; + url: string; + incidentJson: string; + headers: Record; + hasAuth: boolean; +} + +export interface CasesWebhookSecrets { + user: string; + password: string; +} + +export type CasesWebhookActionConnector = UserConfiguredActionConnector< + CasesWebhookConfig, + CasesWebhookSecrets +>; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx index 44cb6cf87684e5..0543f5d9e682d7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx @@ -17,7 +17,7 @@ import { CasesWebhookConfig, CasesWebhookSecrets, CasesWebhookActionConnector, -} from '../types'; +} from './types'; import { isValidUrl } from '../../../lib/value_validators'; export function getActionType(): ActionTypeModel< @@ -50,7 +50,7 @@ export function getActionType(): ActionTypeModel< const configErrors = { url: new Array(), method: new Array(), - incident: new Array(), + incidentJson: new Array(), }; const secretsErrors = { user: new Array(), @@ -66,8 +66,8 @@ export function getActionType(): ActionTypeModel< if (action.config.url && !isValidUrl(action.config.url)) { configErrors.url = [...configErrors.url, translations.URL_INVALID]; } - if (!action.config.incident) { - configErrors.incident.push(translations.INCIDENT_REQUIRED); + if (!action.config.incidentJson) { + configErrors.incidentJson.push(translations.INCIDENT_REQUIRED); } if (!action.config.method) { configErrors.method.push(translations.METHOD_REQUIRED); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx index 08771699193868..6cd0e5a049f2d5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx @@ -26,7 +26,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ActionConnectorFieldsProps } from '../../../../types'; -import { CasesWebhookActionConnector } from '../types'; +import { CasesWebhookActionConnector } from './types'; import { getEncryptedFieldNotifyLabel } from '../../get_encrypted_field_notify_label'; import { JsonEditorWithMessageVariables } from '../../json_editor_with_message_variables'; @@ -37,7 +37,6 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< > = ({ action, editActionConfig, editActionSecrets, errors, readOnly }) => { const { user, password } = action.secrets; const { method, url, incidentJson, headers, hasAuth } = action.config; - console.log('incident here', incidentJson); const [httpHeaderKey, setHttpHeaderKey] = useState(''); const [httpHeaderValue, setHttpHeaderValue] = useState(''); @@ -232,8 +231,10 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< const isUserInvalid: boolean = user !== undefined && errors.user !== undefined && errors.user.length > 0; - const isIncidentInvalid: boolean = - errors.url !== undefined && errors.url.length > 0 && url !== undefined; + const isIncidentJsonInvalid: boolean = + errors.incidentJson !== undefined && + errors.incidentJson.length > 0 && + incidentJson !== undefined; return ( <> @@ -294,10 +295,10 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< { - editActionConfig('incident', json); + editActionConfig('incidentJson', json); }} onBlur={() => { - if (!incident) { - editActionConfig('incident', ''); + if (!incidentJson) { + editActionConfig('incidentJson', ''); } }} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_params.tsx index e0349e63a6e9b4..a02f871f114dd6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_params.tsx @@ -9,7 +9,7 @@ import React, { useCallback, useEffect, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFormRow } from '@elastic/eui'; import { ActionParamsProps } from '../../../../types'; -import { CasesWebhookActionParams } from '../types'; +import { CasesWebhookActionParams } from './types'; import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables'; import { TextFieldWithMessageVariables } from '../../text_field_with_message_variables'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts index d6c18b691ab49c..1d0c58ffb8f213 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts @@ -6,7 +6,6 @@ */ import { EuiIconProps } from '@elastic/eui'; -import { ExecutorSubActionPushParams } from '@kbn/actions-plugin/server/builtin_action_types/cases_webhook/types'; import { UserConfiguredActionConnector } from '../../../types'; export interface EmailActionParams { @@ -73,11 +72,6 @@ export interface WebhookActionParams { body?: string; } -export interface CasesWebhookActionParams { - subAction: string; - subActionParams: ExecutorSubActionPushParams; -} - export interface EmailConfig { from: string; host: string; @@ -138,24 +132,6 @@ export interface WebhookSecrets { export type WebhookActionConnector = UserConfiguredActionConnector; -export interface CasesWebhookConfig { - method: string; - url: string; - incidentJson: string; - headers: Record; - hasAuth: boolean; -} - -export interface CasesWebhookSecrets { - user: string; - password: string; -} - -export type CasesWebhookActionConnector = UserConfiguredActionConnector< - CasesWebhookConfig, - CasesWebhookSecrets ->; - export enum XmattersSeverityOptions { CRITICAL = 'critical', HIGH = 'high', From 4017aa887bb30b790e07d39bd8a2301e62ce4ca5 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 19 May 2022 09:57:48 -0600 Subject: [PATCH 12/97] add all fields to form --- .../cases_webhook/schema.ts | 26 +- .../cases_webhook/service.ts | 62 +-- .../cases_webhook/validators.ts | 9 +- .../server/builtin_action_types/jira/index.ts | 6 +- .../builtin_action_types/jira/schema.ts | 4 +- .../cases_webhook/translations.ts | 90 +++- .../cases_webhook/types.ts | 19 +- .../cases_webhook/webhook.tsx | 86 ++- .../cases_webhook/webhook_connectors.tsx | 497 +++++++++++++++--- 9 files changed, 638 insertions(+), 161 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts index e23515d2388cd2..fd9eb27dad368b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts @@ -12,14 +12,34 @@ import { nullableType } from '../lib/nullable'; const HeadersSchema = schema.recordOf(schema.string(), schema.string()); export const ExternalIncidentServiceConfiguration = { - url: schema.string(), - method: schema.oneOf( + createIncidentUrl: schema.string(), + createIncidentMethod: schema.oneOf( [schema.literal(CasesWebhookMethods.POST), schema.literal(CasesWebhookMethods.PUT)], { defaultValue: CasesWebhookMethods.POST, } ), - incidentJson: schema.string(), // stringified object + createIncidentJson: schema.string(), // stringified object + createIncidentResponseKey: schema.string(), + getIncidentUrl: schema.string(), + getIncidentResponseExternalTitleKey: schema.string(), + getIncidentResponseExternalUrlKey: schema.string(), + updateIncidentUrl: schema.string(), + updateIncidentMethod: schema.oneOf( + [schema.literal(CasesWebhookMethods.POST), schema.literal(CasesWebhookMethods.PUT)], + { + defaultValue: CasesWebhookMethods.POST, + } + ), + updateIncidentJson: schema.string(), + createCommentUrl: schema.string(), + createCommentMethod: schema.oneOf( + [schema.literal(CasesWebhookMethods.POST), schema.literal(CasesWebhookMethods.PUT)], + { + defaultValue: CasesWebhookMethods.POST, + } + ), + createCommentJson: schema.string(), headers: nullableType(HeadersSchema), hasAuth: schema.boolean({ defaultValue: true }), }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts index 3bd122f124eb7c..f70497c6548c8d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts @@ -8,7 +8,6 @@ import axios, { AxiosError, AxiosResponse } from 'axios'; import { Logger } from '@kbn/core/server'; -import { ActionTypeExecutorResult } from '../../../common'; import { isOk, promiseResult, Result } from '../lib/result_type'; import { CreateIncidentParams, @@ -17,6 +16,7 @@ import { ExternalService, CasesWebhookPublicConfigurationType, CasesWebhookSecretConfigurationType, + ExternalServiceIncidentResponse, } from './types'; import * as i18n from './translations'; @@ -29,13 +29,15 @@ export const createExternalService = ( logger: Logger, configurationUtilities: ActionsConfigurationUtilities ): ExternalService => { - const { url, incidentJson } = config as CasesWebhookPublicConfigurationType; + const { createIncidentUrl, createIncidentJson } = config as CasesWebhookPublicConfigurationType; const { password, user } = secrets as CasesWebhookSecretConfigurationType; - if (!url || !password || !user) { + if (!createIncidentUrl || !password || !user) { throw Error(`[Action]${i18n.NAME}: Wrong configuration.`); } - const incidentUrl = url.endsWith('/') ? url.slice(0, -1) : url; + const incidentUrl = createIncidentUrl.endsWith('/') + ? createIncidentUrl.slice(0, -1) + : createIncidentUrl; const axiosInstance = axios.create({ auth: { username: user, password }, @@ -61,47 +63,7 @@ export const createExternalService = ( return msg; }, ''); }; - // - // const hasSupportForNewAPI = (capabilities: { capabilities?: {} }) => - // createMetaCapabilities.every((c) => Object.keys(capabilities?.capabilities ?? {}).includes(c)); - // - // const normalizeIssueTypes = (issueTypes: Array<{ id: string; name: string }>) => - // issueTypes.map((type) => ({ id: type.id, name: type.name })); - // - // const normalizeFields = (fields: { - // [key: string]: { - // allowedValues?: Array<{}>; - // defaultValue?: {}; - // name: string; - // required: boolean; - // schema: FieldSchema; - // }; - // }) => - // Object.keys(fields ?? {}).reduce( - // (fieldsAcc, fieldKey) => ({ - // ...fieldsAcc, - // [fieldKey]: { - // required: fields[fieldKey]?.required, - // allowedValues: fields[fieldKey]?.allowedValues ?? [], - // defaultValue: fields[fieldKey]?.defaultValue ?? {}, - // schema: fields[fieldKey]?.schema, - // name: fields[fieldKey]?.name, - // }, - // }), - // {} - // ); - // - // const normalizeSearchResults = ( - // issues: Array<{ id: string; key: string; fields: { summary: string } }> - // ) => - // issues.map((issue) => ({ id: issue.id, key: issue.key, title: issue.fields?.summary ?? null })); - // - // const normalizeIssue = (issue: { id: string; key: string; fields: { summary: string } }) => ({ - // id: issue.id, - // key: issue.key, - // title: issue.fields?.summary ?? null, - // }); - // + const getIncident = async (id: string) => { try { const res = await request({ @@ -132,13 +94,15 @@ export const createExternalService = ( }; const replaceSumDesc = (sum: string, desc: string) => { - let str = incidentJson; // incident is stringified object + let str = createIncidentJson; // incident is stringified object str = str.replace('$SUM', sum); str = str.replace('$DESC', desc); return JSON.parse(str); }; - const createIncident = async ({ incident }: CreateIncidentParams): Promise => { + const createIncident = async ({ + incident, + }: CreateIncidentParams): Promise => { const { summary, description } = incident; const data = replaceSumDesc(summary, description ?? ''); console.log('cases webhook data!!', { @@ -501,7 +465,3 @@ export const createExternalService = ( // getIssue, }; }; -// Action Executor Result w/ internationalisation -function successResult(actionId: string, data: unknown): ActionTypeExecutorResult { - return { status: 'ok', data, actionId }; -} diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts index 80de70cf63c90a..25da3a03edaeaa 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts @@ -17,14 +17,15 @@ const validateConfig = ( configurationUtilities: ActionsConfigurationUtilities, configObject: CasesWebhookPublicConfigurationType ) => { - const configuredUrl = configObject.url; + const createIncidentUrl = configObject.createIncidentUrl; try { - new URL(configuredUrl); + new URL(createIncidentUrl); } catch (err) { return i18n.translate( 'xpack.actions.builtin.casesWebhook.casesWebhookConfigurationErrorNoHostname', { - defaultMessage: 'error configuring cases webhook action: unable to parse url: {err}', + defaultMessage: + 'error configuring cases webhook action: unable to parse createIncidentUrl: {err}', values: { err, }, @@ -33,7 +34,7 @@ const validateConfig = ( } try { - configurationUtilities.ensureUriAllowed(configuredUrl); + configurationUtilities.ensureUriAllowed(createIncidentUrl); } catch (allowListError) { return i18n.translate('xpack.actions.builtin.casesWebhook.casesWebhookConfigurationError', { defaultMessage: 'error configuring cases webhook action: {message}', 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 07131deae50a71..af94c79e463097 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 @@ -126,11 +126,7 @@ async function executor( } if (subAction === 'pushToService') { const pushToServiceParams = subActionParams as ExecutorSubActionPushParams; - console.log('api push to service', { - externalService, - params: pushToServiceParams, - logger, - }); + data = await api.pushToService({ externalService, params: pushToServiceParams, 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 60877b2c0ee62b..eb2f540deaa9ad 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 @@ -17,8 +17,8 @@ export const ExternalIncidentServiceConfigurationSchema = schema.object( ); export const ExternalIncidentServiceSecretConfiguration = { - user: schema.string(), - password: schema.string(), + email: schema.string(), + apiToken: schema.string(), }; export const ExternalIncidentServiceSecretConfigurationSchema = schema.object( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts index 5a1c66e73ba674..7eee8d22839af8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts @@ -7,30 +7,96 @@ import { i18n } from '@kbn/i18n'; -export const URL_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.error.requiredUrlText', +export const CREATE_URL_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.error.requiredCreateUrlText', { - defaultMessage: 'URL is required.', + defaultMessage: 'Create incident URL is required.', } ); -export const INCIDENT_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.error.requiredIncidentText', +export const CREATE_INCIDENT_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.error.requiredCreateIncidentText', { - defaultMessage: 'Incident object is required.', + defaultMessage: 'Create incident object is required.', } ); -export const URL_INVALID = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.error.invalidUrlTextField', +export const CREATE_METHOD_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.sections.addAction.casesWebhookAction.error.requiredCreateMethodText', { - defaultMessage: 'URL is invalid.', + defaultMessage: 'Create incident method is required.', + } +); + +export const CREATE_RESPONSE_KEY_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.sections.addAction.casesWebhookAction.error.requiredCreateIncidentResponseKeyText', + { + defaultMessage: 'Create incident response incident key is required.', + } +); + +export const UPDATE_URL_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.error.requiredUpdateUrlText', + { + defaultMessage: 'Update incident URL is required.', + } +); +export const UPDATE_INCIDENT_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.error.requiredUpdateIncidentText', + { + defaultMessage: 'Update incident object is required.', + } +); + +export const UPDATE_METHOD_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.sections.addAction.casesWebhookAction.error.requiredUpdateMethodText', + { + defaultMessage: 'Update incident method is required.', + } +); + +export const CREATE_COMMENT_URL_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.error.requiredCreateCommentUrlText', + { + defaultMessage: 'Create comment incident URL is required.', + } +); +export const CREATE_COMMENT_INCIDENT_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.error.requiredCreateCommentIncidentText', + { + defaultMessage: 'Create comment incident object is required.', } ); -export const METHOD_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.sections.addAction.casesWebhookAction.error.requiredMethodText', +export const CREATE_COMMENT_METHOD_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.sections.addAction.casesWebhookAction.error.requiredCreateCommentMethodText', + { + defaultMessage: 'Create comment method is required.', + } +); + +export const GET_INCIDENT_URL_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.error.requiredGetIncidentUrlText', + { + defaultMessage: 'Get incident URL is required.', + } +); +export const GET_RESPONSE_EXTERNAL_TITLE_KEY_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.sections.addAction.casesWebhookAction.error.requiredGetIncidentResponseExternalTitleKeyText', { - defaultMessage: 'Method is required.', + defaultMessage: 'Get incident response external incident title key.', + } +); +export const GET_RESPONSE_EXTERNAL_URL_KEY_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.sections.addAction.casesWebhookAction.error.requiredGetIncidentResponseExternalTitleKeyText', + { + defaultMessage: 'Get incident response external incident URL key.', + } +); + +export const URL_INVALID = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.error.invalidUrlTextField', + { + defaultMessage: 'URL is invalid.', } ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/types.ts index 807a1190f7a8fe..b1867c28263e47 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/types.ts @@ -7,7 +7,11 @@ /* eslint-disable @kbn/eslint/no-restricted-paths */ -import { ExecutorSubActionPushParams } from '@kbn/actions-plugin/server/builtin_action_types/cases_webhook/types'; +import { + CasesWebhookPublicConfigurationType, + CasesWebhookSecretConfigurationType, + ExecutorSubActionPushParams, +} from '@kbn/actions-plugin/server/builtin_action_types/cases_webhook/types'; import { UserConfiguredActionConnector } from '../../../../types'; export interface CasesWebhookActionParams { @@ -15,18 +19,9 @@ export interface CasesWebhookActionParams { subActionParams: ExecutorSubActionPushParams; } -export interface CasesWebhookConfig { - method: string; - url: string; - incidentJson: string; - headers: Record; - hasAuth: boolean; -} +export type CasesWebhookConfig = CasesWebhookPublicConfigurationType; -export interface CasesWebhookSecrets { - user: string; - password: string; -} +export type CasesWebhookSecrets = CasesWebhookSecretConfigurationType; export type CasesWebhookActionConnector = UserConfiguredActionConnector< CasesWebhookConfig, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx index 0543f5d9e682d7..f5f2c9533675ab 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx @@ -44,13 +44,26 @@ export function getActionType(): ActionTypeModel< validateConnector: async ( action: CasesWebhookActionConnector ): Promise< - ConnectorValidationResult, CasesWebhookSecrets> + ConnectorValidationResult< + Omit, + CasesWebhookSecrets + > > => { const translations = await import('./translations'); const configErrors = { - url: new Array(), - method: new Array(), - incidentJson: new Array(), + createCommentJson: new Array(), + createCommentMethod: new Array(), + createCommentUrl: new Array(), + createIncidentJson: new Array(), + createIncidentMethod: new Array(), + createIncidentResponseKey: new Array(), + createIncidentUrl: new Array(), + getIncidentResponseExternalTitleKey: new Array(), + getIncidentResponseExternalUrlKey: new Array(), + getIncidentUrl: new Array(), + updateIncidentJson: new Array(), + updateIncidentMethod: new Array(), + updateIncidentUrl: new Array(), }; const secretsErrors = { user: new Array(), @@ -60,17 +73,66 @@ export function getActionType(): ActionTypeModel< config: { errors: configErrors }, secrets: { errors: secretsErrors }, }; - if (!action.config.url) { - configErrors.url.push(translations.URL_REQUIRED); + if (!action.config.createIncidentUrl) { + configErrors.createIncidentUrl.push(translations.CREATE_URL_REQUIRED); } - if (action.config.url && !isValidUrl(action.config.url)) { - configErrors.url = [...configErrors.url, translations.URL_INVALID]; + if (action.config.createIncidentUrl && !isValidUrl(action.config.createIncidentUrl)) { + configErrors.createIncidentUrl = [ + ...configErrors.createIncidentUrl, + translations.URL_INVALID, + ]; } - if (!action.config.incidentJson) { - configErrors.incidentJson.push(translations.INCIDENT_REQUIRED); + if (!action.config.createIncidentJson) { + configErrors.createIncidentJson.push(translations.CREATE_INCIDENT_REQUIRED); } - if (!action.config.method) { - configErrors.method.push(translations.METHOD_REQUIRED); + if (!action.config.createIncidentMethod) { + configErrors.createIncidentMethod.push(translations.CREATE_METHOD_REQUIRED); + } + if (!action.config.createIncidentResponseKey) { + configErrors.createIncidentResponseKey.push(translations.CREATE_RESPONSE_KEY_REQUIRED); + } + if (!action.config.getIncidentUrl) { + configErrors.getIncidentUrl.push(translations.GET_INCIDENT_URL_REQUIRED); + } + if (!action.config.getIncidentResponseExternalTitleKey) { + configErrors.getIncidentResponseExternalTitleKey.push( + translations.GET_RESPONSE_EXTERNAL_TITLE_KEY_REQUIRED + ); + } + if (!action.config.getIncidentResponseExternalUrlKey) { + configErrors.getIncidentResponseExternalUrlKey.push( + translations.GET_RESPONSE_EXTERNAL_URL_KEY_REQUIRED + ); + } + if (!action.config.updateIncidentUrl) { + configErrors.updateIncidentUrl.push(translations.UPDATE_URL_REQUIRED); + } + if (action.config.updateIncidentUrl && !isValidUrl(action.config.updateIncidentUrl)) { + configErrors.updateIncidentUrl = [ + ...configErrors.updateIncidentUrl, + translations.URL_INVALID, + ]; + } + if (!action.config.updateIncidentJson) { + configErrors.updateIncidentJson.push(translations.UPDATE_INCIDENT_REQUIRED); + } + if (!action.config.updateIncidentMethod) { + configErrors.updateIncidentMethod.push(translations.UPDATE_METHOD_REQUIRED); + } + if (!action.config.createCommentUrl) { + configErrors.createCommentUrl.push(translations.CREATE_COMMENT_URL_REQUIRED); + } + if (action.config.createCommentUrl && !isValidUrl(action.config.createCommentUrl)) { + configErrors.createCommentUrl = [ + ...configErrors.createCommentUrl, + translations.URL_INVALID, + ]; + } + if (!action.config.createCommentJson) { + configErrors.createCommentJson.push(translations.CREATE_COMMENT_INCIDENT_REQUIRED); + } + if (!action.config.createCommentMethod) { + configErrors.createCommentMethod.push(translations.CREATE_COMMENT_METHOD_REQUIRED); } if (action.config.hasAuth && !action.secrets.user && !action.secrets.password) { secretsErrors.user.push(translations.USERNAME_REQUIRED); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx index 6cd0e5a049f2d5..f4846f5d2c30bf 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { @@ -26,7 +26,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ActionConnectorFieldsProps } from '../../../../types'; -import { CasesWebhookActionConnector } from './types'; +import { CasesWebhookActionConnector, CasesWebhookConfig, CasesWebhookSecrets } from './types'; import { getEncryptedFieldNotifyLabel } from '../../get_encrypted_field_notify_label'; import { JsonEditorWithMessageVariables } from '../../json_editor_with_message_variables'; @@ -36,7 +36,23 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< ActionConnectorFieldsProps > = ({ action, editActionConfig, editActionSecrets, errors, readOnly }) => { const { user, password } = action.secrets; - const { method, url, incidentJson, headers, hasAuth } = action.config; + const { + createCommentJson, + createCommentMethod, + createCommentUrl, + createIncidentJson, + createIncidentMethod, + createIncidentResponseKey, + createIncidentUrl, + getIncidentResponseExternalTitleKey, + getIncidentResponseExternalUrlKey, + getIncidentUrl, + hasAuth, + headers, + updateIncidentJson, + updateIncidentMethod, + updateIncidentUrl, + } = action.config; const [httpHeaderKey, setHttpHeaderKey] = useState(''); const [httpHeaderValue, setHttpHeaderValue] = useState(''); @@ -49,8 +65,8 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - if (!method) { - editActionConfig('method', 'post'); // set method to POST by default + if (!createIncidentMethod) { + editActionConfig('createIncidentMethod', 'post'); // set createIncidentMethod to POST by default } const headerErrors = { @@ -103,12 +119,15 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< } function removeHeader(keyToRemove: string) { - const updatedHeaders = Object.keys(headers) - .filter((key) => key !== keyToRemove) - .reduce((headerToRemove: Record, key: string) => { - headerToRemove[key] = headers[key]; - return headerToRemove; - }, {}); + const updatedHeaders = + headers != null + ? Object.keys(headers) + .filter((key) => key !== keyToRemove) + .reduce((headerToRemove: Record, key: string) => { + headerToRemove[key] = headers[key]; + return headerToRemove; + }, {}) + : {}; editActionConfig('headers', updatedHeaders); } @@ -183,7 +202,7 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< addHeader()} + onClick={addHeader} > {key} - {headers[key]} + {headers && headers[key]} ); }); - - const isUrlInvalid: boolean = - errors.url !== undefined && errors.url.length > 0 && url !== undefined; - const isPasswordInvalid: boolean = - password !== undefined && errors.password !== undefined && errors.password.length > 0; - const isUserInvalid: boolean = - user !== undefined && errors.user !== undefined && errors.user.length > 0; - - const isIncidentJsonInvalid: boolean = - errors.incidentJson !== undefined && - errors.incidentJson.length > 0 && - incidentJson !== undefined; + const isConfigKeyValueInvalid = useCallback( + (key: keyof CasesWebhookConfig): boolean => + errors[key] !== undefined && errors[key].length > 0 && action.config[key] !== undefined, + [action.config, errors] + ); + const isSecretsKeyValueInvalid = useCallback( + (key: keyof CasesWebhookSecrets): boolean => + errors[key] !== undefined && errors[key].length > 0 && action.secrets[key] !== undefined, + [action.secrets, errors] + ); return ( <> + {/* start CREATE INCIDENT INPUTS */} ({ text: verb.toUpperCase(), value: verb }))} + onChange={(e) => { + editActionConfig('createIncidentMethod', e.target.value); + }} + /> + + + + + { + editActionConfig('createIncidentUrl', e.target.value); + }} + onBlur={() => { + if (!createIncidentUrl) { + editActionConfig('createIncidentUrl', ''); + } + }} + /> + + + + + + + { + editActionConfig('createIncidentJson', json); + }} + onBlur={() => { + if (!createIncidentJson) { + editActionConfig('createIncidentJson', ''); + } + }} + /> + + + + + + + { + editActionConfig('createIncidentResponseKey', e.target.value); + }} + onBlur={() => { + if (!createIncidentResponseKey) { + editActionConfig('createIncidentResponseKey', ''); + } + }} + /> + + + + {/* end CREATE INCIDENT INPUTS */} + {/* start GET INCIDENT INPUTS */} + + + + { + editActionConfig('getIncidentUrl', e.target.value); + }} + onBlur={() => { + if (!getIncidentUrl) { + editActionConfig('getIncidentUrl', ''); + } + }} + /> + + + + + { + editActionConfig('getIncidentResponseExternalTitleKey', e.target.value); + }} + onBlur={() => { + if (!getIncidentResponseExternalTitleKey) { + editActionConfig('getIncidentResponseExternalTitleKey', ''); + } + }} + /> + + + + + { + editActionConfig('getIncidentResponseExternalUrlKey', e.target.value); + }} + onBlur={() => { + if (!getIncidentResponseExternalUrlKey) { + editActionConfig('getIncidentResponseExternalUrlKey', ''); + } + }} + /> + + + + {/* end GET INCIDENT INPUTS */} + {/* start UPDATE INCIDENT INPUTS */} + + + + ({ text: verb.toUpperCase(), value: verb }))} + onChange={(e) => { + editActionConfig('updateIncidentMethod', e.target.value); + }} + /> + + + + + { + editActionConfig('updateIncidentUrl', e.target.value); + }} + onBlur={() => { + if (!updateIncidentUrl) { + editActionConfig('updateIncidentUrl', ''); + } + }} + /> + + + + + + + { + editActionConfig('updateIncidentJson', json); + }} + onBlur={() => { + if (!updateIncidentJson) { + editActionConfig('updateIncidentJson', ''); + } + }} + /> + + + + {/* end UPDATE INCIDENT INPUTS */} + {/* start CREATE COMMENT INCIDENT INPUTS */} + + + + ({ text: verb.toUpperCase(), value: verb }))} onChange={(e) => { - editActionConfig('method', e.target.value); + editActionConfig('createCommentMethod', e.target.value); }} /> { - editActionConfig('url', e.target.value); + editActionConfig('createCommentUrl', e.target.value); }} onBlur={() => { - if (!url) { - editActionConfig('url', ''); + if (!createCommentUrl) { + editActionConfig('createCommentUrl', ''); } }} /> @@ -295,27 +672,26 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< { - editActionConfig('incidentJson', json); + editActionConfig('createCommentJson', json); }} onBlur={() => { - if (!incidentJson) { - editActionConfig('incidentJson', ''); + if (!createCommentJson) { + editActionConfig('createCommentJson', ''); } }} /> + {/* end CREATE COMMENT INCIDENT INPUTS */} @@ -392,7 +769,7 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< id="webhookUser" fullWidth error={errors.user} - isInvalid={isUserInvalid} + isInvalid={isSecretsKeyValueInvalid('user')} label={i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.userTextFieldLabel', { @@ -402,7 +779,7 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< > { @@ -463,7 +840,7 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< } )} checked={hasHeaders} - onChange={() => viewHeaders()} + onChange={viewHeaders} /> From 8067c34b59739911097a5a6d6f0d2240aa09a358 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 19 May 2022 12:15:31 -0600 Subject: [PATCH 13/97] add in new fields --- .../cases_webhook/schema.ts | 14 +++-- .../cases_webhook/types.ts | 1 + .../cases_webhook/translations.ts | 6 +- .../cases_webhook/webhook.tsx | 8 +-- .../cases_webhook/webhook_connectors.tsx | 61 +++++++++++++------ 5 files changed, 57 insertions(+), 33 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts index fd9eb27dad368b..5be732518c8f69 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts @@ -23,20 +23,24 @@ export const ExternalIncidentServiceConfiguration = { createIncidentResponseKey: schema.string(), getIncidentUrl: schema.string(), getIncidentResponseExternalTitleKey: schema.string(), - getIncidentResponseExternalUrlKey: schema.string(), + getIncidentViewUrl: schema.string(), updateIncidentUrl: schema.string(), updateIncidentMethod: schema.oneOf( - [schema.literal(CasesWebhookMethods.POST), schema.literal(CasesWebhookMethods.PUT)], + [schema.literal(CasesWebhookMethods.POST), schema.literal(CasesWebhookMethods.PATCH)], { - defaultValue: CasesWebhookMethods.POST, + defaultValue: CasesWebhookMethods.PATCH, } ), updateIncidentJson: schema.string(), createCommentUrl: schema.string(), createCommentMethod: schema.oneOf( - [schema.literal(CasesWebhookMethods.POST), schema.literal(CasesWebhookMethods.PUT)], + [ + schema.literal(CasesWebhookMethods.POST), + schema.literal(CasesWebhookMethods.PUT), + schema.literal(CasesWebhookMethods.PATCH), + ], { - defaultValue: CasesWebhookMethods.POST, + defaultValue: CasesWebhookMethods.PATCH, } ), createCommentJson: schema.string(), diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts index 4b64c65a7a3bc6..138a5f3f6eafc1 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts @@ -17,6 +17,7 @@ import { // config definition export const enum CasesWebhookMethods { + PATCH = 'patch', POST = 'post', PUT = 'put', } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts index 7eee8d22839af8..1d25c403db8101 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts @@ -86,10 +86,10 @@ export const GET_RESPONSE_EXTERNAL_TITLE_KEY_REQUIRED = i18n.translate( defaultMessage: 'Get incident response external incident title key.', } ); -export const GET_RESPONSE_EXTERNAL_URL_KEY_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.sections.addAction.casesWebhookAction.error.requiredGetIncidentResponseExternalTitleKeyText', +export const GET_INCIDENT_VIEW_URL = i18n.translate( + 'xpack.triggersActionsUI.sections.addAction.casesWebhookAction.error.requiredGetIncidentViewUrlKeyText', { - defaultMessage: 'Get incident response external incident URL key.', + defaultMessage: 'Get external incident view URL.', } ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx index f5f2c9533675ab..f69b9f653531b4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx @@ -59,7 +59,7 @@ export function getActionType(): ActionTypeModel< createIncidentResponseKey: new Array(), createIncidentUrl: new Array(), getIncidentResponseExternalTitleKey: new Array(), - getIncidentResponseExternalUrlKey: new Array(), + getIncidentViewUrl: new Array(), getIncidentUrl: new Array(), updateIncidentJson: new Array(), updateIncidentMethod: new Array(), @@ -99,10 +99,8 @@ export function getActionType(): ActionTypeModel< translations.GET_RESPONSE_EXTERNAL_TITLE_KEY_REQUIRED ); } - if (!action.config.getIncidentResponseExternalUrlKey) { - configErrors.getIncidentResponseExternalUrlKey.push( - translations.GET_RESPONSE_EXTERNAL_URL_KEY_REQUIRED - ); + if (!action.config.getIncidentViewUrl) { + configErrors.getIncidentViewUrl.push(translations.GET_INCIDENT_VIEW_URL); } if (!action.config.updateIncidentUrl) { configErrors.updateIncidentUrl.push(translations.UPDATE_URL_REQUIRED); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx index f4846f5d2c30bf..c1b462a26d5b79 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx @@ -45,7 +45,7 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< createIncidentResponseKey, createIncidentUrl, getIncidentResponseExternalTitleKey, - getIncidentResponseExternalUrlKey, + getIncidentViewUrl, getIncidentUrl, hasAuth, headers, @@ -413,6 +413,13 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< defaultMessage: 'Get Incident URL', } )} + helpText={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.getIncidentUrlTextFieldLabel', + { + defaultMessage: + 'API URL to GET incident details JSON from external system. Withold the ID from the URL as Kibana will dynamically insert the id based on the provided Create Incident Response Incident Key.', + } + )} > { - editActionConfig('getIncidentResponseExternalUrlKey', e.target.value); + editActionConfig('getIncidentViewUrl', e.target.value); }} onBlur={() => { - if (!getIncidentResponseExternalUrlKey) { - editActionConfig('getIncidentResponseExternalUrlKey', ''); + if (!getIncidentViewUrl) { + editActionConfig('getIncidentViewUrl', ''); } }} /> @@ -523,7 +530,7 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< > ({ text: verb.toUpperCase(), value: verb }))} @@ -545,6 +552,13 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< defaultMessage: 'Update Incident URL', } )} + helpText={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.updateIncidentUrlTextFieldLabel', + { + defaultMessage: + 'API URL to update incident. Withold the ID from the URL as Kibana will dynamically insert the id based on the provided Create Incident Response Incident Key.', + } + )} > ({ text: verb.toUpperCase(), value: verb }))} @@ -646,7 +660,14 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< label={i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.createCommentUrlTextFieldLabel', { - defaultMessage: 'Create Comment Incident URL', + defaultMessage: 'Create Comment URL', + } + )} + helpText={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.createCommentUrlTextFieldLabel', + { + defaultMessage: + 'API URL to add comment to incident. Withold the ID from the URL as Kibana will dynamically insert the id based on the provided Create Incident Response Incident Key.', } )} > @@ -678,7 +699,7 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< label={i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.createCommentJsonTextFieldLabel', { - defaultMessage: 'Create Comment Incident Object', + defaultMessage: 'Create Comment Object', } )} helpText={i18n.translate( From c95868554cb7212bbdb9fa89aa52a7d74e169949 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 19 May 2022 13:20:44 -0600 Subject: [PATCH 14/97] fix cases push --- .../builtin_action_types/cases_webhook/schema.ts | 10 +++++++--- .../server/connectors/cases_webook/format.ts | 9 --------- .../server/connectors/cases_webook/mapping.ts | 16 +--------------- .../cases_webhook/webhook_connectors.tsx | 16 ++++++++++++---- 4 files changed, 20 insertions(+), 31 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts index 5be732518c8f69..0aff9db8010788 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts @@ -26,9 +26,13 @@ export const ExternalIncidentServiceConfiguration = { getIncidentViewUrl: schema.string(), updateIncidentUrl: schema.string(), updateIncidentMethod: schema.oneOf( - [schema.literal(CasesWebhookMethods.POST), schema.literal(CasesWebhookMethods.PATCH)], + [ + schema.literal(CasesWebhookMethods.POST), + schema.literal(CasesWebhookMethods.PATCH), + schema.literal(CasesWebhookMethods.PUT), + ], { - defaultValue: CasesWebhookMethods.PATCH, + defaultValue: CasesWebhookMethods.PUT, } ), updateIncidentJson: schema.string(), @@ -40,7 +44,7 @@ export const ExternalIncidentServiceConfiguration = { schema.literal(CasesWebhookMethods.PATCH), ], { - defaultValue: CasesWebhookMethods.PATCH, + defaultValue: CasesWebhookMethods.PUT, } ), createCommentJson: schema.string(), diff --git a/x-pack/plugins/cases/server/connectors/cases_webook/format.ts b/x-pack/plugins/cases/server/connectors/cases_webook/format.ts index 3508c19d75eb78..8f86f1218a3c0b 100644 --- a/x-pack/plugins/cases/server/connectors/cases_webook/format.ts +++ b/x-pack/plugins/cases/server/connectors/cases_webook/format.ts @@ -5,20 +5,11 @@ * 2.0. */ -import { ConnectorCasesWebhookTypeFields } from '../../../common/api'; import { Format } from './types'; export const format: Format = (theCase, alerts) => { - const { - priority = null, - issueType = null, - parent = null, - } = (theCase.connector.fields as ConnectorCasesWebhookTypeFields['fields']) ?? {}; return { - priority, // CasesWebook do not allows empty spaces on labels. We replace white spaces with hyphens labels: theCase.tags.map((tag) => tag.replace(/\s+/g, '-')), - issueType, - parent, }; }; diff --git a/x-pack/plugins/cases/server/connectors/cases_webook/mapping.ts b/x-pack/plugins/cases/server/connectors/cases_webook/mapping.ts index 8f8a914b4e091a..4cdd0ad548a7bf 100644 --- a/x-pack/plugins/cases/server/connectors/cases_webook/mapping.ts +++ b/x-pack/plugins/cases/server/connectors/cases_webook/mapping.ts @@ -9,20 +9,6 @@ import { GetMapping } from './types'; export const getMapping: GetMapping = () => { return [ - { - source: 'title', - target: 'summary', - action_type: 'overwrite', - }, - { - source: 'description', - target: 'description', - action_type: 'overwrite', - }, - { - source: 'comments', - target: 'comments', - action_type: 'append', - }, + // To do?? ]; }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx index c1b462a26d5b79..5db3f2bd60b908 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx @@ -62,13 +62,21 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< if (!action.id) { editActionConfig('hasAuth', true); } + + if (!createIncidentMethod) { + editActionConfig('createIncidentMethod', 'post'); // set createIncidentMethod to POST by default + } + + if (!updateIncidentMethod) { + editActionConfig('updateIncidentMethod', 'put'); // set updateIncidentMethod to PUT by default + } + + if (!createCommentMethod) { + editActionConfig('createCommentMethod', 'put'); // set createCommentMethod to PUT by default + } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - if (!createIncidentMethod) { - editActionConfig('createIncidentMethod', 'post'); // set createIncidentMethod to POST by default - } - const headerErrors = { keyHeader: new Array(), valueHeader: new Array(), From 2e8a8997896f067c078638d900a40c4e0af954a1 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 19 May 2022 14:49:26 -0600 Subject: [PATCH 15/97] add patch to fe --- .../builtin_action_types/cases_webhook/webhook_connectors.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx index 5db3f2bd60b908..dea7f904fa4428 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx @@ -30,7 +30,7 @@ import { CasesWebhookActionConnector, CasesWebhookConfig, CasesWebhookSecrets } import { getEncryptedFieldNotifyLabel } from '../../get_encrypted_field_notify_label'; import { JsonEditorWithMessageVariables } from '../../json_editor_with_message_variables'; -const HTTP_VERBS = ['post', 'put']; +const HTTP_VERBS = ['post', 'put', 'patch']; const CasesWebhookActionConnectorFields: React.FunctionComponent< ActionConnectorFieldsProps From fe108a58641c1d606d5e92ecb6212dacd69341b9 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Fri, 20 May 2022 14:11:12 -0600 Subject: [PATCH 16/97] make link back url work --- .../cases_webhook/schema.ts | 4 +- .../cases_webhook/service.ts | 173 ++++++++---------- .../cases_webhook/types.ts | 10 +- .../cases_webhook/utils.ts | 103 +++++++++++ .../public/components/all_cases/columns.tsx | 22 +-- .../public/components/user_actions/pushed.tsx | 16 +- .../plugins/cases/server/client/cases/push.ts | 60 +++--- .../server/connectors/cases_webook/format.ts | 2 + .../cases/server/services/cases/transform.ts | 7 - .../server/services/user_actions/index.ts | 7 - .../cases_webhook/translations.ts | 14 +- .../cases_webhook/webhook.tsx | 18 +- .../cases_webhook/webhook_connectors.tsx | 106 +++++++++-- 13 files changed, 354 insertions(+), 188 deletions(-) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts index 0aff9db8010788..6f0b0a56868fd9 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts @@ -22,8 +22,10 @@ export const ExternalIncidentServiceConfiguration = { createIncidentJson: schema.string(), // stringified object createIncidentResponseKey: schema.string(), getIncidentUrl: schema.string(), + getIncidentResponseCreatedDateKey: schema.string(), getIncidentResponseExternalTitleKey: schema.string(), - getIncidentViewUrl: schema.string(), + getIncidentResponseUpdatedDateKey: schema.string(), + incidentViewUrl: schema.string(), updateIncidentUrl: schema.string(), updateIncidentMethod: schema.oneOf( [ diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts index f70497c6548c8d..108fe8cc8cea7d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts @@ -5,22 +5,27 @@ * 2.0. */ -import axios, { AxiosError, AxiosResponse } from 'axios'; +import axios, { AxiosResponse } from 'axios'; import { Logger } from '@kbn/core/server'; -import { isOk, promiseResult, Result } from '../lib/result_type'; +import { + createServiceError, + getObjectValueByKey, + getPushedDate, + throwIfResponseIsNotValidSpecial, +} from './utils'; import { CreateIncidentParams, ExternalServiceCredentials, - ResponseError, ExternalService, CasesWebhookPublicConfigurationType, CasesWebhookSecretConfigurationType, ExternalServiceIncidentResponse, + GetIncidentResponse, } from './types'; import * as i18n from './translations'; -import { request, getErrorMessage, throwIfResponseIsNotValid } from '../lib/axios_utils'; +import { request } from '../lib/axios_utils'; import { ActionsConfigurationUtilities } from '../../actions_config'; export const createExternalService = ( @@ -29,67 +34,67 @@ export const createExternalService = ( logger: Logger, configurationUtilities: ActionsConfigurationUtilities ): ExternalService => { - const { createIncidentUrl, createIncidentJson } = config as CasesWebhookPublicConfigurationType; + const { + createIncidentResponseKey, + createIncidentUrl: createIncidentUrlConfig, + getIncidentUrl: getIncidentUrlConfig, + createIncidentJson, + getIncidentResponseCreatedDateKey, + getIncidentResponseExternalTitleKey, + getIncidentResponseUpdatedDateKey, + incidentViewUrl, + } = config as CasesWebhookPublicConfigurationType; const { password, user } = secrets as CasesWebhookSecretConfigurationType; - if (!createIncidentUrl || !password || !user) { + if (!getIncidentUrlConfig || !password || !user) { throw Error(`[Action]${i18n.NAME}: Wrong configuration.`); } - const incidentUrl = createIncidentUrl.endsWith('/') - ? createIncidentUrl.slice(0, -1) - : createIncidentUrl; + const createIncidentUrl = createIncidentUrlConfig.endsWith('/') + ? createIncidentUrlConfig.slice(0, -1) + : createIncidentUrlConfig; + + const getIncidentUrl = getIncidentUrlConfig.endsWith('/') + ? getIncidentUrlConfig.slice(0, -1) + : getIncidentUrlConfig; + + const getIncidentViewURL = (id: string) => + `${ + incidentViewUrl.endsWith('=') + ? incidentViewUrl + : incidentViewUrl.endsWith('/') + ? incidentViewUrl + : `${incidentViewUrl}/` + }${id}`; const axiosInstance = axios.create({ auth: { username: user, password }, }); - const createErrorMessage = (errorResponse: ResponseError | null | undefined): string => { - if (errorResponse == null) { - return 'unknown: errorResponse was null'; - } - - const { errorMessages, errors } = errorResponse; - - if (errors == null) { - return 'unknown: errorResponse.errors was null'; - } - - if (Array.isArray(errorMessages) && errorMessages.length > 0) { - return `${errorMessages.join(', ')}`; - } - - return Object.entries(errors).reduce((errorMessage, [, value]) => { - const msg = errorMessage.length > 0 ? `${errorMessage} ${value}` : value; - return msg; - }, ''); - }; - - const getIncident = async (id: string) => { + const getIncident = async (id: string): Promise => { try { const res = await request({ axios: axiosInstance, - url: `${incidentUrl}/${id}`, + url: `${getIncidentUrl}/${id}`, logger, configurationUtilities, }); - throwIfResponseIsNotValid({ + throwIfResponseIsNotValidSpecial({ res, - requiredAttributesToBeInTheResponse: ['id', 'key'], + requiredAttributesToBeInTheResponse: [ + getIncidentResponseCreatedDateKey, + getIncidentResponseExternalTitleKey, + getIncidentResponseUpdatedDateKey, + ], }); - const { fields, id: incidentId, key } = res.data; + const title = getObjectValueByKey(res.data, getIncidentResponseExternalTitleKey) as string; + const created = getObjectValueByKey(res.data, getIncidentResponseCreatedDateKey) as string; + const updated = getObjectValueByKey(res.data, getIncidentResponseUpdatedDateKey) as string; - return { id: incidentId, key, created: fields.created, updated: fields.updated, ...fields }; + return { id, title, created, updated }; } catch (error) { - throw new Error( - getErrorMessage( - i18n.NAME, - `Unable to get incident with id ${id}. Error: ${ - error.message - } Reason: ${createErrorMessage(error.response?.data)}` - ) - ); + throw createServiceError(error, 'Unable to get incident'); } }; @@ -104,59 +109,35 @@ export const createExternalService = ( incident, }: CreateIncidentParams): Promise => { const { summary, description } = incident; - const data = replaceSumDesc(summary, description ?? ''); - console.log('cases webhook data!!', { - data, - }); try { - const result: Result = await promiseResult( - request({ - axios: axiosInstance, - url: `${incidentUrl}`, - logger, - method: 'post', - data, - configurationUtilities, - }) - ); - console.log('it happened!!!', result); + const res: AxiosResponse = await request({ + axios: axiosInstance, + url: `${createIncidentUrl}`, + logger, + method: 'post', + data: replaceSumDesc(summary, description ?? ''), + configurationUtilities, + }); + + const { status, statusText, data } = res; + + throwIfResponseIsNotValidSpecial({ + res, + requiredAttributesToBeInTheResponse: [createIncidentResponseKey], + }); + const incidentId = getObjectValueByKey(data, createIncidentResponseKey) as string; + const insertedIncident = await getIncident(incidentId); - if (isOk(result)) { - const { - value: { status, statusText, data: data2 }, - } = result; - console.log('DATA', data2); - logger.debug(`response from webhook action "${actionId}": [HTTP ${status}] ${statusText}`); + logger.debug(`response from webhook action "${actionId}": [HTTP ${status}] ${statusText}`); - return data; // successResult(actionId, data); - } else { - const { error } = result; - if (error.response) { - const { - status, - statusText, - headers: responseHeaders, - data: { message: responseMessage }, - } = error.response; - const responseMessageAsSuffix = responseMessage ? `: ${responseMessage}` : ''; - const message = `[${status}] ${statusText}${responseMessageAsSuffix}`; - logger.error(`error on ${actionId} webhook event: ${message}`); - // The request was made and the server responded with a status code - // that falls out of the range of 2xx - // special handling for 5xx - return { actionId, message }; - } - } + return { + id: incidentId, + title: insertedIncident.title, + url: getIncidentViewURL(incidentId), + pushedDate: getPushedDate(insertedIncident.created), + }; } catch (error) { - console.log('ERROR', error.response); - throw new Error( - getErrorMessage( - i18n.NAME, - `Unable to create incident. Error: ${error.message}. Reason: ${createErrorMessage( - error.response?.data - )}` - ) - ); + throw createServiceError(error, 'Unable to create incident'); } }; @@ -175,7 +156,7 @@ export const createExternalService = ( // const res = await request({ // axios: axiosInstance, // method: 'put', - // url: `${incidentUrl}/${incidentId}`, + // url: `${createIncidentUrl}/${incidentId}`, // logger, // data: { fields }, // configurationUtilities, @@ -425,7 +406,7 @@ export const createExternalService = ( // }; // // const getIssue = async (id: string) => { - // const getIssueUrl = `${incidentUrl}/${id}`; + // const getIssueUrl = `${createIncidentUrl}/${id}`; // try { // const res = await request({ // axios: axiosInstance, @@ -454,7 +435,7 @@ export const createExternalService = ( return { // getFields, - // getIncident, + getIncident, createIncident, // updateIncident, // createComment, diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts index 138a5f3f6eafc1..7b1ac6857e0cfa 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts @@ -34,7 +34,7 @@ export type CasesWebhookSecretConfigurationType = TypeOf< export type CasesWebhookActionParamsType = TypeOf; export interface ExternalServiceCredentials { - config: Record; + config: Record; secrets: Record; } @@ -60,6 +60,7 @@ export type PushToServiceApiParams = ExecutorSubActionPushParams; // incident service export interface ExternalService { + getIncident: (id: string) => Promise; createIncident: (params: CreateIncidentParams) => Promise; } export interface CreateIncidentParams { @@ -77,6 +78,13 @@ export interface PushToServiceApiHandlerArgs extends ExternalServiceApiHandlerAr } export type PushToServiceResponse = ExternalServiceIncidentResponse; +export interface GetIncidentResponse { + id: string; + title: string; + created: string; + updated: string; +} + export interface ExternalServiceApi { pushToService: (args: PushToServiceApiHandlerArgs) => Promise; } diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts new file mode 100644 index 00000000000000..2cc30b2f9976e6 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AxiosResponse } from 'axios'; +import { isEmpty, isObjectLike } from 'lodash'; +import { addTimeZoneToDate, getErrorMessage } from '../lib/axios_utils'; +import * as i18n from './translations'; + +export const createServiceError = (error: any, message: string) => + new Error( + getErrorMessage( + i18n.NAME, + `${message}. Error: ${error.message} Reason: ${error.response?.data}` + ) + ); +export const getPushedDate = (timestamp?: string) => { + if (timestamp != null) { + return new Date(addTimeZoneToDate(timestamp)).toISOString(); + } + + return new Date().toISOString(); +}; + +const splitKeys = (key: string) => { + const split1 = key.split('.'); + const split2 = split1.reduce((acc: string[], k: string) => { + const newSplit = k.split('['); + return [...acc, ...newSplit.filter((j) => j !== '')]; + }, []); + return split2.reduce((acc: string[], k: string) => { + const newSplit = k.split(']'); + return [...acc, ...newSplit.filter((j) => j !== '')]; + }, []); +}; +const findTheValue = (obj: Record | unknown>, keys: string[]) => { + let currentLevel: unknown = obj; + keys.forEach((k: string) => { + // @ts-ignore + currentLevel = currentLevel[k]; + }); + return currentLevel; +}; + +export const getObjectValueByKey = ( + obj: Record | unknown>, + key: string +) => { + return findTheValue(obj, splitKeys(key)); +}; + +export const throwIfResponseIsNotValidSpecial = ({ + res, + requiredAttributesToBeInTheResponse = [], +}: { + res: AxiosResponse; + requiredAttributesToBeInTheResponse?: string[]; +}) => { + const requiredContentType = 'application/json'; + const contentType = res.headers['content-type'] ?? 'undefined'; + const data = res.data; + + /** + * Check that the content-type of the response is application/json. + * Then includes is added because the header can be application/json;charset=UTF-8. + */ + if (!contentType.includes(requiredContentType)) { + throw new Error( + `Unsupported content type: ${contentType} in ${res.config.method} ${res.config.url}. Supported content types: ${requiredContentType}` + ); + } + + /** + * Check if the response is a JS object (data != null && typeof data === 'object') + * in case the content type is application/json but for some reason the response is not. + * Empty responses (204 No content) are ignored because the typeof data will be string and + * isObjectLike will fail. + * Axios converts automatically JSON to JS objects. + */ + if (!isEmpty(data) && !isObjectLike(data)) { + throw new Error('Response is not a valid JSON'); + } + + if (requiredAttributesToBeInTheResponse.length > 0) { + const requiredAttributesError = (attr: string) => + new Error(`Response is missing at the expected field: ${attr}`); + + /** + * If the response is an array and requiredAttributesToBeInTheResponse + * are not empty then we thrown an error assuming that the consumer + * expects an object response and not an array. + */ + requiredAttributesToBeInTheResponse.forEach((attr) => { + // Check only for undefined as null is a valid value + if (getObjectValueByKey(data, attr) === undefined) { + throw requiredAttributesError(attr); + } + }); + } +}; diff --git a/x-pack/plugins/cases/public/components/all_cases/columns.tsx b/x-pack/plugins/cases/public/components/all_cases/columns.tsx index d81fca4d883633..4356b626b5944a 100644 --- a/x-pack/plugins/cases/public/components/all_cases/columns.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/columns.tsx @@ -438,19 +438,15 @@ export const ExternalServiceColumn: React.FC = ({ theCase, connectors }) /> )} - {theCase.externalService?.externalUrl !== 'replace_cases_webhook' ? ( - - {theCase.externalService?.externalTitle} - - ) : ( - `${theCase.externalService?.connectorName} incident` - )} + + {theCase.externalService?.externalTitle} + {hasDataToPush ? renderStringField(i18n.REQUIRES_UPDATE, `case-table-column-external-requiresUpdate`) diff --git a/x-pack/plugins/cases/public/components/user_actions/pushed.tsx b/x-pack/plugins/cases/public/components/user_actions/pushed.tsx index 13522f2bbbcacc..e02bde992b6517 100644 --- a/x-pack/plugins/cases/public/components/user_actions/pushed.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/pushed.tsx @@ -47,17 +47,11 @@ const getLabelTitle = (action: UserActionResponse, firstPush: externalService?.connectorName }`} - {externalService?.externalUrl !== 'replace_cases_webhook' && ( - - - {externalService?.externalTitle} - - - )} + + + {externalService?.externalTitle} + + ); }; diff --git a/x-pack/plugins/cases/server/client/cases/push.ts b/x-pack/plugins/cases/server/client/cases/push.ts index 4c0c514af4801e..8bcc295714f9bf 100644 --- a/x-pack/plugins/cases/server/client/cases/push.ts +++ b/x-pack/plugins/cases/server/client/cases/push.ts @@ -20,7 +20,6 @@ import { OWNER_FIELD, CommentType, CommentRequestAlertType, - ConnectorTypes, } from '../../../common/api'; import { CASE_COMMENT_SAVED_OBJECT } from '../../../common/constants'; @@ -157,7 +156,7 @@ export const push = async ( alerts, casesConnectors, }); - + console.log('externalServiceIncident', externalServiceIncident); const pushRes = await actionsClient.execute({ actionId: connector?.id ?? '', params: { @@ -165,6 +164,7 @@ export const push = async ( subActionParams: externalServiceIncident, }, }); + console.log('pushRes', pushRes); if (pushRes.status === 'error') { throw Boom.failedDependency( @@ -274,38 +274,30 @@ export const push = async ( /* End of update case with push information */ - const flattedCase = flattenCaseSavedObject({ - savedObject: { - ...myCase, - ...updatedCase, - attributes: { ...myCase.attributes, ...updatedCase?.attributes }, - references: myCase.references, - }, - comments: comments.saved_objects.map((origComment) => { - const updatedComment = updatedComments.saved_objects.find((c) => c.id === origComment.id); - return { - ...origComment, - ...updatedComment, - attributes: { - ...origComment.attributes, - ...updatedComment?.attributes, - ...getCommentContextFromAttributes(origComment.attributes), - }, - version: updatedComment?.version ?? origComment.version, - references: origComment?.references ?? [], - }; - }), - }); - if (flattedCase.connector.type === ConnectorTypes.casesWebhook) { - flattedCase.external_service = { - ...flattedCase.external_service, - external_id: 'replace_cases_webhook', - external_title: 'replace_cases_webhook', - external_url: 'replace_cases_webhook', - }; - } - - return CaseResponseRt.encode(flattedCase); + return CaseResponseRt.encode( + flattenCaseSavedObject({ + savedObject: { + ...myCase, + ...updatedCase, + attributes: { ...myCase.attributes, ...updatedCase?.attributes }, + references: myCase.references, + }, + comments: comments.saved_objects.map((origComment) => { + const updatedComment = updatedComments.saved_objects.find((c) => c.id === origComment.id); + return { + ...origComment, + ...updatedComment, + attributes: { + ...origComment.attributes, + ...updatedComment?.attributes, + ...getCommentContextFromAttributes(origComment.attributes), + }, + version: updatedComment?.version ?? origComment.version, + references: origComment?.references ?? [], + }; + }), + }) + ); } catch (error) { throw createCaseError({ message: `Failed to push case: ${error}`, error, logger }); } diff --git a/x-pack/plugins/cases/server/connectors/cases_webook/format.ts b/x-pack/plugins/cases/server/connectors/cases_webook/format.ts index 8f86f1218a3c0b..612de2e7e678fd 100644 --- a/x-pack/plugins/cases/server/connectors/cases_webook/format.ts +++ b/x-pack/plugins/cases/server/connectors/cases_webook/format.ts @@ -9,6 +9,8 @@ import { Format } from './types'; export const format: Format = (theCase, alerts) => { return { + summary: theCase.title, + description: theCase.description, // CasesWebook do not allows empty spaces on labels. We replace white spaces with hyphens labels: theCase.tags.map((tag) => tag.replace(/\s+/g, '-')), }; diff --git a/x-pack/plugins/cases/server/services/cases/transform.ts b/x-pack/plugins/cases/server/services/cases/transform.ts index d48dabd4426183..c013475bd2e9c6 100644 --- a/x-pack/plugins/cases/server/services/cases/transform.ts +++ b/x-pack/plugins/cases/server/services/cases/transform.ts @@ -201,12 +201,5 @@ function transformESExternalService( return { ...externalService, connector_id: connectorIdRef?.id ?? NONE_CONNECTOR_ID, - ...(externalService.external_url == null - ? { - external_id: 'replace_cases_webhook', - external_title: 'replace_cases_webhook', - external_url: 'replace_cases_webhook', - } - : {}), }; } diff --git a/x-pack/plugins/cases/server/services/user_actions/index.ts b/x-pack/plugins/cases/server/services/user_actions/index.ts index c02fb7833379bd..7739437f9e6078 100644 --- a/x-pack/plugins/cases/server/services/user_actions/index.ts +++ b/x-pack/plugins/cases/server/services/user_actions/index.ts @@ -586,13 +586,6 @@ const addReferenceIdToPayload = ( externalService: { ...userActionAttributes.payload.externalService, connector_id: connectorId ?? NONE_CONNECTOR_ID, - ...(userActionAttributes.payload.externalService.external_url == null - ? { - external_id: 'replace_cases_webhook', - external_title: 'replace_cases_webhook', - external_url: 'replace_cases_webhook', - } - : {}), }, }; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts index 1d25c403db8101..89d2c75ea7b03b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts @@ -83,7 +83,19 @@ export const GET_INCIDENT_URL_REQUIRED = i18n.translate( export const GET_RESPONSE_EXTERNAL_TITLE_KEY_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.sections.addAction.casesWebhookAction.error.requiredGetIncidentResponseExternalTitleKeyText', { - defaultMessage: 'Get incident response external incident title key.', + defaultMessage: 'Get incident response external incident title key is re quired.', + } +); +export const GET_RESPONSE_EXTERNAL_CREATED_KEY_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.sections.addAction.casesWebhookAction.error.requiredGetIncidentResponseCreatedKeyText', + { + defaultMessage: 'Get incident response created date key is required.', + } +); +export const GET_RESPONSE_EXTERNAL_UPDATED_KEY_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.sections.addAction.casesWebhookAction.error.requiredGetIncidentResponseUpdatedKeyText', + { + defaultMessage: 'Get incident response updated date key is required.', } ); export const GET_INCIDENT_VIEW_URL = i18n.translate( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx index f69b9f653531b4..b9a973fd05caad 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx @@ -58,8 +58,10 @@ export function getActionType(): ActionTypeModel< createIncidentMethod: new Array(), createIncidentResponseKey: new Array(), createIncidentUrl: new Array(), + getIncidentResponseCreatedDateKey: new Array(), getIncidentResponseExternalTitleKey: new Array(), - getIncidentViewUrl: new Array(), + getIncidentResponseUpdatedDateKey: new Array(), + incidentViewUrl: new Array(), getIncidentUrl: new Array(), updateIncidentJson: new Array(), updateIncidentMethod: new Array(), @@ -99,8 +101,18 @@ export function getActionType(): ActionTypeModel< translations.GET_RESPONSE_EXTERNAL_TITLE_KEY_REQUIRED ); } - if (!action.config.getIncidentViewUrl) { - configErrors.getIncidentViewUrl.push(translations.GET_INCIDENT_VIEW_URL); + if (!action.config.getIncidentResponseCreatedDateKey) { + configErrors.getIncidentResponseCreatedDateKey.push( + translations.GET_RESPONSE_EXTERNAL_CREATED_KEY_REQUIRED + ); + } + if (!action.config.getIncidentResponseUpdatedDateKey) { + configErrors.getIncidentResponseUpdatedDateKey.push( + translations.GET_RESPONSE_EXTERNAL_UPDATED_KEY_REQUIRED + ); + } + if (!action.config.incidentViewUrl) { + configErrors.incidentViewUrl.push(translations.GET_INCIDENT_VIEW_URL); } if (!action.config.updateIncidentUrl) { configErrors.updateIncidentUrl.push(translations.UPDATE_URL_REQUIRED); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx index dea7f904fa4428..b9b65b515d4f72 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx @@ -44,8 +44,10 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< createIncidentMethod, createIncidentResponseKey, createIncidentUrl, + getIncidentResponseCreatedDateKey, getIncidentResponseExternalTitleKey, - getIncidentViewUrl, + getIncidentResponseUpdatedDateKey, + incidentViewUrl, getIncidentUrl, hasAuth, headers, @@ -487,18 +489,94 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< + { + editActionConfig('getIncidentResponseCreatedDateKey', e.target.value); + }} + onBlur={() => { + if (!getIncidentResponseCreatedDateKey) { + editActionConfig('getIncidentResponseCreatedDateKey', ''); + } + }} + /> + + + + + { + editActionConfig('getIncidentResponseUpdatedDateKey', e.target.value); + }} + onBlur={() => { + if (!getIncidentResponseUpdatedDateKey) { + editActionConfig('getIncidentResponseUpdatedDateKey', ''); + } + }} + /> + + + + { - editActionConfig('getIncidentViewUrl', e.target.value); + editActionConfig('incidentViewUrl', e.target.value); }} onBlur={() => { - if (!getIncidentViewUrl) { - editActionConfig('getIncidentViewUrl', ''); + if (!incidentViewUrl) { + editActionConfig('incidentViewUrl', ''); } }} /> From db286dd56263155718916df4ac89233e54c0f15e Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Fri, 20 May 2022 14:12:50 -0600 Subject: [PATCH 17/97] type cleaning --- .../cases_webhook/service.ts | 225 +----------------- .../cases_webhook/utils.ts | 4 +- 2 files changed, 6 insertions(+), 223 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts index 108fe8cc8cea7d..b66850f19da5f7 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts @@ -88,9 +88,9 @@ export const createExternalService = ( ], }); - const title = getObjectValueByKey(res.data, getIncidentResponseExternalTitleKey) as string; - const created = getObjectValueByKey(res.data, getIncidentResponseCreatedDateKey) as string; - const updated = getObjectValueByKey(res.data, getIncidentResponseUpdatedDateKey) as string; + const title = getObjectValueByKey(res.data, getIncidentResponseExternalTitleKey); + const created = getObjectValueByKey(res.data, getIncidentResponseCreatedDateKey); + const updated = getObjectValueByKey(res.data, getIncidentResponseUpdatedDateKey); return { id, title, created, updated }; } catch (error) { @@ -125,7 +125,7 @@ export const createExternalService = ( res, requiredAttributesToBeInTheResponse: [createIncidentResponseKey], }); - const incidentId = getObjectValueByKey(data, createIncidentResponseKey) as string; + const incidentId = getObjectValueByKey(data, createIncidentResponseKey); const insertedIncident = await getIncident(incidentId); logger.debug(`response from webhook action "${actionId}": [HTTP ${status}] ${statusText}`); @@ -222,227 +222,10 @@ export const createExternalService = ( // } // }; - // const getCapabilities = async () => { - // try { - // const res = await request({ - // axios: axiosInstance, - // method: 'get', - // url: capabilitiesUrl, - // logger, - // configurationUtilities, - // }); - // - // throwIfResponseIsNotValid({ - // res, - // requiredAttributesToBeInTheResponse: ['capabilities'], - // }); - // - // return { ...res.data }; - // } catch (error) { - // throw new Error( - // getErrorMessage( - // i18n.NAME, - // `Unable to get capabilities. Error: ${error.message}. Reason: ${createErrorMessage( - // error.response?.data - // )}` - // ) - // ); - // } - // }; - // - // const getIssueTypes = async () => { - // const capabilitiesResponse = await getCapabilities(); - // const supportsNewAPI = hasSupportForNewAPI(capabilitiesResponse); - // try { - // if (!supportsNewAPI) { - // const res = await request({ - // axios: axiosInstance, - // method: 'get', - // url: getIssueTypesOldAPIURL, - // logger, - // configurationUtilities, - // }); - // - // throwIfResponseIsNotValid({ - // res, - // }); - // - // const issueTypes = res.data.projects[0]?.issuetypes ?? []; - // return normalizeIssueTypes(issueTypes); - // } else { - // const res = await request({ - // axios: axiosInstance, - // method: 'get', - // url: getIssueTypesUrl, - // logger, - // configurationUtilities, - // }); - // - // throwIfResponseIsNotValid({ - // res, - // }); - // - // const issueTypes = res.data.values; - // return normalizeIssueTypes(issueTypes); - // } - // } catch (error) { - // throw new Error( - // getErrorMessage( - // i18n.NAME, - // `Unable to get issue types. Error: ${error.message}. Reason: ${createErrorMessage( - // error.response?.data - // )}` - // ) - // ); - // } - // }; - // - // const getFieldsByIssueType = async (issueTypeId: string) => { - // const capabilitiesResponse = await getCapabilities(); - // const supportsNewAPI = hasSupportForNewAPI(capabilitiesResponse); - // try { - // if (!supportsNewAPI) { - // const res = await request({ - // axios: axiosInstance, - // method: 'get', - // url: createGetIssueTypeFieldsUrl(getIssueTypeFieldsOldAPIURL, issueTypeId), - // logger, - // configurationUtilities, - // }); - // - // throwIfResponseIsNotValid({ - // res, - // }); - // - // const fields = res.data.projects[0]?.issuetypes[0]?.fields || {}; - // return normalizeFields(fields); - // } else { - // const res = await request({ - // axios: axiosInstance, - // method: 'get', - // url: createGetIssueTypeFieldsUrl(getIssueTypeFieldsUrl, issueTypeId), - // logger, - // configurationUtilities, - // }); - // - // throwIfResponseIsNotValid({ - // res, - // }); - // - // const fields = res.data.values.reduce( - // (acc: { [x: string]: {} }, value: { fieldId: string }) => ({ - // ...acc, - // [value.fieldId]: { ...value }, - // }), - // {} - // ); - // return normalizeFields(fields); - // } - // } catch (error) { - // throw new Error( - // getErrorMessage( - // i18n.NAME, - // `Unable to get fields. Error: ${error.message}. Reason: ${createErrorMessage( - // error.response?.data - // )}` - // ) - // ); - // } - // }; - // - // const getFields = async () => { - // try { - // const issueTypes = await getIssueTypes(); - // const fieldsPerIssueType = await Promise.all( - // issueTypes.map((issueType) => getFieldsByIssueType(issueType.id)) - // ); - // return fieldsPerIssueType.reduce((acc: GetCommonFieldsResponse, fieldTypesByIssue) => { - // const currentListOfFields = Object.keys(acc); - // return currentListOfFields.length === 0 - // ? fieldTypesByIssue - // : currentListOfFields.reduce( - // (add: GetCommonFieldsResponse, field) => - // Object.keys(fieldTypesByIssue).includes(field) - // ? { ...add, [field]: acc[field] } - // : add, - // {} - // ); - // }, {}); - // } catch (error) { - // // errors that happen here would be thrown in the contained async calls - // throw error; - // } - // }; - // - // const getIssues = async (title: string) => { - // const query = `${searchUrl}?jql=${encodeURIComponent( - // `project="${projectKey}" and summary ~"${title}"` - // )}`; - // - // try { - // const res = await request({ - // axios: axiosInstance, - // method: 'get', - // url: query, - // logger, - // configurationUtilities, - // }); - // - // throwIfResponseIsNotValid({ - // res, - // }); - // - // return normalizeSearchResults(res.data?.issues ?? []); - // } catch (error) { - // throw new Error( - // getErrorMessage( - // i18n.NAME, - // `Unable to get issues. Error: ${error.message}. Reason: ${createErrorMessage( - // error.response?.data - // )}` - // ) - // ); - // } - // }; - // - // const getIssue = async (id: string) => { - // const getIssueUrl = `${createIncidentUrl}/${id}`; - // try { - // const res = await request({ - // axios: axiosInstance, - // method: 'get', - // url: getIssueUrl, - // logger, - // configurationUtilities, - // }); - // - // throwIfResponseIsNotValid({ - // res, - // }); - // - // return normalizeIssue(res.data ?? {}); - // } catch (error) { - // throw new Error( - // getErrorMessage( - // i18n.NAME, - // `Unable to get issue with id ${id}. Error: ${error.message}. Reason: ${createErrorMessage( - // error.response?.data - // )}` - // ) - // ); - // } - // }; - return { - // getFields, getIncident, createIncident, // updateIncident, // createComment, - // getCapabilities, - // getIssueTypes, - // getFieldsByIssueType, - // getIssues, - // getIssue, }; }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts index 2cc30b2f9976e6..7c7b5c0b4db7df 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts @@ -48,8 +48,8 @@ const findTheValue = (obj: Record | unknown>, ke export const getObjectValueByKey = ( obj: Record | unknown>, key: string -) => { - return findTheValue(obj, splitKeys(key)); +): string => { + return findTheValue(obj, splitKeys(key)) as string; }; export const throwIfResponseIsNotValidSpecial = ({ From 9de1167b317034515ca8fb42d444a3600c6122c9 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 23 May 2022 08:59:52 -0600 Subject: [PATCH 18/97] add update and create comment --- .../builtin_action_types/cases_webhook/api.ts | 63 ++++--- .../cases_webhook/service.ts | 175 ++++++++---------- .../cases_webhook/types.ts | 27 ++- .../cases_webhook/utils.ts | 20 ++ .../servicenow/service.ts | 9 +- 5 files changed, 151 insertions(+), 143 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/api.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/api.ts index e782f83f9531a0..059f2c4fd13ae6 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/api.ts @@ -18,41 +18,40 @@ const pushToServiceHandler = async ({ }: PushToServiceApiHandlerArgs): Promise => { // const { comments } = params; const { externalId, ...rest } = params.incident; + const { comments } = params; const incident: Incident = rest; - const res: PushToServiceResponse = await externalService.createIncident({ - incident, - }); + let res: PushToServiceResponse; - // if (externalId != null) { - // res = await externalService.updateIncident({ - // incidentId: externalId, - // incident, - // }); - // } else { - // res = await externalService.createIncident({ - // incident, - // }); - // } + if (externalId != null) { + res = await externalService.updateIncident({ + incidentId: externalId, + incident, + }); + } else { + res = await externalService.createIncident({ + incident, + }); + } - // if (comments && Array.isArray(comments) && comments.length > 0) { - // res.comments = []; - // for (const currentComment of comments) { - // if (!currentComment.comment) { - // continue; - // } - // const comment = await externalService.createComment({ - // incidentId: res.id, - // comment: currentComment, - // }); - // res.comments = [ - // ...(res.comments ?? []), - // { - // commentId: comment.commentId, - // pushedDate: comment.pushedDate, - // }, - // ]; - // } - // } + if (comments && Array.isArray(comments) && comments.length > 0) { + res.comments = []; + for (const currentComment of comments) { + if (!currentComment.comment) { + continue; + } + await externalService.createComment({ + incidentId: res.id, + comment: currentComment, + }); + res.comments = [ + ...(res.comments ?? []), + { + commentId: currentComment.commentId, + pushedDate: res.pushedDate, + }, + ]; + } + } return res; }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts index b66850f19da5f7..bcb1570faf4cbd 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts @@ -12,6 +12,9 @@ import { createServiceError, getObjectValueByKey, getPushedDate, + removeSlash, + replaceComment, + replaceSumDesc, throwIfResponseIsNotValidSpecial, } from './utils'; import { @@ -22,6 +25,8 @@ import { CasesWebhookSecretConfigurationType, ExternalServiceIncidentResponse, GetIncidentResponse, + UpdateIncidentParams, + CreateCommentParams, } from './types'; import * as i18n from './translations'; @@ -35,27 +40,31 @@ export const createExternalService = ( configurationUtilities: ActionsConfigurationUtilities ): ExternalService => { const { + createCommentJson, + createCommentMethod, + createCommentUrl: createCommentUrlConfig, + createIncidentJson, + createIncidentMethod, createIncidentResponseKey, createIncidentUrl: createIncidentUrlConfig, - getIncidentUrl: getIncidentUrlConfig, - createIncidentJson, getIncidentResponseCreatedDateKey, getIncidentResponseExternalTitleKey, getIncidentResponseUpdatedDateKey, + getIncidentUrl: getIncidentUrlConfig, incidentViewUrl, + updateIncidentJson, + updateIncidentMethod, + updateIncidentUrl: updateIncidentUrlConfig, } = config as CasesWebhookPublicConfigurationType; const { password, user } = secrets as CasesWebhookSecretConfigurationType; if (!getIncidentUrlConfig || !password || !user) { throw Error(`[Action]${i18n.NAME}: Wrong configuration.`); } - const createIncidentUrl = createIncidentUrlConfig.endsWith('/') - ? createIncidentUrlConfig.slice(0, -1) - : createIncidentUrlConfig; - - const getIncidentUrl = getIncidentUrlConfig.endsWith('/') - ? getIncidentUrlConfig.slice(0, -1) - : getIncidentUrlConfig; + const createCommentUrl = removeSlash(createCommentUrlConfig); + const createIncidentUrl = removeSlash(createIncidentUrlConfig); + const getIncidentUrl = removeSlash(getIncidentUrlConfig); + const updateIncidentUrl = removeSlash(updateIncidentUrlConfig); const getIncidentViewURL = (id: string) => `${ @@ -98,13 +107,6 @@ export const createExternalService = ( } }; - const replaceSumDesc = (sum: string, desc: string) => { - let str = createIncidentJson; // incident is stringified object - str = str.replace('$SUM', sum); - str = str.replace('$DESC', desc); - return JSON.parse(str); - }; - const createIncident = async ({ incident, }: CreateIncidentParams): Promise => { @@ -114,8 +116,8 @@ export const createExternalService = ( axios: axiosInstance, url: `${createIncidentUrl}`, logger, - method: 'post', - data: replaceSumDesc(summary, description ?? ''), + method: createIncidentMethod, + data: replaceSumDesc(createIncidentJson, summary, description ?? ''), configurationUtilities, }); @@ -141,91 +143,62 @@ export const createExternalService = ( } }; - // const updateIncident = async ({ - // incidentId, - // incident, - // }: UpdateIncidentParams): Promise => { - // const incidentWithoutNullValues = Object.entries(incident).reduce( - // (obj, [key, value]) => (value != null ? { ...obj, [key]: value } : obj), - // {} as Incident - // ); - // - // const fields = createFields(projectKey, incidentWithoutNullValues); - // - // try { - // const res = await request({ - // axios: axiosInstance, - // method: 'put', - // url: `${createIncidentUrl}/${incidentId}`, - // logger, - // data: { fields }, - // configurationUtilities, - // }); - // - // throwIfResponseIsNotValid({ - // res, - // }); - // - // const updatedIncident = await getIncident(incidentId as string); - // - // return { - // title: updatedIncident.key, - // id: updatedIncident.id, - // pushedDate: new Date(updatedIncident.updated).toISOString(), - // url: getIncidentViewURL(updatedIncident.key), - // }; - // } catch (error) { - // throw new Error( - // getErrorMessage( - // i18n.NAME, - // `Unable to update incident with id ${incidentId}. Error: ${ - // error.message - // }. Reason: ${createErrorMessage(error.response?.data)}` - // ) - // ); - // } - // }; - // - // const createComment = async ({ - // incidentId, - // comment, - // }: CreateCommentParams): Promise => { - // try { - // const res = await request({ - // axios: axiosInstance, - // method: 'post', - // url: getCommentsURL(incidentId), - // logger, - // data: { body: comment.comment }, - // configurationUtilities, - // }); - // - // throwIfResponseIsNotValid({ - // res, - // requiredAttributesToBeInTheResponse: ['id', 'created'], - // }); - // - // 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 - // }. Reason: ${createErrorMessage(error.response?.data)}` - // ) - // ); - // } - // }; + const updateIncident = async ({ + incidentId, + incident, + }: UpdateIncidentParams): Promise => { + try { + const res = await request({ + axios: axiosInstance, + method: updateIncidentMethod, + url: `${updateIncidentUrl}/${incidentId}`, + logger, + data: replaceSumDesc(updateIncidentJson, incident.summary, incident.description), + configurationUtilities, + }); + + throwIfResponseIsNotValidSpecial({ + res, + }); + + const updatedIncident = await getIncident(incidentId as string); + + return { + id: incidentId, + title: updatedIncident.title, + url: getIncidentViewURL(incidentId), + pushedDate: getPushedDate(updatedIncident.updated), + }; + } catch (error) { + throw createServiceError(error, 'Unable to update incident'); + } + }; + + const createComment = async ({ incidentId, comment }: CreateCommentParams): Promise => { + try { + const res = await request({ + axios: axiosInstance, + method: createCommentMethod, + url: `${createCommentUrl}/${incidentId}`, + logger, + data: replaceComment(createCommentJson, comment.comment), + configurationUtilities, + }); + + throwIfResponseIsNotValidSpecial({ + res, + }); + + return res; + } catch (error) { + throw createServiceError(error, 'Unable to post comment'); + } + }; return { - getIncident, + createComment, createIncident, - // updateIncident, - // createComment, + getIncident, + updateIncident, }; }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts index 7b1ac6857e0cfa..b97396d7eda6ff 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts @@ -60,12 +60,27 @@ export type PushToServiceApiParams = ExecutorSubActionPushParams; // incident service export interface ExternalService { - getIncident: (id: string) => Promise; + createComment: (params: CreateCommentParams) => Promise; createIncident: (params: CreateIncidentParams) => Promise; + getIncident: (id: string) => Promise; + updateIncident: (params: UpdateIncidentParams) => Promise; } export interface CreateIncidentParams { incident: Incident; } +export interface UpdateIncidentParams { + incidentId: string; + incident: Partial; +} +export interface SimpleComment { + comment: string; + commentId: string; +} + +export interface CreateCommentParams { + incidentId: string; + comment: SimpleComment; +} export interface ExternalServiceApiHandlerArgs { externalService: ExternalService; @@ -76,7 +91,15 @@ export interface PushToServiceApiHandlerArgs extends ExternalServiceApiHandlerAr params: PushToServiceApiParams; logger: Logger; } -export type PushToServiceResponse = ExternalServiceIncidentResponse; +export interface PushToServiceResponse extends ExternalServiceIncidentResponse { + comments?: ExternalServiceCommentResponse[]; +} + +export interface ExternalServiceCommentResponse { + commentId: string; + pushedDate: string; + externalCommentId?: string; +} export interface GetIncidentResponse { id: string; diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts index 7c7b5c0b4db7df..3d36bf4033def3 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts @@ -101,3 +101,23 @@ export const throwIfResponseIsNotValidSpecial = ({ }); } }; + +export const removeSlash = (url: string) => (url.endsWith('/') ? url.slice(0, -1) : url); + +export const replaceSumDesc = (stringifiedJson: string, sum?: string, desc?: string | null) => { + let str = stringifiedJson; + if (sum != null) { + str = str.replace('$SUM', sum); + } + if (desc != null) { + str = str.replace('$DESC', desc); + } + return JSON.parse(str); +}; + +export const replaceComment = (stringifiedJson: string, comment: string) => { + let str = stringifiedJson; + str = str.replace('$COMMENT', comment); + + return JSON.parse(str); +}; 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 0d40fc61595cfb..5a1b1f604cb818 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 @@ -181,14 +181,7 @@ export const createExternalService: ServiceFactory = ({ const createIncident = async ({ incident }: ExternalServiceParamsCreate) => { try { await checkIfApplicationIsInstalled(); - console.log('SN args', { - axios: axiosInstance, - url: getCreateIncidentUrl(), - logger, - method: 'post', - data: prepareIncident(useTableApi, incident), - configurationUtilities, - }); + const res = await request({ axios: axiosInstance, url: getCreateIncidentUrl(), From 211c24bad4f897568b31430a717935ebbecdd4cd Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 23 May 2022 11:40:10 -0600 Subject: [PATCH 19/97] support jira --- .../cases_webhook/service.ts | 40 ++++++++----------- .../cases_webhook/utils.ts | 16 +++++++- .../cases_webhook/webhook_connectors.tsx | 8 ++-- 3 files changed, 35 insertions(+), 29 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts index bcb1570faf4cbd..c94f9715e2e550 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts @@ -12,6 +12,7 @@ import { createServiceError, getObjectValueByKey, getPushedDate, + makeIncidentUrl, removeSlash, replaceComment, replaceSumDesc, @@ -42,7 +43,7 @@ export const createExternalService = ( const { createCommentJson, createCommentMethod, - createCommentUrl: createCommentUrlConfig, + createCommentUrl, createIncidentJson, createIncidentMethod, createIncidentResponseKey, @@ -50,30 +51,18 @@ export const createExternalService = ( getIncidentResponseCreatedDateKey, getIncidentResponseExternalTitleKey, getIncidentResponseUpdatedDateKey, - getIncidentUrl: getIncidentUrlConfig, + getIncidentUrl, incidentViewUrl, updateIncidentJson, updateIncidentMethod, - updateIncidentUrl: updateIncidentUrlConfig, + updateIncidentUrl, } = config as CasesWebhookPublicConfigurationType; const { password, user } = secrets as CasesWebhookSecretConfigurationType; - if (!getIncidentUrlConfig || !password || !user) { + if (!getIncidentUrl || !password || !user) { throw Error(`[Action]${i18n.NAME}: Wrong configuration.`); } - const createCommentUrl = removeSlash(createCommentUrlConfig); const createIncidentUrl = removeSlash(createIncidentUrlConfig); - const getIncidentUrl = removeSlash(getIncidentUrlConfig); - const updateIncidentUrl = removeSlash(updateIncidentUrlConfig); - - const getIncidentViewURL = (id: string) => - `${ - incidentViewUrl.endsWith('=') - ? incidentViewUrl - : incidentViewUrl.endsWith('/') - ? incidentViewUrl - : `${incidentViewUrl}/` - }${id}`; const axiosInstance = axios.create({ auth: { username: user, password }, @@ -83,11 +72,16 @@ export const createExternalService = ( try { const res = await request({ axios: axiosInstance, - url: `${getIncidentUrl}/${id}`, + url: makeIncidentUrl(getIncidentUrl, id), logger, configurationUtilities, }); - + console.log({ + getIncidentResponseCreatedDateKey, + getIncidentResponseExternalTitleKey, + getIncidentResponseUpdatedDateKey, + }); + // console.log(res); throwIfResponseIsNotValidSpecial({ res, requiredAttributesToBeInTheResponse: [ @@ -100,7 +94,7 @@ export const createExternalService = ( const title = getObjectValueByKey(res.data, getIncidentResponseExternalTitleKey); const created = getObjectValueByKey(res.data, getIncidentResponseCreatedDateKey); const updated = getObjectValueByKey(res.data, getIncidentResponseUpdatedDateKey); - + console.log('GET RESPONSE', { id, title, created, updated }); return { id, title, created, updated }; } catch (error) { throw createServiceError(error, 'Unable to get incident'); @@ -135,7 +129,7 @@ export const createExternalService = ( return { id: incidentId, title: insertedIncident.title, - url: getIncidentViewURL(incidentId), + url: makeIncidentUrl(incidentViewUrl, incidentId, insertedIncident.title), pushedDate: getPushedDate(insertedIncident.created), }; } catch (error) { @@ -151,7 +145,7 @@ export const createExternalService = ( const res = await request({ axios: axiosInstance, method: updateIncidentMethod, - url: `${updateIncidentUrl}/${incidentId}`, + url: makeIncidentUrl(updateIncidentUrl, incidentId), logger, data: replaceSumDesc(updateIncidentJson, incident.summary, incident.description), configurationUtilities, @@ -166,7 +160,7 @@ export const createExternalService = ( return { id: incidentId, title: updatedIncident.title, - url: getIncidentViewURL(incidentId), + url: makeIncidentUrl(incidentViewUrl, incidentId, updatedIncident.title), pushedDate: getPushedDate(updatedIncident.updated), }; } catch (error) { @@ -179,7 +173,7 @@ export const createExternalService = ( const res = await request({ axios: axiosInstance, method: createCommentMethod, - url: `${createCommentUrl}/${incidentId}`, + url: makeIncidentUrl(createCommentUrl, incidentId), logger, data: replaceComment(createCommentJson, comment.comment), configurationUtilities, diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts index 3d36bf4033def3..4d6ae336aa58f9 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts @@ -19,9 +19,12 @@ export const createServiceError = (error: any, message: string) => ); export const getPushedDate = (timestamp?: string) => { if (timestamp != null) { - return new Date(addTimeZoneToDate(timestamp)).toISOString(); + try { + return new Date(timestamp).toISOString(); + } catch (e) { + return new Date(addTimeZoneToDate(timestamp)).toISOString(); + } } - return new Date().toISOString(); }; @@ -115,6 +118,15 @@ export const replaceSumDesc = (stringifiedJson: string, sum?: string, desc?: str return JSON.parse(str); }; +export const makeIncidentUrl = (url: string, id: string, title?: string) => { + let str = url; + str = str.replace('$ID', id); + if (title != null) { + str = str.replace('$TITLE', title); + } + return str; +}; + export const replaceComment = (stringifiedJson: string, comment: string) => { let str = stringifiedJson; str = str.replace('$COMMENT', comment); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx index b9b65b515d4f72..411108e7d25217 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx @@ -427,7 +427,7 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.getIncidentUrlTextFieldLabel', { defaultMessage: - 'API URL to GET incident details JSON from external system. Withold the ID from the URL as Kibana will dynamically insert the id based on the provided Create Incident Response Incident Key.', + 'API URL to GET incident details JSON from external system. Use $ID and Kibana will dynamically update the url with the external incident id.', } )} > @@ -579,7 +579,7 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.incidentViewUrlTextFieldLabel', { defaultMessage: - 'URL to view incident in external system. Withold the ID from the URL as Kibana will dynamically insert the id based on the provided Create Incident Response Incident Key.', + 'URL to view incident in external system. Use $ID or $TITLE and Kibana will dynamically update the url with the external incident id or external incident title.', } )} > @@ -642,7 +642,7 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.updateIncidentUrlTextFieldLabel', { defaultMessage: - 'API URL to update incident. Withold the ID from the URL as Kibana will dynamically insert the id based on the provided Create Incident Response Incident Key.', + 'API URL to update incident. Use $ID and Kibana will dynamically update the url with the external incident id.', } )} > @@ -753,7 +753,7 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.createCommentUrlTextFieldLabel', { defaultMessage: - 'API URL to add comment to incident. Withold the ID from the URL as Kibana will dynamically insert the id based on the provided Create Incident Response Incident Key.', + 'API URL to add comment to incident. Use $ID and Kibana will dynamically update the url with the external incident id.', } )} > From 5709c8c3373d6da73ebdea096162e56db97a060c Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 24 May 2022 08:36:38 -0600 Subject: [PATCH 20/97] labels --- .../cases_webhook/service.ts | 19 +++++----- .../cases_webhook/utils.ts | 35 +++++++++++++------ .../builtin_action_types/jira/service.ts | 4 +-- .../cases_webhook/webhook_connectors.tsx | 4 +-- 4 files changed, 37 insertions(+), 25 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts index c94f9715e2e550..9b6526b32e6138 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts @@ -76,12 +76,7 @@ export const createExternalService = ( logger, configurationUtilities, }); - console.log({ - getIncidentResponseCreatedDateKey, - getIncidentResponseExternalTitleKey, - getIncidentResponseUpdatedDateKey, - }); - // console.log(res); + throwIfResponseIsNotValidSpecial({ res, requiredAttributesToBeInTheResponse: [ @@ -94,7 +89,6 @@ export const createExternalService = ( const title = getObjectValueByKey(res.data, getIncidentResponseExternalTitleKey); const created = getObjectValueByKey(res.data, getIncidentResponseCreatedDateKey); const updated = getObjectValueByKey(res.data, getIncidentResponseUpdatedDateKey); - console.log('GET RESPONSE', { id, title, created, updated }); return { id, title, created, updated }; } catch (error) { throw createServiceError(error, 'Unable to get incident'); @@ -104,14 +98,14 @@ export const createExternalService = ( const createIncident = async ({ incident, }: CreateIncidentParams): Promise => { - const { summary, description } = incident; + const { labels, summary, description } = incident; try { const res: AxiosResponse = await request({ axios: axiosInstance, url: `${createIncidentUrl}`, logger, method: createIncidentMethod, - data: replaceSumDesc(createIncidentJson, summary, description ?? ''), + data: replaceSumDesc(createIncidentJson, summary, description ?? '', labels ?? []), configurationUtilities, }); @@ -147,7 +141,12 @@ export const createExternalService = ( method: updateIncidentMethod, url: makeIncidentUrl(updateIncidentUrl, incidentId), logger, - data: replaceSumDesc(updateIncidentJson, incident.summary, incident.description), + data: replaceSumDesc( + updateIncidentJson, + incident.summary, + incident.description, + incident.labels + ), configurationUtilities, }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts index 4d6ae336aa58f9..68d04e4a27670c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts @@ -5,18 +5,19 @@ * 2.0. */ -import { AxiosResponse } from 'axios'; +import { AxiosResponse, AxiosError } from 'axios'; import { isEmpty, isObjectLike } from 'lodash'; import { addTimeZoneToDate, getErrorMessage } from '../lib/axios_utils'; import * as i18n from './translations'; -export const createServiceError = (error: any, message: string) => +export const createServiceError = (error: AxiosError, message: string) => new Error( getErrorMessage( i18n.NAME, - `${message}. Error: ${error.message} Reason: ${error.response?.data}` + `${message}. Error: ${error.message} Reason: ${error.response?.statusText}` ) ); + export const getPushedDate = (timestamp?: string) => { if (timestamp != null) { try { @@ -107,14 +108,28 @@ export const throwIfResponseIsNotValidSpecial = ({ export const removeSlash = (url: string) => (url.endsWith('/') ? url.slice(0, -1) : url); -export const replaceSumDesc = (stringifiedJson: string, sum?: string, desc?: string | null) => { +export const replaceSumDesc = ( + stringifiedJson: string, + sum?: string, + desc?: string | null, + labels?: string[] | null +) => { let str = stringifiedJson; - if (sum != null) { - str = str.replace('$SUM', sum); - } - if (desc != null) { - str = str.replace('$DESC', desc); - } + + const prs = JSON.parse(str); + str = JSON.stringify(prs, function replacer(key, value) { + if (value === '$SUM') { + return sum; + } + if (value === '$DESC') { + return desc; + } + if (value === '$TAGS') { + return labels != null ? labels : []; + } + return value; + }); + return JSON.parse(str); }; 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 4d98ebfda5bc49..5f5147c67e1049 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 @@ -222,9 +222,7 @@ export const createExternalService = ( ...incident, issueType, }); - console.log('jira args', { - fields, - }); + try { const res = await request({ axios: axiosInstance, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx index 411108e7d25217..c42e55d98aa7f1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx @@ -336,7 +336,7 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.createIncidentJsonTextFieldLabel', { defaultMessage: - 'JSON object to create incident. Sub $SUM where the summary/title should go and $DESC where the description should go', + 'JSON object to create incident. Sub $SUM where the summary/title should go, $DESC where the description should go, and $TAGS where tags should go (optional).', } )} > @@ -681,7 +681,7 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.updateIncidentJsonTextFieldLabel', { defaultMessage: - 'JSON object to update incident. Sub $SUM where the summary/title should go and $DESC where the description should go', + 'JSON object to update incident. Sub $SUM where the summary/title should go, $DESC where the description should go, and $TAGS where tags should go (optional).', } )} > From 06a8326993eb2c4944380538fa7650bab693c630 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 25 May 2022 08:36:20 -0600 Subject: [PATCH 21/97] rm console logs and improve validators --- .../cases_webhook/service.ts | 8 +- .../cases_webhook/translations.ts | 24 ++++++ .../cases_webhook/validators.ts | 77 ++++++++++++++----- .../plugins/cases/server/client/cases/push.ts | 3 +- 4 files changed, 88 insertions(+), 24 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts index 9b6526b32e6138..7687b6ec7de61c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts @@ -8,6 +8,7 @@ import axios, { AxiosResponse } from 'axios'; import { Logger } from '@kbn/core/server'; +import { isString } from 'lodash'; import { createServiceError, getObjectValueByKey, @@ -52,6 +53,8 @@ export const createExternalService = ( getIncidentResponseExternalTitleKey, getIncidentResponseUpdatedDateKey, getIncidentUrl, + hasAuth, + headers, incidentViewUrl, updateIncidentJson, updateIncidentMethod, @@ -65,7 +68,10 @@ export const createExternalService = ( const createIncidentUrl = removeSlash(createIncidentUrlConfig); const axiosInstance = axios.create({ - auth: { username: user, password }, + ...(hasAuth && isString(secrets.user) && isString(secrets.password) + ? { auth: { username: secrets.user, password: secrets.password } } + : {}), + ...(headers != null ? { headers } : {}), }); const getIncident = async (id: string): Promise => { diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/translations.ts index 85d7bc545ee1bf..0939a215085075 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/translations.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/translations.ts @@ -10,3 +10,27 @@ import { i18n } from '@kbn/i18n'; export const NAME = i18n.translate('xpack.actions.builtin.cases.casesWebhookTitle', { defaultMessage: 'Cases Webhook', }); + +export const INVALID_URL = (err: string, url: string) => + i18n.translate('xpack.actions.builtin.casesWebhook.casesWebhookConfigurationErrorNoHostname', { + defaultMessage: 'error configuring cases webhook action: unable to parse {url}: {err}', + values: { + err, + url, + }, + }); + +export const CONFIG_ERR = (err: string) => + i18n.translate('xpack.actions.builtin.casesWebhook.casesWebhookConfigurationError', { + defaultMessage: 'error configuring cases webhook action: {err}', + values: { + err, + }, + }); + +export const INVALID_USER_PW = i18n.translate( + 'xpack.actions.builtin.casesWebhook.invalidUsernamePassword', + { + defaultMessage: 'both user and password must be specified', + } +); diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts index 25da3a03edaeaa..45d0865b6f597e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { i18n } from '@kbn/i18n'; +import * as i18n from './translations'; import { ActionsConfigurationUtilities } from '../../actions_config'; import { CasesWebhookPublicConfigurationType, @@ -17,31 +17,68 @@ const validateConfig = ( configurationUtilities: ActionsConfigurationUtilities, configObject: CasesWebhookPublicConfigurationType ) => { - const createIncidentUrl = configObject.createIncidentUrl; + const { + createCommentUrl, + createIncidentUrl, + incidentViewUrl, + getIncidentUrl, + updateIncidentUrl, + } = configObject; + try { new URL(createIncidentUrl); } catch (err) { - return i18n.translate( - 'xpack.actions.builtin.casesWebhook.casesWebhookConfigurationErrorNoHostname', - { - defaultMessage: - 'error configuring cases webhook action: unable to parse createIncidentUrl: {err}', - values: { - err, - }, - } - ); + return i18n.INVALID_URL(err, 'createIncidentUrl'); } try { configurationUtilities.ensureUriAllowed(createIncidentUrl); } catch (allowListError) { - return i18n.translate('xpack.actions.builtin.casesWebhook.casesWebhookConfigurationError', { - defaultMessage: 'error configuring cases webhook action: {message}', - values: { - message: allowListError.message, - }, - }); + return i18n.CONFIG_ERR(allowListError.message); + } + try { + new URL(createCommentUrl); + } catch (err) { + return i18n.INVALID_URL(err, 'createCommentUrl'); + } + + try { + configurationUtilities.ensureUriAllowed(createCommentUrl); + } catch (allowListError) { + return i18n.CONFIG_ERR(allowListError.message); + } + try { + new URL(incidentViewUrl); + } catch (err) { + return i18n.INVALID_URL(err, 'incidentViewUrl'); + } + + try { + configurationUtilities.ensureUriAllowed(incidentViewUrl); + } catch (allowListError) { + return i18n.CONFIG_ERR(allowListError.message); + } + try { + new URL(getIncidentUrl); + } catch (err) { + return i18n.INVALID_URL(err, 'getIncidentUrl'); + } + + try { + configurationUtilities.ensureUriAllowed(getIncidentUrl); + } catch (allowListError) { + return i18n.CONFIG_ERR(allowListError.message); + } + try { + new URL(updateIncidentUrl); + } catch (err) { + return i18n.INVALID_URL(err, 'updateIncidentUrl'); + } + + try { + configurationUtilities.ensureUriAllowed(updateIncidentUrl); + } catch (allowListError) { + return i18n.CONFIG_ERR(allowListError.message); } }; @@ -49,9 +86,7 @@ export const validateSecrets = (secrets: CasesWebhookSecretConfigurationType) => // user and password must be set together (or not at all) if (!secrets.password && !secrets.user) return; if (secrets.password && secrets.user) return; - return i18n.translate('xpack.actions.builtin.casesWebhook.invalidUsernamePassword', { - defaultMessage: 'both user and password must be specified', - }); + return i18n.INVALID_USER_PW; }; export const validate: ExternalServiceValidation = { diff --git a/x-pack/plugins/cases/server/client/cases/push.ts b/x-pack/plugins/cases/server/client/cases/push.ts index 8bcc295714f9bf..b41de9cfd9967a 100644 --- a/x-pack/plugins/cases/server/client/cases/push.ts +++ b/x-pack/plugins/cases/server/client/cases/push.ts @@ -156,7 +156,7 @@ export const push = async ( alerts, casesConnectors, }); - console.log('externalServiceIncident', externalServiceIncident); + const pushRes = await actionsClient.execute({ actionId: connector?.id ?? '', params: { @@ -164,7 +164,6 @@ export const push = async ( subActionParams: externalServiceIncident, }, }); - console.log('pushRes', pushRes); if (pushRes.status === 'error') { throw Boom.failedDependency( From 33adfe57990d6632d5885ac272180ee060819b5b Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 25 May 2022 09:00:06 -0600 Subject: [PATCH 22/97] fix type --- .../server/builtin_action_types/cases_webhook/index.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts index cfc3aa26f65939..a6350ffb447819 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { i18n } from '@kbn/i18n'; import { curry } from 'lodash'; import { schema } from '@kbn/config-schema'; import { Logger } from '@kbn/core/server'; @@ -27,9 +26,10 @@ import { } from './schema'; import { api } from './api'; import { validate } from './validators'; +import * as i18n from './translations'; const supportedSubActions: string[] = ['pushToService']; - +export type ActionParamsType = CasesWebhookActionParamsType; export const ActionTypeId = '.cases-webhook'; // action type definition export function getActionType({ @@ -47,9 +47,7 @@ export function getActionType({ return { id: ActionTypeId, minimumLicenseRequired: 'gold', - name: i18n.translate('xpack.actions.builtin.casesWebhookTitle', { - defaultMessage: 'Cases Webhook', - }), + name: i18n.NAME, validate: { config: schema.object(ExternalIncidentServiceConfiguration, { validate: curry(validate.config)(configurationUtilities), From b0162214dc21f805aa2518431dd4a7501b550584 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 25 May 2022 12:58:52 -0600 Subject: [PATCH 23/97] fix triggers actions ui tests --- .../cases_webhook/translations.ts | 28 +-- .../cases_webhook/webhook.test.tsx | 192 ++++++++++++++---- .../cases_webhook/webhook.tsx | 28 +-- .../cases_webhook/webhook_connectors.test.tsx | 140 +++++++++---- .../cases_webhook/webhook_params.test.tsx | 24 ++- 5 files changed, 300 insertions(+), 112 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts index 89d2c75ea7b03b..0011ccc952de3a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts @@ -57,13 +57,13 @@ export const UPDATE_METHOD_REQUIRED = i18n.translate( export const CREATE_COMMENT_URL_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.error.requiredCreateCommentUrlText', { - defaultMessage: 'Create comment incident URL is required.', + defaultMessage: 'Create comment URL is required.', } ); export const CREATE_COMMENT_INCIDENT_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.error.requiredCreateCommentIncidentText', { - defaultMessage: 'Create comment incident object is required.', + defaultMessage: 'Create comment object is required.', } ); @@ -101,16 +101,20 @@ export const GET_RESPONSE_EXTERNAL_UPDATED_KEY_REQUIRED = i18n.translate( export const GET_INCIDENT_VIEW_URL = i18n.translate( 'xpack.triggersActionsUI.sections.addAction.casesWebhookAction.error.requiredGetIncidentViewUrlKeyText', { - defaultMessage: 'Get external incident view URL.', + defaultMessage: 'View incident URL is required.', } ); -export const URL_INVALID = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.error.invalidUrlTextField', - { - defaultMessage: 'URL is invalid.', - } -); +export const URL_INVALID = (urlType: string) => + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.error.invalidUrlTextField', + { + defaultMessage: '{urlType} URL is invalid.', + values: { + urlType, + }, + } + ); export const USERNAME_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.sections.addAction.casesWebhookAction.error.requiredAuthUserNameText', @@ -140,9 +144,9 @@ export const USERNAME_REQUIRED_FOR_PASSWORD = i18n.translate( } ); -export const BODY_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredWebhookBodyText', +export const SUMMARY_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredWebhookSummaryText', { - defaultMessage: 'Body is required.', + defaultMessage: 'Summary is required.', } ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.test.tsx index 8a17b1c0b206a6..238815a8bce93f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.test.tsx @@ -8,14 +8,15 @@ import { TypeRegistry } from '../../../type_registry'; import { registerBuiltInActionTypes } from '..'; import { ActionTypeModel } from '../../../../types'; -import { WebhookActionConnector } from '../types'; +import { CasesWebhookActionConnector } from './types'; +import { registrationServicesMock } from '../../../../mocks'; -const ACTION_TYPE_ID = '.webhook'; +const ACTION_TYPE_ID = '.cases-webhook'; let actionTypeModel: ActionTypeModel; beforeAll(() => { const actionTypeRegistry = new TypeRegistry(); - registerBuiltInActionTypes({ actionTypeRegistry }); + registerBuiltInActionTypes({ actionTypeRegistry, services: registrationServicesMock }); const getResult = actionTypeRegistry.get(ACTION_TYPE_ID); if (getResult !== null) { actionTypeModel = getResult; @@ -25,7 +26,7 @@ beforeAll(() => { describe('actionTypeRegistry.get() works', () => { test('action type static data is as expected', () => { expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID); - expect(actionTypeModel.iconClass).toEqual('logoWebhook'); + expect(actionTypeModel.iconClass).toEqual('indexManagementApp'); }); }); @@ -37,22 +38,51 @@ describe('webhook connector validation', () => { password: 'pass', }, id: 'test', - actionTypeId: '.webhook', - name: 'webhook', + actionTypeId: '.cases-webhook', + name: 'Jira Webhook', + isDeprecated: false, isPreconfigured: false, config: { - method: 'PUT', - url: 'http://test.com', - headers: { 'content-type': 'text' }, + createCommentJson: '{"body":"$COMMENT"}', + createCommentMethod: 'post', + createCommentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/$ID/comment', + createIncidentJson: + '{"fields":{"summary":"$SUM","description":"$DESC","project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', + createIncidentMethod: 'post', + createIncidentResponseKey: 'id', + createIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue', + getIncidentResponseCreatedDateKey: 'fields.created', + getIncidentResponseExternalTitleKey: 'key', + getIncidentResponseUpdatedDateKey: 'fields.udpated', hasAuth: true, + headers: { 'content-type': 'text' }, + incidentViewUrl: 'https://siem-kibana.atlassian.net/browse/$TITLE', + getIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/$ID', + updateIncidentJson: + '{"fields":{"summary":"$SUM","description":"$DESC","project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', + updateIncidentMethod: 'put', + updateIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/$ID', }, - } as WebhookActionConnector; + } as CasesWebhookActionConnector; expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ config: { errors: { - url: [], - method: [], + createCommentJson: [], + createCommentMethod: [], + createCommentUrl: [], + createIncidentJson: [], + createIncidentMethod: [], + createIncidentResponseKey: [], + createIncidentUrl: [], + getIncidentResponseCreatedDateKey: [], + getIncidentResponseExternalTitleKey: [], + getIncidentResponseUpdatedDateKey: [], + incidentViewUrl: [], + getIncidentUrl: [], + updateIncidentJson: [], + updateIncidentMethod: [], + updateIncidentUrl: [], }, }, secrets: { @@ -71,22 +101,51 @@ describe('webhook connector validation', () => { password: '', }, id: 'test', - actionTypeId: '.webhook', - name: 'webhook', + actionTypeId: '.cases-webhook', + name: 'Jira Webhook', + isDeprecated: false, isPreconfigured: false, config: { - method: 'PUT', - url: 'http://test.com', - headers: { 'content-type': 'text' }, + createCommentJson: '{"body":"$COMMENT"}', + createCommentMethod: 'post', + createCommentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/$ID/comment', + createIncidentJson: + '{"fields":{"summary":"$SUM","description":"$DESC","project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', + createIncidentMethod: 'post', + createIncidentResponseKey: 'id', + createIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue', + getIncidentResponseCreatedDateKey: 'fields.created', + getIncidentResponseExternalTitleKey: 'key', + getIncidentResponseUpdatedDateKey: 'fields.udpated', hasAuth: false, + headers: { 'content-type': 'text' }, + incidentViewUrl: 'https://siem-kibana.atlassian.net/browse/$TITLE', + getIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/$ID', + updateIncidentJson: + '{"fields":{"summary":"$SUM","description":"$DESC","project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', + updateIncidentMethod: 'put', + updateIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/$ID', }, - } as WebhookActionConnector; + } as CasesWebhookActionConnector; expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ config: { errors: { - url: [], - method: [], + createCommentJson: [], + createCommentMethod: [], + createCommentUrl: [], + createIncidentJson: [], + createIncidentMethod: [], + createIncidentResponseKey: [], + createIncidentUrl: [], + getIncidentResponseCreatedDateKey: [], + getIncidentResponseExternalTitleKey: [], + getIncidentResponseUpdatedDateKey: [], + incidentViewUrl: [], + getIncidentUrl: [], + updateIncidentJson: [], + updateIncidentMethod: [], + updateIncidentUrl: [], }, }, secrets: { @@ -102,21 +161,49 @@ describe('webhook connector validation', () => { const actionConnector = { secrets: { user: 'user', + password: '', }, id: 'test', - actionTypeId: '.webhook', - name: 'webhook', + actionTypeId: '.cases-webhook', + name: 'Jira Webhook', + isDeprecated: false, + isPreconfigured: false, config: { - method: 'PUT', + createCommentJson: '{"body":"$COMMENT"}', + createCommentMethod: 'post', + createIncidentJson: + '{"fields":{"summary":"$SUM","description":"$DESC","project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', + createIncidentMethod: 'post', + createIncidentResponseKey: 'id', + getIncidentResponseCreatedDateKey: 'fields.created', + getIncidentResponseExternalTitleKey: 'key', + getIncidentResponseUpdatedDateKey: 'fields.udpated', hasAuth: true, + headers: { 'content-type': 'text' }, + updateIncidentJson: + '{"fields":{"summary":"$SUM","description":"$DESC","project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', + updateIncidentMethod: 'put', }, - } as WebhookActionConnector; + } as unknown as CasesWebhookActionConnector; expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ config: { errors: { - url: ['URL is required.'], - method: [], + createCommentJson: [], + createCommentMethod: [], + createCommentUrl: ['Create comment URL is required.'], + createIncidentJson: [], + createIncidentMethod: [], + createIncidentResponseKey: [], + createIncidentUrl: ['Create incident URL is required.'], + getIncidentResponseCreatedDateKey: [], + getIncidentResponseExternalTitleKey: [], + getIncidentResponseUpdatedDateKey: [], + incidentViewUrl: ['View incident URL is required.'], + getIncidentUrl: ['Get incident URL is required.'], + updateIncidentJson: [], + updateIncidentMethod: [], + updateIncidentUrl: ['Update incident URL is required.'], }, }, secrets: { @@ -135,20 +222,51 @@ describe('webhook connector validation', () => { password: 'pass', }, id: 'test', - actionTypeId: '.webhook', - name: 'webhook', + actionTypeId: '.cases-webhook', + name: 'Jira Webhook', + isDeprecated: false, + isPreconfigured: false, config: { - method: 'PUT', - url: 'invalid.url', + createCommentJson: '{"body":"$COMMENT"}', + createCommentMethod: 'post', + createCommentUrl: 'invalid.url', + createIncidentJson: + '{"fields":{"summary":"$SUM","description":"$DESC","project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', + createIncidentMethod: 'post', + createIncidentResponseKey: 'id', + createIncidentUrl: 'invalid.url', + getIncidentResponseCreatedDateKey: 'fields.created', + getIncidentResponseExternalTitleKey: 'key', + getIncidentResponseUpdatedDateKey: 'fields.udpated', hasAuth: true, + headers: { 'content-type': 'text' }, + incidentViewUrl: 'invalid.url', + getIncidentUrl: 'invalid.url', + updateIncidentJson: + '{"fields":{"summary":"$SUM","description":"$DESC","project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', + updateIncidentMethod: 'put', + updateIncidentUrl: 'invalid.url', }, - } as WebhookActionConnector; + } as CasesWebhookActionConnector; expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ config: { errors: { - url: ['URL is invalid.'], - method: [], + createCommentJson: [], + createCommentMethod: [], + createCommentUrl: ['Create comment URL is invalid.'], + createIncidentJson: [], + createIncidentMethod: [], + createIncidentResponseKey: [], + createIncidentUrl: ['Create incident URL is invalid.'], + getIncidentResponseCreatedDateKey: [], + getIncidentResponseExternalTitleKey: [], + getIncidentResponseUpdatedDateKey: [], + incidentViewUrl: ['View incident URL is invalid.'], + getIncidentUrl: ['Get incident URL is invalid.'], + updateIncidentJson: [], + updateIncidentMethod: [], + updateIncidentUrl: ['Update incident URL is invalid.'], }, }, secrets: { @@ -164,22 +282,22 @@ describe('webhook connector validation', () => { describe('webhook action params validation', () => { test('action params validation succeeds when action params is valid', async () => { const actionParams = { - body: 'message {test}', + subActionParams: { incident: { summary: 'some title {{test}}' }, comments: [] }, }; expect(await actionTypeModel.validateParams(actionParams)).toEqual({ - errors: { body: [] }, + errors: { 'subActionParams.incident.summary': [] }, }); }); test('params validation fails when body is not valid', async () => { const actionParams = { - body: '', + subActionParams: { incident: { summary: '' }, comments: [] }, }; expect(await actionTypeModel.validateParams(actionParams)).toEqual({ errors: { - body: ['Body is required.'], + 'subActionParams.incident.summary': ['Summary is required.'], }, }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx index b9a973fd05caad..200e69a684402a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx @@ -81,7 +81,7 @@ export function getActionType(): ActionTypeModel< if (action.config.createIncidentUrl && !isValidUrl(action.config.createIncidentUrl)) { configErrors.createIncidentUrl = [ ...configErrors.createIncidentUrl, - translations.URL_INVALID, + translations.URL_INVALID('Create incident'), ]; } if (!action.config.createIncidentJson) { @@ -96,6 +96,12 @@ export function getActionType(): ActionTypeModel< if (!action.config.getIncidentUrl) { configErrors.getIncidentUrl.push(translations.GET_INCIDENT_URL_REQUIRED); } + if (action.config.getIncidentUrl && !isValidUrl(action.config.getIncidentUrl)) { + configErrors.getIncidentUrl = [ + ...configErrors.getIncidentUrl, + translations.URL_INVALID('Get incident'), + ]; + } if (!action.config.getIncidentResponseExternalTitleKey) { configErrors.getIncidentResponseExternalTitleKey.push( translations.GET_RESPONSE_EXTERNAL_TITLE_KEY_REQUIRED @@ -114,13 +120,19 @@ export function getActionType(): ActionTypeModel< if (!action.config.incidentViewUrl) { configErrors.incidentViewUrl.push(translations.GET_INCIDENT_VIEW_URL); } + if (action.config.incidentViewUrl && !isValidUrl(action.config.incidentViewUrl)) { + configErrors.incidentViewUrl = [ + ...configErrors.incidentViewUrl, + translations.URL_INVALID('View incident'), + ]; + } if (!action.config.updateIncidentUrl) { configErrors.updateIncidentUrl.push(translations.UPDATE_URL_REQUIRED); } if (action.config.updateIncidentUrl && !isValidUrl(action.config.updateIncidentUrl)) { configErrors.updateIncidentUrl = [ ...configErrors.updateIncidentUrl, - translations.URL_INVALID, + translations.URL_INVALID('Update incident'), ]; } if (!action.config.updateIncidentJson) { @@ -135,7 +147,7 @@ export function getActionType(): ActionTypeModel< if (action.config.createCommentUrl && !isValidUrl(action.config.createCommentUrl)) { configErrors.createCommentUrl = [ ...configErrors.createCommentUrl, - translations.URL_INVALID, + translations.URL_INVALID('Create comment'), ]; } if (!action.config.createCommentJson) { @@ -164,7 +176,6 @@ export function getActionType(): ActionTypeModel< const translations = await import('./translations'); const errors = { 'subActionParams.incident.summary': new Array(), - 'subActionParams.incident.description': new Array(), }; const validationResult = { errors }; if ( @@ -172,14 +183,7 @@ export function getActionType(): ActionTypeModel< actionParams.subActionParams.incident && !actionParams.subActionParams.incident.summary?.length ) { - errors['subActionParams.incident.summary'].push(translations.BODY_REQUIRED); - } - if ( - actionParams.subActionParams && - actionParams.subActionParams.incident && - !actionParams.subActionParams.incident.description?.length - ) { - errors['subActionParams.incident.description'].push(translations.BODY_REQUIRED); + errors['subActionParams.incident.summary'].push(translations.SUMMARY_REQUIRED); } return validationResult; }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx index 962a8090102ba9..741b4765d0a8a4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx @@ -7,8 +7,61 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test-jest-helpers'; -import { CasesWebhookActionConnector, WebhookActionConnector } from '../types'; +import { CasesWebhookActionConnector } from './types'; import WebhookActionConnectorFields from './webhook_connectors'; +import { MockCodeEditor } from '../../../code_editor.mock'; +jest.mock('../../../../common/lib/kibana'); +const kibanaReactPath = '../../../../../../../../src/plugins/kibana_react/public'; + +jest.mock(kibanaReactPath, () => { + const original = jest.requireActual(kibanaReactPath); + return { + ...original, + CodeEditor: (props: any) => { + return ; + }, + }; +}); + +const config = { + createCommentJson: '{"body":"$COMMENT"}', + createCommentMethod: 'post', + createCommentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/$ID/comment', + createIncidentJson: + '{"fields":{"summary":"$SUM","description":"$DESC","project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', + createIncidentMethod: 'post', + createIncidentResponseKey: 'id', + createIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue', + getIncidentResponseCreatedDateKey: 'fields.created', + getIncidentResponseExternalTitleKey: 'key', + getIncidentResponseUpdatedDateKey: 'fields.udpated', + hasAuth: true, + headers: { 'content-type': 'text' }, + incidentViewUrl: 'https://siem-kibana.atlassian.net/browse/$TITLE', + getIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/$ID', + updateIncidentJson: + '{"fields":{"summary":"$SUM","description":"$DESC","project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', + updateIncidentMethod: 'put', + updateIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/$ID', +}; + +const configErrors = { + createCommentJson: [], + createCommentMethod: [], + createCommentUrl: [], + createIncidentJson: [], + createIncidentMethod: [], + createIncidentResponseKey: [], + createIncidentUrl: [], + getIncidentResponseCreatedDateKey: [], + getIncidentResponseExternalTitleKey: [], + getIncidentResponseUpdatedDateKey: [], + incidentViewUrl: [], + getIncidentUrl: [], + updateIncidentJson: [], + updateIncidentMethod: [], + updateIncidentUrl: [], +}; describe('WebhookActionConnectorFields renders', () => { test('all connector fields is rendered', () => { @@ -18,23 +71,16 @@ describe('WebhookActionConnectorFields renders', () => { password: 'pass', }, id: 'test', - actionTypeId: '.webhook-cases', + actionTypeId: '.cases-webhook', isPreconfigured: false, isDeprecated: false, name: 'cases webhook', - config: { - method: 'PUT', - url: 'http:\\test', - incident: - '{"fields":{"summary":"$SUM","description":"$DESC","project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', - headers: { 'content-type': 'text' }, - hasAuth: true, - }, + config, } as CasesWebhookActionConnector; const wrapper = mountWithIntl( {}} editActionSecrets={() => {}} readOnly={false} @@ -42,29 +88,53 @@ describe('WebhookActionConnectorFields renders', () => { isEdit={false} /> ); - expect(wrapper.find('[data-test-subj="webhookViewHeadersSwitch"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="webhookHeaderText"]').length > 0).toBeTruthy(); - wrapper.find('[data-test-subj="webhookViewHeadersSwitch"]').first().simulate('click'); - expect(wrapper.find('[data-test-subj="webhookMethodSelect"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="webhookUrlText"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="webhookCreateMethodSelect"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="webhookCreateUrlText"]').length > 0).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="createIncidentResponseKeyText"]').length > 0 + ).toBeTruthy(); + expect(wrapper.find('[data-test-subj="webhookCreateUrlText"]').length > 0).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="getIncidentResponseExternalTitleKeyText"]').length > 0 + ).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="getIncidentResponseCreatedDateKeyText"]').length > 0 + ).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="getIncidentResponseUpdatedDateKeyText"]').length > 0 + ).toBeTruthy(); + expect(wrapper.find('[data-test-subj="incidentViewUrlText"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="webhookUpdateMethodSelect"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="webhookUpdateUrlText"]').length > 0).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="webhookCreateCommentMethodSelect"]').length > 0 + ).toBeTruthy(); + expect(wrapper.find('[data-test-subj="webhookUpdateUrlText"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="webhookUserInput"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="webhookPasswordInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="webhookViewHeadersSwitch"]').length > 0).toBeTruthy(); + + expect(wrapper.find('[data-test-subj="webhookViewHeadersSwitch"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="webhookHeaderText"]').length > 0).toBeTruthy(); + wrapper.find('[data-test-subj="webhookViewHeadersSwitch"]').last().simulate('click'); + expect(wrapper.find('[data-test-subj="webhookAddHeaderButton"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="webhookHeadersKeyInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="webhookHeadersValueInput"]').length > 0).toBeTruthy(); }); test('should display a message on create to remember credentials', () => { const actionConnector = { secrets: {}, - actionTypeId: '.webhook-cases', + actionTypeId: '.cases-webhook', isPreconfigured: false, isDeprecated: false, - config: { - hasAuth: true, - }, - } as WebhookActionConnector; + name: 'cases webhook', + config, + } as unknown as CasesWebhookActionConnector; const wrapper = mountWithIntl( {}} editActionSecrets={() => {}} readOnly={false} @@ -83,19 +153,12 @@ describe('WebhookActionConnectorFields renders', () => { password: 'pass', }, id: 'test', - actionTypeId: '.webhook', + actionTypeId: '.cases-webhook', isPreconfigured: false, - isDeprecated: false, + isDeprecated: true, name: 'webhook', - config: { - method: 'PUT', - url: 'http:\\test', - incident: - '{"fields":{"summary":"$SUM","description":"$DESC","project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', - headers: { 'content-type': 'text' }, - hasAuth: true, - }, - } as WebhookActionConnector; + config, + } as CasesWebhookActionConnector; const wrapper = mountWithIntl( { isMissingSecrets: true, isDeprecated: false, name: 'webhook', - config: { - method: 'PUT', - url: 'http:\\test', - incident: - '{"fields":{"summary":"$SUM","description":"$DESC","project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', - headers: { 'content-type': 'text' }, - hasAuth: true, - }, - } as WebhookActionConnector; + config, + } as CasesWebhookActionConnector; const wrapper = mountWithIntl( { }; }); +const actionParams = { + subAction: 'pushToService', + subActionParams: { + incident: { + summary: 'sn title', + description: 'some description', + labels: ['kibana'], + externalId: null, + }, + comments: [], + }, +}; + describe('WebhookParamsFields renders', () => { test('all params fields is rendered', () => { - const actionParams = { - body: 'test message', - }; - const wrapper = mountWithIntl( { ]} /> ); - expect(wrapper.find('[data-test-subj="bodyJsonEditor"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="bodyJsonEditor"]').first().prop('value')).toStrictEqual( - 'test message' - ); - expect(wrapper.find('[data-test-subj="bodyAddVariableButton"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="summaryInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="descriptionTextArea"]').length > 0).toBeTruthy(); }); }); From 9d2e57c3d9df87e6f058777e718b563b33f86441 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 26 May 2022 08:55:06 -0600 Subject: [PATCH 24/97] cleanup --- .../common/api/connectors/cases_webhook.ts | 5 - .../cases/common/api/connectors/index.ts | 4 +- .../cases/common/api/connectors/jira.ts | 1 - .../cases/common/api/connectors/resilient.ts | 1 - .../common/api/connectors/servicenow_itsm.ts | 1 - .../common/api/connectors/servicenow_sir.ts | 1 - .../cases/common/api/connectors/swimlane.ts | 1 - .../components/configure_cases/connectors.tsx | 1 + .../components/configure_cases/mapping.tsx | 17 +- .../configure_cases/translations.ts | 8 + .../connectors/cases_webhook/case_fields.tsx | 43 +- .../connectors/cases_webhook/index.ts | 7 - .../connectors/cases_webhook/translations.ts | 42 - .../connectors/cases_webhook/validator.ts | 38 - .../cases_webhook/translations.ts | 293 +++++++ .../cases_webhook/webhook_connectors.tsx | 765 ++++++------------ 16 files changed, 571 insertions(+), 657 deletions(-) delete mode 100644 x-pack/plugins/cases/public/components/connectors/cases_webhook/translations.ts delete mode 100644 x-pack/plugins/cases/public/components/connectors/cases_webhook/validator.ts diff --git a/x-pack/plugins/cases/common/api/connectors/cases_webhook.ts b/x-pack/plugins/cases/common/api/connectors/cases_webhook.ts index 5a597adedb0d5e..07db50f282e608 100644 --- a/x-pack/plugins/cases/common/api/connectors/cases_webhook.ts +++ b/x-pack/plugins/cases/common/api/connectors/cases_webhook.ts @@ -7,14 +7,9 @@ import * as rt from 'io-ts'; -// New fields should also be added at: x-pack/plugins/cases/server/connectors/case/schema.ts export const CasesWebhookFieldsRT = rt.record( rt.string, rt.union([rt.string, rt.array(rt.string), rt.undefined, rt.null]) ); export type CasesWebhookFieldsType = rt.TypeOf; - -export enum CasesWebhookConnectorType { - Cases = 'cases', -} diff --git a/x-pack/plugins/cases/common/api/connectors/index.ts b/x-pack/plugins/cases/common/api/connectors/index.ts index f72991fa06e9b8..4d87c6ade07b01 100644 --- a/x-pack/plugins/cases/common/api/connectors/index.ts +++ b/x-pack/plugins/cases/common/api/connectors/index.ts @@ -53,8 +53,6 @@ export enum ConnectorTypes { swimlane = '.swimlane', } -export const connectorTypes = Object.values(ConnectorTypes); - const ConnectorCasesWebhookTypeFieldsRt = rt.type({ type: rt.literal(ConnectorTypes.casesWebhook), fields: rt.union([CasesWebhookFieldsRT, rt.null]), @@ -62,7 +60,7 @@ const ConnectorCasesWebhookTypeFieldsRt = rt.type({ const ConnectorJiraTypeFieldsRt = rt.type({ type: rt.literal(ConnectorTypes.jira), - fields: rt.union([CasesWebhookFieldsRT, rt.null]), + fields: rt.union([JiraFieldsRT, rt.null]), }); const ConnectorResilientTypeFieldsRt = rt.type({ diff --git a/x-pack/plugins/cases/common/api/connectors/jira.ts b/x-pack/plugins/cases/common/api/connectors/jira.ts index b8a8d0147420eb..15a6768b075616 100644 --- a/x-pack/plugins/cases/common/api/connectors/jira.ts +++ b/x-pack/plugins/cases/common/api/connectors/jira.ts @@ -7,7 +7,6 @@ import * as rt from 'io-ts'; -// New fields should also be added at: x-pack/plugins/cases/server/connectors/case/schema.ts export const JiraFieldsRT = rt.type({ issueType: rt.union([rt.string, rt.null]), priority: rt.union([rt.string, rt.null]), diff --git a/x-pack/plugins/cases/common/api/connectors/resilient.ts b/x-pack/plugins/cases/common/api/connectors/resilient.ts index d28c96968d6e59..d19aa5b21fb52c 100644 --- a/x-pack/plugins/cases/common/api/connectors/resilient.ts +++ b/x-pack/plugins/cases/common/api/connectors/resilient.ts @@ -7,7 +7,6 @@ import * as rt from 'io-ts'; -// New fields should also be added at: x-pack/plugins/cases/server/connectors/case/schema.ts export const ResilientFieldsRT = rt.type({ incidentTypes: rt.union([rt.array(rt.string), rt.null]), severityCode: rt.union([rt.string, rt.null]), diff --git a/x-pack/plugins/cases/common/api/connectors/servicenow_itsm.ts b/x-pack/plugins/cases/common/api/connectors/servicenow_itsm.ts index 6a5453561c73da..c3cf298a6aade5 100644 --- a/x-pack/plugins/cases/common/api/connectors/servicenow_itsm.ts +++ b/x-pack/plugins/cases/common/api/connectors/servicenow_itsm.ts @@ -7,7 +7,6 @@ import * as rt from 'io-ts'; -// New fields should also be added at: x-pack/plugins/cases/server/connectors/case/schema.ts export const ServiceNowITSMFieldsRT = rt.type({ impact: rt.union([rt.string, rt.null]), severity: rt.union([rt.string, rt.null]), diff --git a/x-pack/plugins/cases/common/api/connectors/servicenow_sir.ts b/x-pack/plugins/cases/common/api/connectors/servicenow_sir.ts index 3da8e694b473df..749abdea87437b 100644 --- a/x-pack/plugins/cases/common/api/connectors/servicenow_sir.ts +++ b/x-pack/plugins/cases/common/api/connectors/servicenow_sir.ts @@ -7,7 +7,6 @@ import * as rt from 'io-ts'; -// New fields should also be added at: x-pack/plugins/cases/server/connectors/case/schema.ts export const ServiceNowSIRFieldsRT = rt.type({ category: rt.union([rt.string, rt.null]), destIp: rt.union([rt.boolean, rt.null]), diff --git a/x-pack/plugins/cases/common/api/connectors/swimlane.ts b/x-pack/plugins/cases/common/api/connectors/swimlane.ts index bc4d9df9ae6a0f..a5bf60edbf1cdd 100644 --- a/x-pack/plugins/cases/common/api/connectors/swimlane.ts +++ b/x-pack/plugins/cases/common/api/connectors/swimlane.ts @@ -7,7 +7,6 @@ import * as rt from 'io-ts'; -// New fields should also be added at: x-pack/plugins/cases/server/connectors/case/schema.ts export const SwimlaneFieldsRT = rt.type({ caseId: rt.union([rt.string, rt.null]), }); diff --git a/x-pack/plugins/cases/public/components/configure_cases/connectors.tsx b/x-pack/plugins/cases/public/components/configure_cases/connectors.tsx index 4b608246a4c222..0db2c0b0feee1a 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/connectors.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/connectors.tsx @@ -129,6 +129,7 @@ const ConnectorsComponent: React.FC = ({ diff --git a/x-pack/plugins/cases/public/components/configure_cases/mapping.tsx b/x-pack/plugins/cases/public/components/configure_cases/mapping.tsx index 4ce971d6528794..1f7736041d5612 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/mapping.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/mapping.tsx @@ -10,6 +10,7 @@ import React, { useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiText, EuiTextColor } from '@elastic/eui'; import { TextColor } from '@elastic/eui/src/components/text/text_color'; +import { ConnectorTypes } from '../../../common/api'; import * as i18n from './translations'; import { FieldMapping } from './field_mapping'; @@ -17,11 +18,17 @@ import { CaseConnectorMapping } from '../../containers/configure/types'; export interface MappingProps { actionTypeName: string; + connectorType: ConnectorTypes; isLoading: boolean; mappings: CaseConnectorMapping[]; } -const MappingComponent: React.FC = ({ actionTypeName, isLoading, mappings }) => { +const MappingComponent: React.FC = ({ + actionTypeName, + connectorType, + isLoading, + mappings, +}) => { const fieldMappingDesc: { desc: string; color: TextColor } = useMemo( () => mappings.length > 0 || isLoading @@ -29,12 +36,18 @@ const MappingComponent: React.FC = ({ actionTypeName, isLoading, m desc: i18n.FIELD_MAPPING_DESC(actionTypeName), color: 'subdued', } + : connectorType === ConnectorTypes.casesWebhook + ? { + desc: i18n.CASES_WEBHOOK_MAPPINGS, + color: 'subdued', + } : { desc: i18n.FIELD_MAPPING_DESC_ERR(actionTypeName), color: 'danger', }, - [isLoading, mappings.length, actionTypeName] + [mappings.length, isLoading, actionTypeName, connectorType] ); + return ( diff --git a/x-pack/plugins/cases/public/components/configure_cases/translations.ts b/x-pack/plugins/cases/public/components/configure_cases/translations.ts index ba457905590ab7..070c934bbde46e 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/translations.ts +++ b/x-pack/plugins/cases/public/components/configure_cases/translations.ts @@ -173,3 +173,11 @@ export const DEPRECATED_TOOLTIP_CONTENT = i18n.translate( export const CONFIGURE_CASES_PAGE_TITLE = i18n.translate('xpack.cases.configureCases.headerTitle', { defaultMessage: 'Configure cases', }); + +export const CASES_WEBHOOK_MAPPINGS = i18n.translate( + 'xpack.cases.configureCases.casesWebhookMappings', + { + defaultMessage: + 'Cases Webhook field mappings are configured in the connector settings in the third-party REST API JSON.', + } +); diff --git a/x-pack/plugins/cases/public/components/connectors/cases_webhook/case_fields.tsx b/x-pack/plugins/cases/public/components/connectors/cases_webhook/case_fields.tsx index b9eefdd8869447..94dc7aed771dfc 100644 --- a/x-pack/plugins/cases/public/components/connectors/cases_webhook/case_fields.tsx +++ b/x-pack/plugins/cases/public/components/connectors/cases_webhook/case_fields.tsx @@ -5,43 +5,26 @@ * 2.0. */ -import React, { useMemo } from 'react'; -import { EuiCallOut } from '@elastic/eui'; -import * as i18n from './translations'; +import React from 'react'; import { ConnectorTypes, CasesWebhookFieldsType } from '../../../../common/api'; import { ConnectorFieldsProps } from '../types'; import { ConnectorCard } from '../card'; -import { connectorValidator } from './validator'; const CasesWebhookComponent: React.FunctionComponent< ConnectorFieldsProps -> = ({ connector, isEdit = true }) => { - const showMappingWarning = useMemo(() => connectorValidator(connector) != null, [connector]); - - return ( - <> - {!isEdit && ( - - )} - {showMappingWarning && ( - - {i18n.EMPTY_MAPPING_WARNING_DESC} - - )} - - ); -}; +> = ({ connector, isEdit = true }) => ( + <> + {!isEdit && ( + + )} + +); CasesWebhookComponent.displayName = 'CasesWebhook'; // eslint-disable-next-line import/no-default-export diff --git a/x-pack/plugins/cases/public/components/connectors/cases_webhook/index.ts b/x-pack/plugins/cases/public/components/connectors/cases_webhook/index.ts index 76ea641fc7e4a7..dac2cb5bd1f54f 100644 --- a/x-pack/plugins/cases/public/components/connectors/cases_webhook/index.ts +++ b/x-pack/plugins/cases/public/components/connectors/cases_webhook/index.ts @@ -9,7 +9,6 @@ import { lazy } from 'react'; import { CaseConnector } from '../types'; import { ConnectorTypes, CasesWebhookFieldsType } from '../../../../common/api'; -import * as i18n from './translations'; export const getCaseConnector = (): CaseConnector => { return { @@ -17,9 +16,3 @@ export const getCaseConnector = (): CaseConnector => { fieldsComponent: lazy(() => import('./case_fields')), }; }; - -export const fieldLabels = { - caseId: i18n.CASE_ID_LABEL, - caseName: i18n.CASE_NAME_LABEL, - severity: i18n.SEVERITY_LABEL, -}; diff --git a/x-pack/plugins/cases/public/components/connectors/cases_webhook/translations.ts b/x-pack/plugins/cases/public/components/connectors/cases_webhook/translations.ts deleted file mode 100644 index 7dba4c676cbc28..00000000000000 --- a/x-pack/plugins/cases/public/components/connectors/cases_webhook/translations.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const ALERT_SOURCE_LABEL = i18n.translate( - 'xpack.cases.connectors.casesWebhook.alertSourceLabel', - { - defaultMessage: 'Alert Source', - } -); - -export const CASE_ID_LABEL = i18n.translate('xpack.cases.connectors.casesWebhook.caseIdLabel', { - defaultMessage: 'Case Id', -}); - -export const CASE_NAME_LABEL = i18n.translate('xpack.cases.connectors.casesWebhook.caseNameLabel', { - defaultMessage: 'Case Name', -}); - -export const SEVERITY_LABEL = i18n.translate('xpack.cases.connectors.casesWebhook.severityLabel', { - defaultMessage: 'Severity', -}); - -export const EMPTY_MAPPING_WARNING_TITLE = i18n.translate( - 'xpack.cases.connectors.casesWebhook.emptyMappingWarningTitle', - { - defaultMessage: 'This connector has missing field mappings', - } -); - -export const EMPTY_MAPPING_WARNING_DESC = i18n.translate( - 'xpack.cases.connectors.casesWebhook.emptyMappingWarningDesc', - { - defaultMessage: - 'This connector cannot be selected because it is missing the required case field mappings. You can edit this connector to add required field mappings or select a connector of type Cases.', - } -); diff --git a/x-pack/plugins/cases/public/components/connectors/cases_webhook/validator.ts b/x-pack/plugins/cases/public/components/connectors/cases_webhook/validator.ts deleted file mode 100644 index f8a132803ae37f..00000000000000 --- a/x-pack/plugins/cases/public/components/connectors/cases_webhook/validator.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ValidationConfig } from '../../../common/shared_imports'; -import { CaseActionConnector } from '../../types'; - -const casesRequiredFields = [ - 'caseIdConfig', - 'caseNameConfig', - 'descriptionConfig', - 'commentsConfig', -]; - -export const isAnyRequiredFieldNotSet = (mapping: Record | undefined) => - casesRequiredFields.some((field) => mapping?.[field] == null); - -/** - * The user can use either a connector of type cases or all. - * If the connector is of type all we should check if all - * required field have been configured. - */ - -export const connectorValidator = ( - connector: CaseActionConnector -): ReturnType => { - const { - config: { mappings, connectorType }, - } = connector; - // if (connectorType !== CasesWebhookConnectorType.Cases || isAnyRequiredFieldNotSet(mappings)) { - // return { - // message: 'Invalid connector', - // }; - // } -}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts index 0011ccc952de3a..25f1477e5aee3d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts @@ -150,3 +150,296 @@ export const SUMMARY_REQUIRED = i18n.translate( defaultMessage: 'Summary is required.', } ); + +export const KEY_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.sections.addAction.casesWebhookAction.error.requiredHeaderKeyText', + { + defaultMessage: 'Key is required.', + } +); + +export const VALUE_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.sections.addAction.casesWebhookAction.error.requiredHeaderValueText', + { + defaultMessage: 'Value is required.', + } +); + +export const ADD_HEADER = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.addHeader', + { + defaultMessage: 'Add header', + } +); + +export const KEY_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.keyTextFieldLabel', + { + defaultMessage: 'Key', + } +); + +export const VALUE_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.valueTextFieldLabel', + { + defaultMessage: 'Value', + } +); + +export const ADD_BUTTON = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.addHeaderButton', + { + defaultMessage: 'Add', + } +); + +export const DELETE_BUTTON = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.deleteHeaderButton', + { + defaultMessage: 'Delete', + description: 'Delete HTTP header', + } +); + +export const CREATE_INCIDENT_METHOD = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.createIncidentMethodTextFieldLabel', + { + defaultMessage: 'Create Incident Method', + } +); + +export const CREATE_INCIDENT_URL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.createIncidentUrlTextFieldLabel', + { + defaultMessage: 'Create Incident URL', + } +); + +export const CREATE_INCIDENT_JSON = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.createIncidentJsonTextFieldLabel', + { + defaultMessage: 'Create Incident JSON', + } +); + +export const CREATE_INCIDENT_JSON_HELP = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.createIncidentJsonHelpText', + { + defaultMessage: + 'JSON object to create incident. Sub $SUM where the summary/title should go, $DESC where the description should go, and $TAGS where tags should go (optional).', + } +); + +export const JSON = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.jsonFieldLabel', + { + defaultMessage: 'JSON', + } +); +export const CODE_EDITOR = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.jsonCodeEditorAriaLabel', + { + defaultMessage: 'Code editor', + } +); + +export const CREATE_INCIDENT_RESPONSE_KEY = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.createIncidentResponseKeyTextFieldLabel', + { + defaultMessage: 'Create Incident Response Incident Key', + } +); + +export const CREATE_INCIDENT_RESPONSE_KEY_HELP = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.createIncidentResponseKeyHelpText', + { + defaultMessage: 'JSON key in create incident response that contains the external incident id', + } +); + +export const GET_INCIDENT_URL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.getIncidentUrlTextFieldLabel', + { + defaultMessage: 'Get Incident URL', + } +); +export const GET_INCIDENT_URL_HELP = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.getIncidentUrlHelp', + { + defaultMessage: + 'API URL to GET incident details JSON from external system. Use $ID and Kibana will dynamically update the url with the external incident id.', + } +); + +export const GET_INCIDENT_TITLE_KEY = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.getIncidentResponseExternalTitleKeyTextFieldLabel', + { + defaultMessage: 'Get Incident Response External Title Key', + } +); +export const GET_INCIDENT_TITLE_KEY_HELP = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.getIncidentResponseExternalTitleKeyHelp', + { + defaultMessage: 'JSON key in get incident response that contains the external incident title', + } +); + +export const GET_INCIDENT_CREATED_KEY = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.getIncidentResponseCreatedDateKeyTextFieldLabel', + { + defaultMessage: 'Get Incident Response Created Date Key', + } +); +export const GET_INCIDENT_CREATED_KEY_HELP = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.getIncidentResponseCreatedDateKeyHelp', + { + defaultMessage: + 'JSON key in get incident response that contains the date the incident was created.', + } +); + +export const GET_INCIDENT_UPDATED_KEY = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.getIncidentResponseUpdatedDateKeyTextFieldLabel', + { + defaultMessage: 'Get Incident Response Updated Date Key', + } +); +export const GET_INCIDENT_UPDATED_KEY_HELP = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.getIncidentResponseUpdatedDateKeyHelp', + { + defaultMessage: + 'JSON key in get incident response that contains the date the incident was updated.', + } +); + +export const EXTERNAL_INCIDENT_VIEW_URL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.incidentViewUrlTextFieldLabel', + { + defaultMessage: 'External Incident View URL', + } +); +export const EXTERNAL_INCIDENT_VIEW_URL_HELP = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.incidentViewUrlHelp', + { + defaultMessage: + 'URL to view incident in external system. Use $ID or $TITLE and Kibana will dynamically update the url with the external incident id or external incident title.', + } +); + +export const UPDATE_INCIDENT_METHOD = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.updateIncidentMethodTextFieldLabel', + { + defaultMessage: 'Update Incident Method', + } +); + +export const UPDATE_INCIDENT_URL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.updateIncidentUrlTextFieldLabel', + { + defaultMessage: 'Update Incident URL', + } +); +export const UPDATE_INCIDENT_URL_HELP = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.updateIncidentUrlHelp', + { + defaultMessage: + 'API URL to update incident. Use $ID and Kibana will dynamically update the url with the external incident id.', + } +); + +export const UPDATE_INCIDENT_JSON = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.updateIncidentJsonTextFieldLabel', + { + defaultMessage: 'Update Incident JSON', + } +); +export const UPDATE_INCIDENT_JSON_HELP = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.updateIncidentJsonHelpl', + { + defaultMessage: + 'JSON object to update incident. Sub $SUM where the summary/title should go, $DESC where the description should go, and $TAGS where tags should go (optional).', + } +); + +export const CREATE_COMMENT_METHOD = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.createCommentMethodTextFieldLabel', + { + defaultMessage: 'Create Comment Method', + } +); +export const CREATE_COMMENT_URL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.createCommentUrlTextFieldLabel', + { + defaultMessage: 'Create Comment URL', + } +); + +export const CREATE_COMMENT_URL_HELP = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.createCommentUrlHelp', + { + defaultMessage: + 'API URL to add comment to incident. Use $ID and Kibana will dynamically update the url with the external incident id.', + } +); + +export const CREATE_COMMENT_JSON = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.createCommentJsonTextFieldLabel', + { + defaultMessage: 'Create Comment JSON', + } +); +export const CREATE_COMMENT_JSON_HELP = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.createCommentJsonHelp', + { + defaultMessage: 'JSON object to update incident. Sub $COMMENT where the comment should go', + } +); + +export const HAS_AUTH = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.hasAuthSwitchLabel', + { + defaultMessage: 'Require authentication for this webhook', + } +); + +export const REENTER_VALUES = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.reenterValuesLabel', + { + defaultMessage: 'Username and password are encrypted. Please reenter values for these fields.', + } +); + +export const USERNAME = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.userTextFieldLabel', + { + defaultMessage: 'Username', + } +); + +export const PASSWORD = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.passwordTextFieldLabel', + { + defaultMessage: 'Password', + } +); + +export const HEADERS_SWITCH = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.viewHeadersSwitch', + { + defaultMessage: 'Add HTTP header', + } +); + +export const HEADERS_TITLE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.httpHeadersTitle', + { + defaultMessage: 'Headers in use', + } +); + +export const AUTH_TITLE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.authenticationLabel', + { + defaultMessage: 'Authentication', + } +); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx index c42e55d98aa7f1..1ac1ce6547dfd7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx @@ -5,8 +5,7 @@ * 2.0. */ -import React, { useCallback, useEffect, useState } from 'react'; -import { FormattedMessage } from '@kbn/i18n-react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { EuiFieldPassword, @@ -24,11 +23,11 @@ import { EuiSwitch, EuiButtonEmpty, } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; import { ActionConnectorFieldsProps } from '../../../../types'; import { CasesWebhookActionConnector, CasesWebhookConfig, CasesWebhookSecrets } from './types'; import { getEncryptedFieldNotifyLabel } from '../../get_encrypted_field_notify_label'; import { JsonEditorWithMessageVariables } from '../../json_editor_with_message_variables'; +import * as i18n from './translations'; const HTTP_VERBS = ['post', 'put', 'patch']; @@ -79,37 +78,30 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const headerErrors = { - keyHeader: new Array(), - valueHeader: new Array(), - }; - if (!httpHeaderKey && httpHeaderValue) { - headerErrors.keyHeader.push( - i18n.translate( - 'xpack.triggersActionsUI.sections.addAction.casesWebhookAction.error.requiredHeaderKeyText', - { - defaultMessage: 'Key is required.', - } - ) - ); - } - if (httpHeaderKey && !httpHeaderValue) { - headerErrors.valueHeader.push( - i18n.translate( - 'xpack.triggersActionsUI.sections.addAction.casesWebhookAction.error.requiredHeaderValueText', - { - defaultMessage: 'Value is required.', - } - ) - ); - } - const hasHeaderErrors: boolean = - (headerErrors.keyHeader !== undefined && - headerErrors.valueHeader !== undefined && - headerErrors.keyHeader.length > 0) || - headerErrors.valueHeader.length > 0; + const headerErrors = useMemo(() => { + const hErrors = { + keyHeader: new Array(), + valueHeader: new Array(), + }; + if (!httpHeaderKey && httpHeaderValue) { + hErrors.keyHeader.push(i18n.KEY_REQUIRED); + } + if (httpHeaderKey && !httpHeaderValue) { + hErrors.valueHeader.push(i18n.VALUE_REQUIRED); + } + return hErrors; + }, [httpHeaderKey, httpHeaderValue]); + + const hasHeaderErrors: boolean = useMemo( + () => + (headerErrors.keyHeader !== undefined && + headerErrors.valueHeader !== undefined && + headerErrors.keyHeader.length > 0) || + headerErrors.valueHeader.length > 0, + [headerErrors.keyHeader, headerErrors.valueHeader] + ); - function addHeader() { + const addHeader = useCallback(() => { if (headers && !!Object.keys(headers).find((key) => key === httpHeaderKey)) { return; } @@ -119,139 +111,136 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< editActionConfig('headers', updatedHeaders); setHttpHeaderKey(''); setHttpHeaderValue(''); - } + }, [editActionConfig, headers, httpHeaderKey, httpHeaderValue]); - function viewHeaders() { + const viewHeaders = useCallback(() => { setHasHeaders(!hasHeaders); if (!hasHeaders && !headers) { editActionConfig('headers', {}); } - } + }, [editActionConfig, hasHeaders, headers]); - function removeHeader(keyToRemove: string) { - const updatedHeaders = - headers != null - ? Object.keys(headers) - .filter((key) => key !== keyToRemove) - .reduce((headerToRemove: Record, key: string) => { - headerToRemove[key] = headers[key]; - return headerToRemove; - }, {}) - : {}; - editActionConfig('headers', updatedHeaders); - } + const removeHeader = useCallback( + (keyToRemove: string) => { + const updatedHeaders = + headers != null + ? Object.keys(headers) + .filter((key) => key !== keyToRemove) + .reduce((headerToRemove: Record, key: string) => { + headerToRemove[key] = headers[key]; + return headerToRemove; + }, {}) + : {}; + editActionConfig('headers', updatedHeaders); + }, + [editActionConfig, headers] + ); - let headerControl; - if (hasHeaders) { - headerControl = ( - <> - -
- -
-
- - - - - { + if (hasHeaders) { + return ( + <> + +
{i18n.ADD_HEADER}
+
+ + + + { - setHttpHeaderKey(e.target.value); - }} - /> - - - - - + { + setHttpHeaderKey(e.target.value); + }} + /> + + + + { - setHttpHeaderValue(e.target.value); - }} - /> - - - - - - { + setHttpHeaderValue(e.target.value); + }} /> - - - - - - ); - } +
+
+ + + + {i18n.ADD_BUTTON} + + + +
+ + ); + } + return null; + }, [ + addHeader, + hasHeaderErrors, + hasHeaders, + headerErrors.keyHeader, + headerErrors.valueHeader, + httpHeaderKey, + httpHeaderValue, + readOnly, + ]); + + const headersList = useMemo( + () => + Object.keys(headers || {}).map((key: string) => { + return ( + + + removeHeader(key)} + /> + + + + {key} + + {headers && headers[key]} + + + + + ); + }), + [headers, removeHeader] + ); - const headersList = Object.keys(headers || {}).map((key: string) => { - return ( - - - removeHeader(key)} - /> - - - - {key} - {headers && headers[key]} - - - - ); - }); const isConfigKeyValueInvalid = useCallback( (key: keyof CasesWebhookConfig): boolean => errors[key] !== undefined && errors[key].length > 0 && action.config[key] !== undefined, @@ -263,28 +252,46 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< [action.secrets, errors] ); + const onConfigChange = useCallback( + (key: keyof CasesWebhookConfig, value: string) => editActionConfig(key, value), + [editActionConfig] + ); + + const onConfigBlur = useCallback( + (key: keyof CasesWebhookConfig) => { + if (!action.config[key]) { + editActionConfig(key, ''); + } + }, + [action.config, editActionConfig] + ); + const onSecretsChange = useCallback( + (key: keyof CasesWebhookSecrets, value: string) => editActionSecrets(key, value), + [editActionSecrets] + ); + + const onSecretsBlur = useCallback( + (key: keyof CasesWebhookSecrets) => { + if (!action.secrets[key]) { + editActionSecrets(key, ''); + } + }, + [action.secrets, editActionSecrets] + ); + return ( <> {/* start CREATE INCIDENT INPUTS */} - + ({ text: verb.toUpperCase(), value: verb }))} - onChange={(e) => { - editActionConfig('createIncidentMethod', e.target.value); - }} + onChange={(e) => onConfigChange('createIncidentMethod', e.target.value)} /> @@ -294,12 +301,7 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< fullWidth error={errors.createIncidentUrl} isInvalid={isConfigKeyValueInvalid('createIncidentUrl')} - label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.createIncidentUrlTextFieldLabel', - { - defaultMessage: 'Create Incident URL', - } - )} + label={i18n.CREATE_INCIDENT_URL} > { - editActionConfig('createIncidentUrl', e.target.value); - }} - onBlur={() => { - if (!createIncidentUrl) { - editActionConfig('createIncidentUrl', ''); - } - }} + onChange={(e) => onConfigChange('createIncidentUrl', e.target.value)} + onBlur={() => onConfigBlur('createIncidentUrl')} />
@@ -326,44 +322,17 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< id="createIncidentJson" fullWidth isInvalid={isConfigKeyValueInvalid('createIncidentJson')} - label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.createIncidentJsonTextFieldLabel', - { - defaultMessage: 'Create Incident Object', - } - )} - helpText={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.createIncidentJsonTextFieldLabel', - { - defaultMessage: - 'JSON object to create incident. Sub $SUM where the summary/title should go, $DESC where the description should go, and $TAGS where tags should go (optional).', - } - )} + label={i18n.CREATE_INCIDENT_JSON} + helpText={i18n.CREATE_INCIDENT_JSON_HELP} > { - editActionConfig('createIncidentJson', json); - }} - onBlur={() => { - if (!createIncidentJson) { - editActionConfig('createIncidentJson', ''); - } - }} + onDocumentsChange={(json: string) => onConfigChange('createIncidentJson', json)} + onBlur={() => onConfigBlur('createIncidentJson')} />
@@ -375,19 +344,8 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< fullWidth error={errors.createIncidentResponseKey} isInvalid={isConfigKeyValueInvalid('createIncidentResponseKey')} - label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.createIncidentResponseKeyTextFieldLabel', - { - defaultMessage: 'Create Incident Response Incident Key', - } - )} - helpText={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.createIncidentResponseKeyTextFieldLabel', - { - defaultMessage: - 'JSON key in create incident response that contains the external incident id', - } - )} + label={i18n.CREATE_INCIDENT_RESPONSE_KEY} + helpText={i18n.CREATE_INCIDENT_RESPONSE_KEY_HELP} > { - editActionConfig('createIncidentResponseKey', e.target.value); - }} - onBlur={() => { - if (!createIncidentResponseKey) { - editActionConfig('createIncidentResponseKey', ''); - } - }} + onChange={(e) => onConfigChange('createIncidentResponseKey', e.target.value)} + onBlur={() => onConfigBlur('createIncidentResponseKey')} /> @@ -417,19 +369,8 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< fullWidth error={errors.getIncidentUrl} isInvalid={isConfigKeyValueInvalid('getIncidentUrl')} - label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.getIncidentUrlTextFieldLabel', - { - defaultMessage: 'Get Incident URL', - } - )} - helpText={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.getIncidentUrlTextFieldLabel', - { - defaultMessage: - 'API URL to GET incident details JSON from external system. Use $ID and Kibana will dynamically update the url with the external incident id.', - } - )} + label={i18n.GET_INCIDENT_URL} + helpText={i18n.GET_INCIDENT_URL_HELP} > { - editActionConfig('getIncidentUrl', e.target.value); - }} - onBlur={() => { - if (!getIncidentUrl) { - editActionConfig('getIncidentUrl', ''); - } - }} + onChange={(e) => onConfigChange('getIncidentUrl', e.target.value)} + onBlur={() => onConfigBlur('getIncidentUrl')} /> @@ -455,19 +390,8 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< fullWidth error={errors.getIncidentResponseExternalTitleKey} isInvalid={isConfigKeyValueInvalid('getIncidentResponseExternalTitleKey')} - label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.getIncidentResponseExternalTitleKeyTextFieldLabel', - { - defaultMessage: 'Get Incident Response External Title Key', - } - )} - helpText={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.getIncidentResponseExternalTitleKeyTextFieldLabel', - { - defaultMessage: - 'JSON key in get incident response that contains the external incident title', - } - )} + label={i18n.GET_INCIDENT_TITLE_KEY} + helpText={i18n.GET_INCIDENT_TITLE_KEY_HELP} > { - editActionConfig('getIncidentResponseExternalTitleKey', e.target.value); - }} - onBlur={() => { - if (!getIncidentResponseExternalTitleKey) { - editActionConfig('getIncidentResponseExternalTitleKey', ''); - } - }} + onChange={(e) => + onConfigChange('getIncidentResponseExternalTitleKey', e.target.value) + } + onBlur={() => onConfigBlur('getIncidentResponseExternalTitleKey')} /> @@ -493,19 +413,8 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< fullWidth error={errors.getIncidentResponseCreatedDateKey} isInvalid={isConfigKeyValueInvalid('getIncidentResponseCreatedDateKey')} - label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.getIncidentResponseCreatedDateKeyTextFieldLabel', - { - defaultMessage: 'Get Incident Response Created Date Key', - } - )} - helpText={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.getIncidentResponseCreatedDateKeyTextFieldLabel', - { - defaultMessage: - 'JSON key in get incident response that contains the date the incident was created.', - } - )} + label={i18n.GET_INCIDENT_CREATED_KEY} + helpText={i18n.GET_INCIDENT_CREATED_KEY_HELP} > { - editActionConfig('getIncidentResponseCreatedDateKey', e.target.value); - }} - onBlur={() => { - if (!getIncidentResponseCreatedDateKey) { - editActionConfig('getIncidentResponseCreatedDateKey', ''); - } - }} + onChange={(e) => onConfigChange('getIncidentResponseCreatedDateKey', e.target.value)} + onBlur={() => onConfigBlur('getIncidentResponseCreatedDateKey')} /> @@ -531,19 +434,8 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< fullWidth error={errors.getIncidentResponseUpdatedDateKey} isInvalid={isConfigKeyValueInvalid('getIncidentResponseUpdatedDateKey')} - label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.getIncidentResponseUpdatedDateKeyTextFieldLabel', - { - defaultMessage: 'Get Incident Response Updated Date Key', - } - )} - helpText={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.getIncidentResponseUpdatedDateKeyTextFieldLabel', - { - defaultMessage: - 'JSON key in get incident response that contains the date the incident was updated.', - } - )} + label={i18n.GET_INCIDENT_UPDATED_KEY} + helpText={i18n.GET_INCIDENT_UPDATED_KEY_HELP} > { - editActionConfig('getIncidentResponseUpdatedDateKey', e.target.value); - }} - onBlur={() => { - if (!getIncidentResponseUpdatedDateKey) { - editActionConfig('getIncidentResponseUpdatedDateKey', ''); - } - }} + onChange={(e) => onConfigChange('getIncidentResponseUpdatedDateKey', e.target.value)} + onBlur={() => onConfigBlur('getIncidentResponseUpdatedDateKey')} /> @@ -569,19 +455,8 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< fullWidth error={errors.incidentViewUrl} isInvalid={isConfigKeyValueInvalid('incidentViewUrl')} - label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.incidentViewUrlTextFieldLabel', - { - defaultMessage: 'External Incident View URL', - } - )} - helpText={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.incidentViewUrlTextFieldLabel', - { - defaultMessage: - 'URL to view incident in external system. Use $ID or $TITLE and Kibana will dynamically update the url with the external incident id or external incident title.', - } - )} + label={i18n.EXTERNAL_INCIDENT_VIEW_URL} + helpText={i18n.EXTERNAL_INCIDENT_VIEW_URL_HELP} > { - editActionConfig('incidentViewUrl', e.target.value); - }} - onBlur={() => { - if (!incidentViewUrl) { - editActionConfig('incidentViewUrl', ''); - } - }} + onChange={(e) => onConfigChange('incidentViewUrl', e.target.value)} + onBlur={() => onConfigBlur('incidentViewUrl')} /> @@ -606,23 +475,14 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< {/* start UPDATE INCIDENT INPUTS */} - + ({ text: verb.toUpperCase(), value: verb }))} - onChange={(e) => { - editActionConfig('updateIncidentMethod', e.target.value); - }} + onChange={(e) => onConfigChange('updateIncidentMethod', e.target.value)} /> @@ -632,19 +492,8 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< fullWidth error={errors.updateIncidentUrl} isInvalid={isConfigKeyValueInvalid('updateIncidentUrl')} - label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.updateIncidentUrlTextFieldLabel', - { - defaultMessage: 'Update Incident URL', - } - )} - helpText={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.updateIncidentUrlTextFieldLabel', - { - defaultMessage: - 'API URL to update incident. Use $ID and Kibana will dynamically update the url with the external incident id.', - } - )} + label={i18n.UPDATE_INCIDENT_URL} + helpText={i18n.UPDATE_INCIDENT_URL_HELP} > { - editActionConfig('updateIncidentUrl', e.target.value); - }} - onBlur={() => { - if (!updateIncidentUrl) { - editActionConfig('updateIncidentUrl', ''); - } - }} + onChange={(e) => onConfigChange('updateIncidentUrl', e.target.value)} + onBlur={() => onConfigBlur('updateIncidentUrl')} /> @@ -671,44 +514,17 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< id="updateIncidentJson" fullWidth isInvalid={isConfigKeyValueInvalid('updateIncidentJson')} - label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.updateIncidentJsonTextFieldLabel', - { - defaultMessage: 'Update Incident Object', - } - )} - helpText={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.updateIncidentJsonTextFieldLabel', - { - defaultMessage: - 'JSON object to update incident. Sub $SUM where the summary/title should go, $DESC where the description should go, and $TAGS where tags should go (optional).', - } - )} + label={i18n.UPDATE_INCIDENT_JSON} + helpText={i18n.UPDATE_INCIDENT_JSON_HELP} > { - editActionConfig('updateIncidentJson', json); - }} - onBlur={() => { - if (!updateIncidentJson) { - editActionConfig('updateIncidentJson', ''); - } - }} + onDocumentsChange={(json: string) => onConfigChange('updateIncidentJson', json)} + onBlur={() => onConfigBlur('updateIncidentJson')} /> @@ -717,23 +533,14 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< {/* start CREATE COMMENT INCIDENT INPUTS */} - + ({ text: verb.toUpperCase(), value: verb }))} - onChange={(e) => { - editActionConfig('createCommentMethod', e.target.value); - }} + onChange={(e) => onConfigChange('createCommentMethod', e.target.value)} /> @@ -743,19 +550,8 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< fullWidth error={errors.createCommentUrl} isInvalid={isConfigKeyValueInvalid('createCommentUrl')} - label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.createCommentUrlTextFieldLabel', - { - defaultMessage: 'Create Comment URL', - } - )} - helpText={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.createCommentUrlTextFieldLabel', - { - defaultMessage: - 'API URL to add comment to incident. Use $ID and Kibana will dynamically update the url with the external incident id.', - } - )} + label={i18n.CREATE_COMMENT_URL} + helpText={i18n.CREATE_COMMENT_URL_HELP} > { - editActionConfig('createCommentUrl', e.target.value); - }} - onBlur={() => { - if (!createCommentUrl) { - editActionConfig('createCommentUrl', ''); - } - }} + onChange={(e) => onConfigChange('createCommentUrl', e.target.value)} + onBlur={() => onConfigBlur('createCommentUrl')} /> @@ -782,44 +572,17 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< id="createCommentJson" fullWidth isInvalid={isConfigKeyValueInvalid('createCommentJson')} - label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.createCommentJsonTextFieldLabel', - { - defaultMessage: 'Create Comment Object', - } - )} - helpText={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.createCommentJsonTextFieldLabel', - { - defaultMessage: - 'JSON object to update incident. Sub $COMMENT where the comment should go', - } - )} + label={i18n.CREATE_COMMENT_JSON} + helpText={i18n.CREATE_COMMENT_JSON_HELP} > { - editActionConfig('createCommentJson', json); - }} - onBlur={() => { - if (!createCommentJson) { - editActionConfig('createCommentJson', ''); - } - }} + onDocumentsChange={(json: string) => onConfigChange('createCommentJson', json)} + onBlur={() => onConfigBlur('createCommentJson')} /> @@ -829,21 +592,11 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< -

- -

+

{i18n.AUTH_TITLE}

{ @@ -862,13 +615,7 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< !action.id, 2, action.isMissingSecrets ?? false, - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.reenterValuesLabel', - { - defaultMessage: - 'Username and password are encrypted. Please reenter values for these fields.', - } - ) + i18n.REENTER_VALUES )} @@ -877,12 +624,7 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< fullWidth error={errors.user} isInvalid={isSecretsKeyValueInvalid('user')} - label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.userTextFieldLabel', - { - defaultMessage: 'Username', - } - )} + label={i18n.USERNAME} > { - editActionSecrets('user', e.target.value); - }} - onBlur={() => { - if (!user) { - editActionSecrets('user', ''); - } - }} + onChange={(e) => onSecretsChange('user', e.target.value)} + onBlur={() => onSecretsBlur('user')} /> @@ -908,12 +644,7 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< fullWidth error={errors.password} isInvalid={isSecretsKeyValueInvalid('password')} - label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.passwordTextFieldLabel', - { - defaultMessage: 'Password', - } - )} + label={i18n.PASSWORD} > { - editActionSecrets('password', e.target.value); - }} - onBlur={() => { - if (!password) { - editActionSecrets('password', ''); - } - }} + onChange={(e) => onSecretsChange('password', e.target.value)} + onBlur={() => onSecretsBlur('password')} />
@@ -940,12 +665,7 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< @@ -956,12 +676,7 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent< <> -
- -
+
{i18n.HEADERS_TITLE}
{headersList} From 96368bf11e3f79adb45686ed051aab946ad2acdd Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 26 May 2022 08:58:39 -0600 Subject: [PATCH 25/97] fix translations --- .../builtin_action_types/cases_webhook/translations.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts index 25f1477e5aee3d..0926bb65ca700e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts @@ -218,7 +218,7 @@ export const CREATE_INCIDENT_URL = i18n.translate( export const CREATE_INCIDENT_JSON = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.createIncidentJsonTextFieldLabel', { - defaultMessage: 'Create Incident JSON', + defaultMessage: 'Create Incident Object', } ); @@ -350,7 +350,7 @@ export const UPDATE_INCIDENT_URL_HELP = i18n.translate( export const UPDATE_INCIDENT_JSON = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.updateIncidentJsonTextFieldLabel', { - defaultMessage: 'Update Incident JSON', + defaultMessage: 'Update Incident Object', } ); export const UPDATE_INCIDENT_JSON_HELP = i18n.translate( @@ -385,7 +385,7 @@ export const CREATE_COMMENT_URL_HELP = i18n.translate( export const CREATE_COMMENT_JSON = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.createCommentJsonTextFieldLabel', { - defaultMessage: 'Create Comment JSON', + defaultMessage: 'Create Comment Object', } ); export const CREATE_COMMENT_JSON_HELP = i18n.translate( From c41aca008f06f1a517d9c7a08511c8b08b515a24 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 26 May 2022 09:49:44 -0600 Subject: [PATCH 26/97] fix types --- .../cases/public/components/configure_cases/connectors.tsx | 2 +- .../cases/public/components/configure_cases/mapping.test.tsx | 2 ++ .../tests/common/configure/post_configure.ts | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/cases/public/components/configure_cases/connectors.tsx b/x-pack/plugins/cases/public/components/configure_cases/connectors.tsx index 0db2c0b0feee1a..f0772195fc5222 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/connectors.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/connectors.tsx @@ -43,7 +43,7 @@ export interface Props { isLoading: boolean; mappings: CaseConnectorMapping[]; onChangeConnector: (id: string) => void; - selectedConnector: { id: string; type: string }; + selectedConnector: { id: string; type: ConnectorTypes }; updateConnectorDisabled: boolean; } const ConnectorsComponent: React.FC = ({ diff --git a/x-pack/plugins/cases/public/components/configure_cases/mapping.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/mapping.test.tsx index 6299429612be82..22ebc5412dcc5f 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/mapping.test.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/mapping.test.tsx @@ -11,10 +11,12 @@ import { mount } from 'enzyme'; import { TestProviders } from '../../common/mock'; import { Mapping, MappingProps } from './mapping'; import { mappings } from './__mock__'; +import { ConnectorTypes } from '../../../common/api'; describe('Mapping', () => { const props: MappingProps = { actionTypeName: 'ServiceNow ITSM', + connectorType: ConnectorTypes.serviceNowITSM, isLoading: false, mappings, }; diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/configure/post_configure.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/configure/post_configure.ts index 47744830ec8b85..4c7f9f344096e8 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/configure/post_configure.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/configure/post_configure.ts @@ -284,11 +284,11 @@ export default ({ getService }: FtrProviderContext): void => { await createConfiguration( supertest, { + // @ts-expect-error connector: { id: 'test-id', type: ConnectorTypes.none, name: 'Connector', - // @ts-expect-error fields: {}, }, closure_type: 'close-by-user', From 93f68a9e73df2d46422b150125f7973133f45580 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 21 Jun 2022 14:45:28 -0600 Subject: [PATCH 27/97] update to new format --- .../cases_webhook/translations.ts | 10 +- .../cases_webhook/webhook.tsx | 143 +-- .../cases_webhook/webhook_connectors.tsx | 1025 +++++++---------- 3 files changed, 436 insertions(+), 742 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts index 0926bb65ca700e..603813a5505d40 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts @@ -16,7 +16,7 @@ export const CREATE_URL_REQUIRED = i18n.translate( export const CREATE_INCIDENT_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.error.requiredCreateIncidentText', { - defaultMessage: 'Create incident object is required.', + defaultMessage: 'Create incident object is required and must be valid JSON.', } ); @@ -43,7 +43,7 @@ export const UPDATE_URL_REQUIRED = i18n.translate( export const UPDATE_INCIDENT_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.error.requiredUpdateIncidentText', { - defaultMessage: 'Update incident object is required.', + defaultMessage: 'Update incident object is required and must be valid JSON.', } ); @@ -60,10 +60,10 @@ export const CREATE_COMMENT_URL_REQUIRED = i18n.translate( defaultMessage: 'Create comment URL is required.', } ); -export const CREATE_COMMENT_INCIDENT_REQUIRED = i18n.translate( +export const CREATE_COMMENT_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.error.requiredCreateCommentIncidentText', { - defaultMessage: 'Create comment object is required.', + defaultMessage: 'Create comment object is required and must be valid JSON.', } ); @@ -98,7 +98,7 @@ export const GET_RESPONSE_EXTERNAL_UPDATED_KEY_REQUIRED = i18n.translate( defaultMessage: 'Get incident response updated date key is required.', } ); -export const GET_INCIDENT_VIEW_URL = i18n.translate( +export const GET_INCIDENT_VIEW_URL_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.sections.addAction.casesWebhookAction.error.requiredGetIncidentViewUrlKeyText', { defaultMessage: 'View incident URL is required.', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx index 200e69a684402a..096b9d4e386fde 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx @@ -7,18 +7,8 @@ import { lazy } from 'react'; import { i18n } from '@kbn/i18n'; -import { - ActionTypeModel, - GenericValidationResult, - ConnectorValidationResult, -} from '../../../../types'; -import { - CasesWebhookActionParams, - CasesWebhookConfig, - CasesWebhookSecrets, - CasesWebhookActionConnector, -} from './types'; -import { isValidUrl } from '../../../lib/value_validators'; +import { ActionTypeModel, GenericValidationResult } from '../../../../types'; +import { CasesWebhookActionParams, CasesWebhookConfig, CasesWebhookSecrets } from './types'; export function getActionType(): ActionTypeModel< CasesWebhookConfig, @@ -41,135 +31,6 @@ export function getActionType(): ActionTypeModel< defaultMessage: 'Cases Webhook data', } ), - validateConnector: async ( - action: CasesWebhookActionConnector - ): Promise< - ConnectorValidationResult< - Omit, - CasesWebhookSecrets - > - > => { - const translations = await import('./translations'); - const configErrors = { - createCommentJson: new Array(), - createCommentMethod: new Array(), - createCommentUrl: new Array(), - createIncidentJson: new Array(), - createIncidentMethod: new Array(), - createIncidentResponseKey: new Array(), - createIncidentUrl: new Array(), - getIncidentResponseCreatedDateKey: new Array(), - getIncidentResponseExternalTitleKey: new Array(), - getIncidentResponseUpdatedDateKey: new Array(), - incidentViewUrl: new Array(), - getIncidentUrl: new Array(), - updateIncidentJson: new Array(), - updateIncidentMethod: new Array(), - updateIncidentUrl: new Array(), - }; - const secretsErrors = { - user: new Array(), - password: new Array(), - }; - const validationResult = { - config: { errors: configErrors }, - secrets: { errors: secretsErrors }, - }; - if (!action.config.createIncidentUrl) { - configErrors.createIncidentUrl.push(translations.CREATE_URL_REQUIRED); - } - if (action.config.createIncidentUrl && !isValidUrl(action.config.createIncidentUrl)) { - configErrors.createIncidentUrl = [ - ...configErrors.createIncidentUrl, - translations.URL_INVALID('Create incident'), - ]; - } - if (!action.config.createIncidentJson) { - configErrors.createIncidentJson.push(translations.CREATE_INCIDENT_REQUIRED); - } - if (!action.config.createIncidentMethod) { - configErrors.createIncidentMethod.push(translations.CREATE_METHOD_REQUIRED); - } - if (!action.config.createIncidentResponseKey) { - configErrors.createIncidentResponseKey.push(translations.CREATE_RESPONSE_KEY_REQUIRED); - } - if (!action.config.getIncidentUrl) { - configErrors.getIncidentUrl.push(translations.GET_INCIDENT_URL_REQUIRED); - } - if (action.config.getIncidentUrl && !isValidUrl(action.config.getIncidentUrl)) { - configErrors.getIncidentUrl = [ - ...configErrors.getIncidentUrl, - translations.URL_INVALID('Get incident'), - ]; - } - if (!action.config.getIncidentResponseExternalTitleKey) { - configErrors.getIncidentResponseExternalTitleKey.push( - translations.GET_RESPONSE_EXTERNAL_TITLE_KEY_REQUIRED - ); - } - if (!action.config.getIncidentResponseCreatedDateKey) { - configErrors.getIncidentResponseCreatedDateKey.push( - translations.GET_RESPONSE_EXTERNAL_CREATED_KEY_REQUIRED - ); - } - if (!action.config.getIncidentResponseUpdatedDateKey) { - configErrors.getIncidentResponseUpdatedDateKey.push( - translations.GET_RESPONSE_EXTERNAL_UPDATED_KEY_REQUIRED - ); - } - if (!action.config.incidentViewUrl) { - configErrors.incidentViewUrl.push(translations.GET_INCIDENT_VIEW_URL); - } - if (action.config.incidentViewUrl && !isValidUrl(action.config.incidentViewUrl)) { - configErrors.incidentViewUrl = [ - ...configErrors.incidentViewUrl, - translations.URL_INVALID('View incident'), - ]; - } - if (!action.config.updateIncidentUrl) { - configErrors.updateIncidentUrl.push(translations.UPDATE_URL_REQUIRED); - } - if (action.config.updateIncidentUrl && !isValidUrl(action.config.updateIncidentUrl)) { - configErrors.updateIncidentUrl = [ - ...configErrors.updateIncidentUrl, - translations.URL_INVALID('Update incident'), - ]; - } - if (!action.config.updateIncidentJson) { - configErrors.updateIncidentJson.push(translations.UPDATE_INCIDENT_REQUIRED); - } - if (!action.config.updateIncidentMethod) { - configErrors.updateIncidentMethod.push(translations.UPDATE_METHOD_REQUIRED); - } - if (!action.config.createCommentUrl) { - configErrors.createCommentUrl.push(translations.CREATE_COMMENT_URL_REQUIRED); - } - if (action.config.createCommentUrl && !isValidUrl(action.config.createCommentUrl)) { - configErrors.createCommentUrl = [ - ...configErrors.createCommentUrl, - translations.URL_INVALID('Create comment'), - ]; - } - if (!action.config.createCommentJson) { - configErrors.createCommentJson.push(translations.CREATE_COMMENT_INCIDENT_REQUIRED); - } - if (!action.config.createCommentMethod) { - configErrors.createCommentMethod.push(translations.CREATE_COMMENT_METHOD_REQUIRED); - } - if (action.config.hasAuth && !action.secrets.user && !action.secrets.password) { - secretsErrors.user.push(translations.USERNAME_REQUIRED); - } - if (action.config.hasAuth && !action.secrets.user && !action.secrets.password) { - secretsErrors.password.push(translations.PASSWORD_REQUIRED); - } - if (action.secrets.user && !action.secrets.password) { - secretsErrors.password.push(translations.PASSWORD_REQUIRED_FOR_USER); - } - if (!action.secrets.user && action.secrets.password) { - secretsErrors.user.push(translations.USERNAME_REQUIRED_FOR_PASSWORD); - } - return validationResult; - }, validateParams: async ( actionParams: CasesWebhookActionParams ): Promise> => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx index 1ac1ce6547dfd7..08204116a74a68 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx @@ -5,687 +5,520 @@ * 2.0. */ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React from 'react'; import { - EuiFieldPassword, - EuiFieldText, - EuiFormRow, - EuiSelect, + EuiButtonEmpty, + EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiSpacer, - EuiButtonIcon, - EuiDescriptionList, - EuiDescriptionListDescription, - EuiDescriptionListTitle, EuiTitle, - EuiSwitch, - EuiButtonEmpty, } from '@elastic/eui'; +import { + FIELD_TYPES, + UseArray, + UseField, + useFormContext, + useFormData, +} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { Field, TextField } from '@kbn/es-ui-shared-plugin/static/forms/components'; +import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; +import { PasswordField } from '../../password_field'; import { ActionConnectorFieldsProps } from '../../../../types'; -import { CasesWebhookActionConnector, CasesWebhookConfig, CasesWebhookSecrets } from './types'; -import { getEncryptedFieldNotifyLabel } from '../../get_encrypted_field_notify_label'; -import { JsonEditorWithMessageVariables } from '../../json_editor_with_message_variables'; import * as i18n from './translations'; +const { emptyField, urlField, isJsonField } = fieldValidators; const HTTP_VERBS = ['post', 'put', 'patch']; -const CasesWebhookActionConnectorFields: React.FunctionComponent< - ActionConnectorFieldsProps -> = ({ action, editActionConfig, editActionSecrets, errors, readOnly }) => { - const { user, password } = action.secrets; - const { - createCommentJson, - createCommentMethod, - createCommentUrl, - createIncidentJson, - createIncidentMethod, - createIncidentResponseKey, - createIncidentUrl, - getIncidentResponseCreatedDateKey, - getIncidentResponseExternalTitleKey, - getIncidentResponseUpdatedDateKey, - incidentViewUrl, - getIncidentUrl, - hasAuth, - headers, - updateIncidentJson, - updateIncidentMethod, - updateIncidentUrl, - } = action.config; - - const [httpHeaderKey, setHttpHeaderKey] = useState(''); - const [httpHeaderValue, setHttpHeaderValue] = useState(''); - const [hasHeaders, setHasHeaders] = useState(false); - - useEffect(() => { - if (!action.id) { - editActionConfig('hasAuth', true); - } - - if (!createIncidentMethod) { - editActionConfig('createIncidentMethod', 'post'); // set createIncidentMethod to POST by default - } - - if (!updateIncidentMethod) { - editActionConfig('updateIncidentMethod', 'put'); // set updateIncidentMethod to PUT by default - } - - if (!createCommentMethod) { - editActionConfig('createCommentMethod', 'put'); // set createCommentMethod to PUT by default - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const headerErrors = useMemo(() => { - const hErrors = { - keyHeader: new Array(), - valueHeader: new Array(), - }; - if (!httpHeaderKey && httpHeaderValue) { - hErrors.keyHeader.push(i18n.KEY_REQUIRED); - } - if (httpHeaderKey && !httpHeaderValue) { - hErrors.valueHeader.push(i18n.VALUE_REQUIRED); - } - return hErrors; - }, [httpHeaderKey, httpHeaderValue]); - - const hasHeaderErrors: boolean = useMemo( - () => - (headerErrors.keyHeader !== undefined && - headerErrors.valueHeader !== undefined && - headerErrors.keyHeader.length > 0) || - headerErrors.valueHeader.length > 0, - [headerErrors.keyHeader, headerErrors.valueHeader] - ); - - const addHeader = useCallback(() => { - if (headers && !!Object.keys(headers).find((key) => key === httpHeaderKey)) { - return; - } - const updatedHeaders = headers - ? { ...headers, [httpHeaderKey]: httpHeaderValue } - : { [httpHeaderKey]: httpHeaderValue }; - editActionConfig('headers', updatedHeaders); - setHttpHeaderKey(''); - setHttpHeaderValue(''); - }, [editActionConfig, headers, httpHeaderKey, httpHeaderValue]); - - const viewHeaders = useCallback(() => { - setHasHeaders(!hasHeaders); - if (!hasHeaders && !headers) { - editActionConfig('headers', {}); - } - }, [editActionConfig, hasHeaders, headers]); - - const removeHeader = useCallback( - (keyToRemove: string) => { - const updatedHeaders = - headers != null - ? Object.keys(headers) - .filter((key) => key !== keyToRemove) - .reduce((headerToRemove: Record, key: string) => { - headerToRemove[key] = headers[key]; - return headerToRemove; - }, {}) - : {}; - editActionConfig('headers', updatedHeaders); - }, - [editActionConfig, headers] - ); - - const headerControl = useMemo(() => { - if (hasHeaders) { - return ( - <> - -
{i18n.ADD_HEADER}
-
- - - - - { - setHttpHeaderKey(e.target.value); - }} - /> - - - - - { - setHttpHeaderValue(e.target.value); - }} - /> - - - - - - {i18n.ADD_BUTTON} - - - - - - ); - } - return null; - }, [ - addHeader, - hasHeaderErrors, - hasHeaders, - headerErrors.keyHeader, - headerErrors.valueHeader, - httpHeaderKey, - httpHeaderValue, - readOnly, - ]); - - const headersList = useMemo( - () => - Object.keys(headers || {}).map((key: string) => { - return ( - - - removeHeader(key)} - /> - - - - {key} - - {headers && headers[key]} - - - - - ); - }), - [headers, removeHeader] - ); - - const isConfigKeyValueInvalid = useCallback( - (key: keyof CasesWebhookConfig): boolean => - errors[key] !== undefined && errors[key].length > 0 && action.config[key] !== undefined, - [action.config, errors] - ); - const isSecretsKeyValueInvalid = useCallback( - (key: keyof CasesWebhookSecrets): boolean => - errors[key] !== undefined && errors[key].length > 0 && action.secrets[key] !== undefined, - [action.secrets, errors] - ); - - const onConfigChange = useCallback( - (key: keyof CasesWebhookConfig, value: string) => editActionConfig(key, value), - [editActionConfig] - ); +const CasesWebhookActionConnectorFields: React.FunctionComponent = ({ + readOnly, +}) => { + const { getFieldDefaultValue } = useFormContext(); + const [{ config, __internal__ }] = useFormData({ + watch: ['config.hasAuth', '__internal__.hasHeaders'], + }); - const onConfigBlur = useCallback( - (key: keyof CasesWebhookConfig) => { - if (!action.config[key]) { - editActionConfig(key, ''); - } - }, - [action.config, editActionConfig] - ); - const onSecretsChange = useCallback( - (key: keyof CasesWebhookSecrets, value: string) => editActionSecrets(key, value), - [editActionSecrets] - ); + const hasHeadersDefaultValue = !!getFieldDefaultValue('config.headers'); - const onSecretsBlur = useCallback( - (key: keyof CasesWebhookSecrets) => { - if (!action.secrets[key]) { - editActionSecrets(key, ''); - } - }, - [action.secrets, editActionSecrets] - ); + const hasAuth = config == null ? true : config.hasAuth; + const hasHeaders = __internal__ != null ? __internal__.hasHeaders : false; return ( <> {/* start CREATE INCIDENT INPUTS */} - - ({ text: verb.toUpperCase(), value: verb }))} - onChange={(e) => onConfigChange('createIncidentMethod', e.target.value)} - /> - + ({ text: verb.toUpperCase(), value: verb })), + readOnly, + }, + }} + /> - - onConfigChange('createIncidentUrl', e.target.value)} - onBlur={() => onConfigBlur('createIncidentUrl')} - /> - + - - onConfigChange('createIncidentJson', json)} - onBlur={() => onConfigBlur('createIncidentJson')} - /> - + - - onConfigChange('createIncidentResponseKey', e.target.value)} - onBlur={() => onConfigBlur('createIncidentResponseKey')} - /> - + {/* end CREATE INCIDENT INPUTS */} {/* start GET INCIDENT INPUTS */} - - onConfigChange('getIncidentUrl', e.target.value)} - onBlur={() => onConfigBlur('getIncidentUrl')} - /> - + - - - onConfigChange('getIncidentResponseExternalTitleKey', e.target.value) - } - onBlur={() => onConfigBlur('getIncidentResponseExternalTitleKey')} - /> - + - - onConfigChange('getIncidentResponseCreatedDateKey', e.target.value)} - onBlur={() => onConfigBlur('getIncidentResponseCreatedDateKey')} - /> - + - - onConfigChange('getIncidentResponseUpdatedDateKey', e.target.value)} - onBlur={() => onConfigBlur('getIncidentResponseUpdatedDateKey')} - /> - + - - onConfigChange('incidentViewUrl', e.target.value)} - onBlur={() => onConfigBlur('incidentViewUrl')} - /> - + {/* end GET INCIDENT INPUTS */} {/* start UPDATE INCIDENT INPUTS */} - - ({ text: verb.toUpperCase(), value: verb }))} - onChange={(e) => onConfigChange('updateIncidentMethod', e.target.value)} - /> - + ({ text: verb.toUpperCase(), value: verb })), + readOnly, + }, + }} + /> - - onConfigChange('updateIncidentUrl', e.target.value)} - onBlur={() => onConfigBlur('updateIncidentUrl')} - /> - + - - onConfigChange('updateIncidentJson', json)} - onBlur={() => onConfigBlur('updateIncidentJson')} - /> - + {/* end UPDATE INCIDENT INPUTS */} {/* start CREATE COMMENT INCIDENT INPUTS */} - - ({ text: verb.toUpperCase(), value: verb }))} - onChange={(e) => onConfigChange('createCommentMethod', e.target.value)} - /> - + ({ text: verb.toUpperCase(), value: verb })), + readOnly, + }, + }} + /> - - onConfigChange('createCommentUrl', e.target.value)} - onBlur={() => onConfigBlur('createCommentUrl')} - /> - + - - onConfigChange('createCommentJson', json)} - onBlur={() => onConfigBlur('createCommentJson')} - /> - + {/* end CREATE COMMENT INCIDENT INPUTS */} -

{i18n.AUTH_TITLE}

- - { - editActionConfig('hasAuth', e.target.checked); - if (!e.target.checked) { - editActionSecrets('user', null); - editActionSecrets('password', null); - } + +
{hasAuth ? ( - <> - {getEncryptedFieldNotifyLabel( - !action.id, - 2, - action.isMissingSecrets ?? false, - i18n.REENTER_VALUES - )} - - - - onSecretsChange('user', e.target.value)} - onBlur={() => onSecretsBlur('user')} - /> - - - - - onSecretsChange('password', e.target.value)} - onBlur={() => onSecretsBlur('password')} - /> - - - - + + + + + + + + ) : null} - - -
- {Object.keys(headers || {}).length > 0 ? ( - <> - - -
{i18n.HEADERS_TITLE}
-
- - {headersList} - - ) : null} - - {hasHeaders && headerControl} - -
+ {hasHeaders ? ( + + {({ items, addItem, removeItem }) => { + return ( + <> + +
{i18n.HEADERS_TITLE}
+
+ + {items.map((item) => ( + + + + + + + + + removeItem(item.id)} + iconType="minusInCircle" + aria-label={i18n.DELETE_BUTTON} + style={{ marginTop: '28px' }} + /> + + + ))} + + + {i18n.ADD_BUTTON} + + + + ); + }} +
+ ) : null} ); }; From 4966881be2f0d96de53b84a26f4d82c831368180 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 22 Jun 2022 10:10:29 -0600 Subject: [PATCH 28/97] fix webhook tests --- .../cases_webhook/schema.ts | 7 +- .../cases_webhook/webhook.test.tsx | 250 ------------- .../cases_webhook/webhook_connectors.test.tsx | 337 +++++++++++------- .../cases_webhook/webhook_connectors.tsx | 17 +- .../action_connector_form/connector_form.tsx | 2 +- 5 files changed, 223 insertions(+), 390 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts index 6f0b0a56868fd9..9f0639e2069890 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts @@ -9,7 +9,12 @@ import { schema } from '@kbn/config-schema'; import { CasesWebhookMethods } from './types'; import { nullableType } from '../lib/nullable'; -const HeadersSchema = schema.recordOf(schema.string(), schema.string()); +const HeadersSchema = schema.arrayOf( + schema.object({ + key: schema.string(), + value: schema.string(), + }) +); export const ExternalIncidentServiceConfiguration = { createIncidentUrl: schema.string(), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.test.tsx index 238815a8bce93f..a834e361791fd1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.test.tsx @@ -8,7 +8,6 @@ import { TypeRegistry } from '../../../type_registry'; import { registerBuiltInActionTypes } from '..'; import { ActionTypeModel } from '../../../../types'; -import { CasesWebhookActionConnector } from './types'; import { registrationServicesMock } from '../../../../mocks'; const ACTION_TYPE_ID = '.cases-webhook'; @@ -30,255 +29,6 @@ describe('actionTypeRegistry.get() works', () => { }); }); -describe('webhook connector validation', () => { - test('connector validation succeeds when hasAuth is true and connector config is valid', async () => { - const actionConnector = { - secrets: { - user: 'user', - password: 'pass', - }, - id: 'test', - actionTypeId: '.cases-webhook', - name: 'Jira Webhook', - isDeprecated: false, - isPreconfigured: false, - config: { - createCommentJson: '{"body":"$COMMENT"}', - createCommentMethod: 'post', - createCommentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/$ID/comment', - createIncidentJson: - '{"fields":{"summary":"$SUM","description":"$DESC","project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', - createIncidentMethod: 'post', - createIncidentResponseKey: 'id', - createIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue', - getIncidentResponseCreatedDateKey: 'fields.created', - getIncidentResponseExternalTitleKey: 'key', - getIncidentResponseUpdatedDateKey: 'fields.udpated', - hasAuth: true, - headers: { 'content-type': 'text' }, - incidentViewUrl: 'https://siem-kibana.atlassian.net/browse/$TITLE', - getIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/$ID', - updateIncidentJson: - '{"fields":{"summary":"$SUM","description":"$DESC","project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', - updateIncidentMethod: 'put', - updateIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/$ID', - }, - } as CasesWebhookActionConnector; - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { - errors: { - createCommentJson: [], - createCommentMethod: [], - createCommentUrl: [], - createIncidentJson: [], - createIncidentMethod: [], - createIncidentResponseKey: [], - createIncidentUrl: [], - getIncidentResponseCreatedDateKey: [], - getIncidentResponseExternalTitleKey: [], - getIncidentResponseUpdatedDateKey: [], - incidentViewUrl: [], - getIncidentUrl: [], - updateIncidentJson: [], - updateIncidentMethod: [], - updateIncidentUrl: [], - }, - }, - secrets: { - errors: { - user: [], - password: [], - }, - }, - }); - }); - - test('connector validation succeeds when hasAuth is false and connector config is valid', async () => { - const actionConnector = { - secrets: { - user: '', - password: '', - }, - id: 'test', - actionTypeId: '.cases-webhook', - name: 'Jira Webhook', - isDeprecated: false, - isPreconfigured: false, - config: { - createCommentJson: '{"body":"$COMMENT"}', - createCommentMethod: 'post', - createCommentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/$ID/comment', - createIncidentJson: - '{"fields":{"summary":"$SUM","description":"$DESC","project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', - createIncidentMethod: 'post', - createIncidentResponseKey: 'id', - createIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue', - getIncidentResponseCreatedDateKey: 'fields.created', - getIncidentResponseExternalTitleKey: 'key', - getIncidentResponseUpdatedDateKey: 'fields.udpated', - hasAuth: false, - headers: { 'content-type': 'text' }, - incidentViewUrl: 'https://siem-kibana.atlassian.net/browse/$TITLE', - getIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/$ID', - updateIncidentJson: - '{"fields":{"summary":"$SUM","description":"$DESC","project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', - updateIncidentMethod: 'put', - updateIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/$ID', - }, - } as CasesWebhookActionConnector; - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { - errors: { - createCommentJson: [], - createCommentMethod: [], - createCommentUrl: [], - createIncidentJson: [], - createIncidentMethod: [], - createIncidentResponseKey: [], - createIncidentUrl: [], - getIncidentResponseCreatedDateKey: [], - getIncidentResponseExternalTitleKey: [], - getIncidentResponseUpdatedDateKey: [], - incidentViewUrl: [], - getIncidentUrl: [], - updateIncidentJson: [], - updateIncidentMethod: [], - updateIncidentUrl: [], - }, - }, - secrets: { - errors: { - user: [], - password: [], - }, - }, - }); - }); - - test('connector validation fails when connector config is not valid', async () => { - const actionConnector = { - secrets: { - user: 'user', - password: '', - }, - id: 'test', - actionTypeId: '.cases-webhook', - name: 'Jira Webhook', - isDeprecated: false, - isPreconfigured: false, - config: { - createCommentJson: '{"body":"$COMMENT"}', - createCommentMethod: 'post', - createIncidentJson: - '{"fields":{"summary":"$SUM","description":"$DESC","project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', - createIncidentMethod: 'post', - createIncidentResponseKey: 'id', - getIncidentResponseCreatedDateKey: 'fields.created', - getIncidentResponseExternalTitleKey: 'key', - getIncidentResponseUpdatedDateKey: 'fields.udpated', - hasAuth: true, - headers: { 'content-type': 'text' }, - updateIncidentJson: - '{"fields":{"summary":"$SUM","description":"$DESC","project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', - updateIncidentMethod: 'put', - }, - } as unknown as CasesWebhookActionConnector; - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { - errors: { - createCommentJson: [], - createCommentMethod: [], - createCommentUrl: ['Create comment URL is required.'], - createIncidentJson: [], - createIncidentMethod: [], - createIncidentResponseKey: [], - createIncidentUrl: ['Create incident URL is required.'], - getIncidentResponseCreatedDateKey: [], - getIncidentResponseExternalTitleKey: [], - getIncidentResponseUpdatedDateKey: [], - incidentViewUrl: ['View incident URL is required.'], - getIncidentUrl: ['Get incident URL is required.'], - updateIncidentJson: [], - updateIncidentMethod: [], - updateIncidentUrl: ['Update incident URL is required.'], - }, - }, - secrets: { - errors: { - user: [], - password: ['Password is required when username is used.'], - }, - }, - }); - }); - - test('connector validation fails when url in config is not valid', async () => { - const actionConnector = { - secrets: { - user: 'user', - password: 'pass', - }, - id: 'test', - actionTypeId: '.cases-webhook', - name: 'Jira Webhook', - isDeprecated: false, - isPreconfigured: false, - config: { - createCommentJson: '{"body":"$COMMENT"}', - createCommentMethod: 'post', - createCommentUrl: 'invalid.url', - createIncidentJson: - '{"fields":{"summary":"$SUM","description":"$DESC","project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', - createIncidentMethod: 'post', - createIncidentResponseKey: 'id', - createIncidentUrl: 'invalid.url', - getIncidentResponseCreatedDateKey: 'fields.created', - getIncidentResponseExternalTitleKey: 'key', - getIncidentResponseUpdatedDateKey: 'fields.udpated', - hasAuth: true, - headers: { 'content-type': 'text' }, - incidentViewUrl: 'invalid.url', - getIncidentUrl: 'invalid.url', - updateIncidentJson: - '{"fields":{"summary":"$SUM","description":"$DESC","project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', - updateIncidentMethod: 'put', - updateIncidentUrl: 'invalid.url', - }, - } as CasesWebhookActionConnector; - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { - errors: { - createCommentJson: [], - createCommentMethod: [], - createCommentUrl: ['Create comment URL is invalid.'], - createIncidentJson: [], - createIncidentMethod: [], - createIncidentResponseKey: [], - createIncidentUrl: ['Create incident URL is invalid.'], - getIncidentResponseCreatedDateKey: [], - getIncidentResponseExternalTitleKey: [], - getIncidentResponseUpdatedDateKey: [], - incidentViewUrl: ['View incident URL is invalid.'], - getIncidentUrl: ['Get incident URL is invalid.'], - updateIncidentJson: [], - updateIncidentMethod: [], - updateIncidentUrl: ['Update incident URL is invalid.'], - }, - }, - secrets: { - errors: { - user: [], - password: [], - }, - }, - }); - }); -}); - describe('webhook action params validation', () => { test('action params validation succeeds when action params is valid', async () => { const actionParams = { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx index 741b4765d0a8a4..68739efc4519f0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx @@ -8,20 +8,11 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import { CasesWebhookActionConnector } from './types'; -import WebhookActionConnectorFields from './webhook_connectors'; -import { MockCodeEditor } from '../../../code_editor.mock'; +import CasesWebhookActionConnectorFields from './webhook_connectors'; +import { ConnectorFormTestProvider, waitForComponentToUpdate } from '../test_utils'; +import { act, render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; jest.mock('../../../../common/lib/kibana'); -const kibanaReactPath = '../../../../../../../../src/plugins/kibana_react/public'; - -jest.mock(kibanaReactPath, () => { - const original = jest.requireActual(kibanaReactPath); - return { - ...original, - CodeEditor: (props: any) => { - return ; - }, - }; -}); const config = { createCommentJson: '{"body":"$COMMENT"}', @@ -36,7 +27,7 @@ const config = { getIncidentResponseExternalTitleKey: 'key', getIncidentResponseUpdatedDateKey: 'fields.udpated', hasAuth: true, - headers: { 'content-type': 'text' }, + headers: [{ key: 'content-type', value: 'text' }], incidentViewUrl: 'https://siem-kibana.atlassian.net/browse/$TITLE', getIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/$ID', updateIncidentJson: @@ -44,50 +35,31 @@ const config = { updateIncidentMethod: 'put', updateIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/$ID', }; - -const configErrors = { - createCommentJson: [], - createCommentMethod: [], - createCommentUrl: [], - createIncidentJson: [], - createIncidentMethod: [], - createIncidentResponseKey: [], - createIncidentUrl: [], - getIncidentResponseCreatedDateKey: [], - getIncidentResponseExternalTitleKey: [], - getIncidentResponseUpdatedDateKey: [], - incidentViewUrl: [], - getIncidentUrl: [], - updateIncidentJson: [], - updateIncidentMethod: [], - updateIncidentUrl: [], -}; - -describe('WebhookActionConnectorFields renders', () => { - test('all connector fields is rendered', () => { - const actionConnector = { - secrets: { - user: 'user', - password: 'pass', - }, - id: 'test', - actionTypeId: '.cases-webhook', - isPreconfigured: false, - isDeprecated: false, - name: 'cases webhook', - config, - } as CasesWebhookActionConnector; +const actionConnector = { + secrets: { + user: 'user', + password: 'pass', + }, + id: 'test', + actionTypeId: '.cases-webhook', + isDeprecated: false, + isPreconfigured: false, + name: 'cases webhook', + config, +} as CasesWebhookActionConnector; +describe('CasesWebhookActionConnectorFields renders', () => { + test('all connector fields is rendered', async () => { const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> + + {}} + /> + ); + + await waitForComponentToUpdate(); expect(wrapper.find('[data-test-subj="webhookCreateMethodSelect"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="webhookCreateUrlText"]').length > 0).toBeTruthy(); expect( @@ -116,89 +88,188 @@ describe('WebhookActionConnectorFields renders', () => { expect(wrapper.find('[data-test-subj="webhookViewHeadersSwitch"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="webhookHeaderText"]').length > 0).toBeTruthy(); - wrapper.find('[data-test-subj="webhookViewHeadersSwitch"]').last().simulate('click'); expect(wrapper.find('[data-test-subj="webhookAddHeaderButton"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="webhookHeadersKeyInput"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="webhookHeadersValueInput"]').length > 0).toBeTruthy(); + wrapper.find('[data-test-subj="webhookViewHeadersSwitch"]').last().simulate('click'); + expect(wrapper.find('[data-test-subj="webhookAddHeaderButton"]').length > 0).toBeFalsy(); + expect(wrapper.find('[data-test-subj="webhookHeadersKeyInput"]').length > 0).toBeFalsy(); + expect(wrapper.find('[data-test-subj="webhookHeadersValueInput"]').length > 0).toBeFalsy(); }); - test('should display a message on create to remember credentials', () => { - const actionConnector = { - secrets: {}, - actionTypeId: '.cases-webhook', - isPreconfigured: false, - isDeprecated: false, - name: 'cases webhook', - config, - } as unknown as CasesWebhookActionConnector; - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> - ); - expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0); - expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toEqual(0); - }); + describe('Validation', () => { + const onSubmit = jest.fn(); - test('should display a message on edit to re-enter credentials', () => { - const actionConnector = { - secrets: { - user: 'user', - password: 'pass', - }, - id: 'test', - actionTypeId: '.cases-webhook', - isPreconfigured: false, - isDeprecated: true, - name: 'webhook', - config, - } as CasesWebhookActionConnector; - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> - ); - expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0); - expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0); - }); + beforeEach(() => { + jest.clearAllMocks(); + }); - test('should display a message for missing secrets after import', () => { - const actionConnector = { - secrets: { - user: 'user', - password: 'pass', - }, - id: 'test', - actionTypeId: '.webhook', - isPreconfigured: false, - isMissingSecrets: true, - isDeprecated: false, - name: 'webhook', - config, - } as CasesWebhookActionConnector; - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> - ); - expect(wrapper.find('[data-test-subj="missingSecretsMessage"]').length).toBeGreaterThan(0); + const tests: Array<[string, string]> = [ + ['webhookCreateUrlText', 'not-valid'], + ['webhookUserInput', ''], + ['webhookPasswordInput', ''], + ]; + + it('connector validation succeeds when connector config is valid', async () => { + const { getByTestId } = render( + + {}} + /> + + ); + + await act(async () => { + userEvent.click(getByTestId('form-test-provide-submit')); + }); + + const { isPreconfigured, ...rest } = actionConnector; + + expect(onSubmit).toBeCalledWith({ + data: { + ...rest, + __internal__: { + hasHeaders: true, + }, + }, + isValid: true, + }); + }); + + it('connector validation succeeds when auth=false', async () => { + const connector = { + ...actionConnector, + config: { + ...actionConnector.config, + hasAuth: false, + }, + }; + + const { getByTestId } = render( + + {}} + /> + + ); + + await act(async () => { + userEvent.click(getByTestId('form-test-provide-submit')); + }); + + const { isPreconfigured, secrets, ...rest } = actionConnector; + expect(onSubmit).toBeCalledWith({ + data: { + ...rest, + config: { + ...actionConnector.config, + hasAuth: false, + }, + __internal__: { + hasHeaders: true, + }, + }, + isValid: true, + }); + }); + + it('connector validation succeeds without headers', async () => { + const connector = { + ...actionConnector, + config: { + ...actionConnector.config, + headers: null, + }, + }; + + const { getByTestId } = render( + + {}} + /> + + ); + + await act(async () => { + userEvent.click(getByTestId('form-test-provide-submit')); + }); + + const { isPreconfigured, ...rest } = actionConnector; + const { headers, ...rest2 } = actionConnector.config; + expect(onSubmit).toBeCalledWith({ + data: { + ...rest, + config: rest2, + __internal__: { + hasHeaders: false, + }, + }, + isValid: true, + }); + }); + + it('validates correctly if the method is empty', async () => { + const connector = { + ...actionConnector, + config: { + ...actionConnector.config, + createIncidentMethod: '', + }, + }; + + const res = render( + + {}} + /> + + ); + + await act(async () => { + userEvent.click(res.getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false }); + }); + + it.each(tests)('validates correctly %p', async (field, value) => { + const connector = { + ...actionConnector, + config: { + ...actionConnector.config, + headers: [], + }, + }; + + const res = render( + + {}} + /> + + ); + + await act(async () => { + await userEvent.type(res.getByTestId(field), `{selectall}{backspace}${value}`, { + delay: 10, + }); + }); + + await act(async () => { + userEvent.click(res.getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false }); + }); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx index 08204116a74a68..e7b50127e35594 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx @@ -466,12 +466,12 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent { return ( <> - +
{i18n.HEADERS_TITLE}
{items.map((item) => ( - + @@ -494,7 +494,10 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent @@ -510,7 +513,11 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent ))} - + {i18n.ADD_BUTTON} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_form.tsx index f3dabe0b1284cc..3938fab8cf3a79 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_form.tsx @@ -50,7 +50,7 @@ interface Props { // TODO: Remove when https://github.com/elastic/kibana/issues/133107 is resolved const formDeserializer = (data: ConnectorFormSchema): ConnectorFormSchema => { - if (data.actionTypeId !== '.webhook') { + if (data.actionTypeId !== '.webhook' && data.actionTypeId !== '.cases-webhook') { return data; } From f1a8cf37c8152d6128a1a48ac0229e4bbd5d4842 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 22 Jun 2022 12:09:27 -0600 Subject: [PATCH 29/97] fix type --- .../server/builtin_action_types/cases_webhook/schema.ts | 7 +------ .../components/builtin_action_types/cases_webhook/types.ts | 6 ------ .../cases_webhook/webhook_connectors.test.tsx | 3 +-- .../sections/action_connector_form/connector_form.tsx | 2 +- 4 files changed, 3 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts index 9f0639e2069890..6f0b0a56868fd9 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts @@ -9,12 +9,7 @@ import { schema } from '@kbn/config-schema'; import { CasesWebhookMethods } from './types'; import { nullableType } from '../lib/nullable'; -const HeadersSchema = schema.arrayOf( - schema.object({ - key: schema.string(), - value: schema.string(), - }) -); +const HeadersSchema = schema.recordOf(schema.string(), schema.string()); export const ExternalIncidentServiceConfiguration = { createIncidentUrl: schema.string(), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/types.ts index b1867c28263e47..17b826b5a326ef 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/types.ts @@ -12,7 +12,6 @@ import { CasesWebhookSecretConfigurationType, ExecutorSubActionPushParams, } from '@kbn/actions-plugin/server/builtin_action_types/cases_webhook/types'; -import { UserConfiguredActionConnector } from '../../../../types'; export interface CasesWebhookActionParams { subAction: string; @@ -22,8 +21,3 @@ export interface CasesWebhookActionParams { export type CasesWebhookConfig = CasesWebhookPublicConfigurationType; export type CasesWebhookSecrets = CasesWebhookSecretConfigurationType; - -export type CasesWebhookActionConnector = UserConfiguredActionConnector< - CasesWebhookConfig, - CasesWebhookSecrets ->; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx index 68739efc4519f0..8fb18c787a15fd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx @@ -7,7 +7,6 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test-jest-helpers'; -import { CasesWebhookActionConnector } from './types'; import CasesWebhookActionConnectorFields from './webhook_connectors'; import { ConnectorFormTestProvider, waitForComponentToUpdate } from '../test_utils'; import { act, render } from '@testing-library/react'; @@ -46,7 +45,7 @@ const actionConnector = { isPreconfigured: false, name: 'cases webhook', config, -} as CasesWebhookActionConnector; +}; describe('CasesWebhookActionConnectorFields renders', () => { test('all connector fields is rendered', async () => { const wrapper = mountWithIntl( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_form.tsx index 3938fab8cf3a79..b0d8072dae6527 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_form.tsx @@ -71,7 +71,7 @@ const formDeserializer = (data: ConnectorFormSchema): ConnectorFormSchema => { // TODO: Remove when https://github.com/elastic/kibana/issues/133107 is resolved const formSerializer = (formData: ConnectorFormSchema): ConnectorFormSchema => { - if (formData.actionTypeId !== '.webhook') { + if (formData.actionTypeId !== '.webhook' && formData.actionTypeId !== '.cases-webhook') { return formData; } From 6abc85dd68ccb7c9f258d374ac1f4a87bbe2291f Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 29 Jun 2022 12:22:16 -0500 Subject: [PATCH 30/97] use mustache --- .../cases_webhook/service.ts | 34 ++++++++++---- .../cases_webhook/translations.ts | 2 +- .../actions/server/lib/mustache_renderer.ts | 11 +++++ .../cases_webhook/webhook.tsx | 2 +- .../cases_webhook/webhook_connectors.tsx | 42 +++++++++++++---- .../components/json_field_wrapper.scss | 3 ++ .../components/json_field_wrapper.tsx | 47 +++++++++++++++++++ 7 files changed, 121 insertions(+), 20 deletions(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/json_field_wrapper.scss create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/json_field_wrapper.tsx diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts index 7687b6ec7de61c..a83764952340ab 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts @@ -9,14 +9,13 @@ import axios, { AxiosResponse } from 'axios'; import { Logger } from '@kbn/core/server'; import { isString } from 'lodash'; +import { renderMustacheStringInJson } from '../../lib/mustache_renderer'; import { createServiceError, getObjectValueByKey, getPushedDate, makeIncidentUrl, removeSlash, - replaceComment, - replaceSumDesc, throwIfResponseIsNotValidSpecial, } from './utils'; import { @@ -101,17 +100,29 @@ export const createExternalService = ( } }; + const makeCaseStringy = (properties: Record) => ({ + case: Object.entries(properties).map(([key, value]) => ({ [key]: JSON.stringify(value) })), + }); + const createIncident = async ({ incident, }: CreateIncidentParams): Promise => { const { labels, summary, description } = incident; + try { const res: AxiosResponse = await request({ axios: axiosInstance, url: `${createIncidentUrl}`, logger, method: createIncidentMethod, - data: replaceSumDesc(createIncidentJson, summary, description ?? '', labels ?? []), + data: renderMustacheStringInJson( + createIncidentJson, + makeCaseStringy({ + title: summary, + description: description ?? '', + tags: labels ?? [], + }) + ), configurationUtilities, }); @@ -142,16 +153,20 @@ export const createExternalService = ( incident, }: UpdateIncidentParams): Promise => { try { + const { labels, summary, description } = incident; + const res = await request({ axios: axiosInstance, method: updateIncidentMethod, url: makeIncidentUrl(updateIncidentUrl, incidentId), logger, - data: replaceSumDesc( + data: renderMustacheStringInJson( updateIncidentJson, - incident.summary, - incident.description, - incident.labels + makeCaseStringy({ + ...(summary ? { title: summary } : {}), + ...(description ? { title: description } : {}), + ...(labels ? { tags: labels } : {}), + }) ), configurationUtilities, }); @@ -180,7 +195,10 @@ export const createExternalService = ( method: createCommentMethod, url: makeIncidentUrl(createCommentUrl, incidentId), logger, - data: replaceComment(createCommentJson, comment.comment), + data: renderMustacheStringInJson( + createCommentJson, + makeCaseStringy({ comment: comment.comment }) + ), configurationUtilities, }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/translations.ts index 0939a215085075..28b4e79133c43a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/translations.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/translations.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; export const NAME = i18n.translate('xpack.actions.builtin.cases.casesWebhookTitle', { - defaultMessage: 'Cases Webhook', + defaultMessage: 'Webhook - Incident Management', }); export const INVALID_URL = (err: string, url: string) => diff --git a/x-pack/plugins/actions/server/lib/mustache_renderer.ts b/x-pack/plugins/actions/server/lib/mustache_renderer.ts index 1e7f2dd3ab644a..99c8d2531f3d1f 100644 --- a/x-pack/plugins/actions/server/lib/mustache_renderer.ts +++ b/x-pack/plugins/actions/server/lib/mustache_renderer.ts @@ -11,6 +11,17 @@ import { isString, isPlainObject, cloneDeepWith } from 'lodash'; export type Escape = 'markdown' | 'slack' | 'json' | 'none'; type Variables = Record; +// return a rendered mustache template in JSON given the specified variables and escape +// Individual variable values should be stringified already +export function renderMustacheStringInJson(string: string, variables: Variables): string { + try { + return Mustache.render(`${string}`, variables); + } catch (err) { + // log error; the mustache code does not currently leak variables + return `error rendering mustache template "${string}": ${err.message}`; + } +} + // return a rendered mustache template given the specified variables and escape export function renderMustacheString(string: string, variables: Variables, escape: Escape): string { const augmentedVariables = augmentObjectVariables(variables); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx index 096b9d4e386fde..c42a4cd525b2dd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx @@ -18,7 +18,7 @@ export function getActionType(): ActionTypeModel< return { id: '.cases-webhook', // TODO: Steph/cases webhook get an icon - iconClass: 'indexManagementApp', + iconClass: 'logoWebhook', selectMessage: i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.selectMessageText', { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx index e7b50127e35594..8f8a80ca18c7bd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx @@ -24,13 +24,29 @@ import { } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { Field, TextField } from '@kbn/es-ui-shared-plugin/static/forms/components'; import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; +import { ActionVariable } from '@kbn/alerting-plugin/common'; +import { JsonFieldWrapper } from '../../json_field_wrapper'; import { PasswordField } from '../../password_field'; import { ActionConnectorFieldsProps } from '../../../../types'; import * as i18n from './translations'; -const { emptyField, urlField, isJsonField } = fieldValidators; +const { emptyField, urlField } = fieldValidators; const HTTP_VERBS = ['post', 'put', 'patch']; +const casesVars: ActionVariable[] = [ + { name: 'case.title', description: 'test title', useWithTripleBracesInTemplates: true }, + { + name: 'case.description', + description: 'test description', + useWithTripleBracesInTemplates: true, + }, + { name: 'case.tags', description: 'test tags', useWithTripleBracesInTemplates: true }, +]; + +const commentVars: ActionVariable[] = [ + { name: 'case.comment', description: 'test comment', useWithTripleBracesInTemplates: true }, +]; + const CasesWebhookActionConnectorFields: React.FunctionComponent = ({ readOnly, }) => { @@ -101,18 +117,20 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent @@ -306,18 +324,20 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent @@ -379,18 +399,20 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/json_field_wrapper.scss b/x-pack/plugins/triggers_actions_ui/public/application/components/json_field_wrapper.scss new file mode 100644 index 00000000000000..91b6c802ec105d --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/json_field_wrapper.scss @@ -0,0 +1,3 @@ +.euiFormRow__fieldWrapper .kibanaCodeEditor { + height: auto; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/json_field_wrapper.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/json_field_wrapper.tsx new file mode 100644 index 00000000000000..390110e9743e50 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/json_field_wrapper.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + FieldHook, + getFieldValidityAndErrorMessage, +} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import React, { useCallback } from 'react'; +import './json_field_wrapper.scss'; +import { ActionVariable } from '@kbn/alerting-plugin/common'; +import { JsonEditorWithMessageVariables } from './json_editor_with_message_variables'; + +interface Props { + field: FieldHook; + messageVariables?: ActionVariable[]; + paramsProperty: string; + euiCodeEditorProps?: { [key: string]: any }; + [key: string]: any; +} + +export const JsonFieldWrapper = ({ field, ...rest }: Props) => { + const { errorMessage } = getFieldValidityAndErrorMessage(field); + + const { label, helpText, value, setValue } = field; + + const onJsonUpdate = useCallback( + (updatedJson: string) => { + setValue(updatedJson); + }, + [setValue] + ); + + return ( + {helpText}

} + inputTargetValue={value} + label={label ?? 'JSON Editor'} + onDocumentsChange={onJsonUpdate} + {...rest} + /> + ); +}; From c429071b7899df32fabe02246a60761d5c4505ec Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 29 Jun 2022 12:40:38 -0500 Subject: [PATCH 31/97] fix! --- .../server/builtin_action_types/cases_webhook/service.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts index a83764952340ab..b035859b645f8e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts @@ -101,7 +101,10 @@ export const createExternalService = ( }; const makeCaseStringy = (properties: Record) => ({ - case: Object.entries(properties).map(([key, value]) => ({ [key]: JSON.stringify(value) })), + case: Object.entries(properties).reduce( + (acc, [key, value]) => ({ ...acc, [key]: JSON.stringify(value) }), + {} + ), }); const createIncident = async ({ @@ -164,7 +167,7 @@ export const createExternalService = ( updateIncidentJson, makeCaseStringy({ ...(summary ? { title: summary } : {}), - ...(description ? { title: description } : {}), + ...(description ? { description } : {}), ...(labels ? { tags: labels } : {}), }) ), From 2e982c58d9164fc7510c22c3eb2cb595bf193b52 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 30 Jun 2022 09:26:49 -0500 Subject: [PATCH 32/97] add mustache to urls --- .../builtin_action_types/cases_webhook/api.ts | 4 +- .../cases_webhook/service.ts | 66 ++++++++++++++----- .../cases_webhook/types.ts | 4 +- .../cases_webhook/utils.ts | 41 ------------ .../actions/server/lib/mustache_renderer.ts | 4 +- .../cases_webhook/webhook_connectors.tsx | 28 ++++++-- .../mustache_text_field_wrapper.tsx | 63 ++++++++++++++++++ 7 files changed, 141 insertions(+), 69 deletions(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/mustache_text_field_wrapper.tsx diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/api.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/api.ts index 059f2c4fd13ae6..bae26a5a213139 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/api.ts @@ -24,7 +24,7 @@ const pushToServiceHandler = async ({ if (externalId != null) { res = await externalService.updateIncident({ - incidentId: externalId, + externalId, incident, }); } else { @@ -40,7 +40,7 @@ const pushToServiceHandler = async ({ continue; } await externalService.createComment({ - incidentId: res.id, + externalId: res.id, comment: currentComment, }); res.comments = [ diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts index b035859b645f8e..434d939c233625 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts @@ -9,12 +9,11 @@ import axios, { AxiosResponse } from 'axios'; import { Logger } from '@kbn/core/server'; import { isString } from 'lodash'; -import { renderMustacheStringInJson } from '../../lib/mustache_renderer'; +import { renderMustacheStringNoEscape } from '../../lib/mustache_renderer'; import { createServiceError, getObjectValueByKey, getPushedDate, - makeIncidentUrl, removeSlash, throwIfResponseIsNotValidSpecial, } from './utils'; @@ -77,7 +76,13 @@ export const createExternalService = ( try { const res = await request({ axios: axiosInstance, - url: makeIncidentUrl(getIncidentUrl, id), + url: renderMustacheStringNoEscape(getIncidentUrl, { + external: { + system: { + id, + }, + }, + }), logger, configurationUtilities, }); @@ -118,7 +123,7 @@ export const createExternalService = ( url: `${createIncidentUrl}`, logger, method: createIncidentMethod, - data: renderMustacheStringInJson( + data: renderMustacheStringNoEscape( createIncidentJson, makeCaseStringy({ title: summary, @@ -135,15 +140,22 @@ export const createExternalService = ( res, requiredAttributesToBeInTheResponse: [createIncidentResponseKey], }); - const incidentId = getObjectValueByKey(data, createIncidentResponseKey); - const insertedIncident = await getIncident(incidentId); + const externalId = getObjectValueByKey(data, createIncidentResponseKey); + const insertedIncident = await getIncident(externalId); logger.debug(`response from webhook action "${actionId}": [HTTP ${status}] ${statusText}`); return { - id: incidentId, + id: externalId, title: insertedIncident.title, - url: makeIncidentUrl(incidentViewUrl, incidentId, insertedIncident.title), + url: renderMustacheStringNoEscape(incidentViewUrl, { + external: { + system: { + id: externalId, + title: insertedIncident.title, + }, + }, + }), pushedDate: getPushedDate(insertedIncident.created), }; } catch (error) { @@ -152,18 +164,23 @@ export const createExternalService = ( }; const updateIncident = async ({ - incidentId, + externalId, incident, }: UpdateIncidentParams): Promise => { try { const { labels, summary, description } = incident; - const res = await request({ axios: axiosInstance, method: updateIncidentMethod, - url: makeIncidentUrl(updateIncidentUrl, incidentId), + url: renderMustacheStringNoEscape(updateIncidentUrl, { + external: { + system: { + id: externalId, + }, + }, + }), logger, - data: renderMustacheStringInJson( + data: renderMustacheStringNoEscape( updateIncidentJson, makeCaseStringy({ ...(summary ? { title: summary } : {}), @@ -178,12 +195,19 @@ export const createExternalService = ( res, }); - const updatedIncident = await getIncident(incidentId as string); + const updatedIncident = await getIncident(externalId as string); return { - id: incidentId, + id: externalId, title: updatedIncident.title, - url: makeIncidentUrl(incidentViewUrl, incidentId, updatedIncident.title), + url: renderMustacheStringNoEscape(incidentViewUrl, { + external: { + system: { + id: externalId, + title: updatedIncident.title, + }, + }, + }), pushedDate: getPushedDate(updatedIncident.updated), }; } catch (error) { @@ -191,14 +215,20 @@ export const createExternalService = ( } }; - const createComment = async ({ incidentId, comment }: CreateCommentParams): Promise => { + const createComment = async ({ externalId, comment }: CreateCommentParams): Promise => { try { const res = await request({ axios: axiosInstance, method: createCommentMethod, - url: makeIncidentUrl(createCommentUrl, incidentId), + url: renderMustacheStringNoEscape(createCommentUrl, { + external: { + system: { + id: externalId, + }, + }, + }), logger, - data: renderMustacheStringInJson( + data: renderMustacheStringNoEscape( createCommentJson, makeCaseStringy({ comment: comment.comment }) ), diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts index b97396d7eda6ff..c8d27e8b9f12c7 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts @@ -69,7 +69,7 @@ export interface CreateIncidentParams { incident: Incident; } export interface UpdateIncidentParams { - incidentId: string; + externalId: string; incident: Partial; } export interface SimpleComment { @@ -78,7 +78,7 @@ export interface SimpleComment { } export interface CreateCommentParams { - incidentId: string; + externalId: string; comment: SimpleComment; } diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts index 68d04e4a27670c..9d94b0e8f8fea1 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts @@ -107,44 +107,3 @@ export const throwIfResponseIsNotValidSpecial = ({ }; export const removeSlash = (url: string) => (url.endsWith('/') ? url.slice(0, -1) : url); - -export const replaceSumDesc = ( - stringifiedJson: string, - sum?: string, - desc?: string | null, - labels?: string[] | null -) => { - let str = stringifiedJson; - - const prs = JSON.parse(str); - str = JSON.stringify(prs, function replacer(key, value) { - if (value === '$SUM') { - return sum; - } - if (value === '$DESC') { - return desc; - } - if (value === '$TAGS') { - return labels != null ? labels : []; - } - return value; - }); - - return JSON.parse(str); -}; - -export const makeIncidentUrl = (url: string, id: string, title?: string) => { - let str = url; - str = str.replace('$ID', id); - if (title != null) { - str = str.replace('$TITLE', title); - } - return str; -}; - -export const replaceComment = (stringifiedJson: string, comment: string) => { - let str = stringifiedJson; - str = str.replace('$COMMENT', comment); - - return JSON.parse(str); -}; diff --git a/x-pack/plugins/actions/server/lib/mustache_renderer.ts b/x-pack/plugins/actions/server/lib/mustache_renderer.ts index 99c8d2531f3d1f..dd734dd25b6b58 100644 --- a/x-pack/plugins/actions/server/lib/mustache_renderer.ts +++ b/x-pack/plugins/actions/server/lib/mustache_renderer.ts @@ -11,9 +11,9 @@ import { isString, isPlainObject, cloneDeepWith } from 'lodash'; export type Escape = 'markdown' | 'slack' | 'json' | 'none'; type Variables = Record; -// return a rendered mustache template in JSON given the specified variables and escape +// return a rendered mustache template with no escape given the specified variables and escape // Individual variable values should be stringified already -export function renderMustacheStringInJson(string: string, variables: Variables): string { +export function renderMustacheStringNoEscape(string: string, variables: Variables): string { try { return Mustache.render(`${string}`, variables); } catch (err) { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx index 8f8a80ca18c7bd..dabfabcd1111d4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx @@ -25,6 +25,7 @@ import { import { Field, TextField } from '@kbn/es-ui-shared-plugin/static/forms/components'; import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; import { ActionVariable } from '@kbn/alerting-plugin/common'; +import { MustacheTextFieldWrapper } from '../../mustache_text_field_wrapper'; import { JsonFieldWrapper } from '../../json_field_wrapper'; import { PasswordField } from '../../password_field'; import { ActionConnectorFieldsProps } from '../../../../types'; @@ -47,6 +48,10 @@ const commentVars: ActionVariable[] = [ { name: 'case.comment', description: 'test comment', useWithTripleBracesInTemplates: true }, ]; +const urlVars: ActionVariable[] = [ + { name: 'external.system.id', description: 'test id', useWithTripleBracesInTemplates: true }, +]; + const CasesWebhookActionConnectorFields: React.FunctionComponent = ({ readOnly, }) => { @@ -173,11 +178,13 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent @@ -257,11 +264,20 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent @@ -305,11 +321,13 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent @@ -380,11 +398,13 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/mustache_text_field_wrapper.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/mustache_text_field_wrapper.tsx new file mode 100644 index 00000000000000..d36e1374ab603d --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/mustache_text_field_wrapper.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + FieldHook, + getFieldValidityAndErrorMessage, +} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import React, { useCallback } from 'react'; +import './json_field_wrapper.scss'; +import { ActionVariable } from '@kbn/alerting-plugin/common'; +import { EuiFormRow } from '@elastic/eui'; +import { TextFieldWithMessageVariables } from './text_field_with_message_variables'; + +interface Props { + field: FieldHook; + messageVariables?: ActionVariable[]; + paramsProperty: string; + euiCodeEditorProps?: { [key: string]: any }; + [key: string]: any; +} + +export const MustacheTextFieldWrapper = ({ field, euiFieldProps = {}, idAria, ...rest }: Props) => { + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + + const { value, onChange } = field; + + const editAction = useCallback( + (property: string, newValue: string) => { + onChange({ + // @ts-ignore we don't have to send the whole type + target: { + value: newValue, + }, + }); + }, + [onChange] + ); + + return ( + + + + ); +}; From 6fb6da2b71d5c7b0b8f243d74b9711dba325ccf4 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 30 Jun 2022 13:55:47 -0500 Subject: [PATCH 33/97] better form validation for mustache variables --- .../cases_webhook/schema.ts | 24 +-- .../cases_webhook/translations.ts | 2 +- .../cases_webhook/validators.ts | 21 ++- .../components/add_message_variables.tsx | 16 +- .../cases_webhook/action_variables.ts | 35 ++++ .../cases_webhook/translations.ts | 35 ++-- .../cases_webhook/validator.ts | 158 ++++++++++++++++++ .../cases_webhook/webhook_connectors.tsx | 59 ++++--- .../json_editor_with_message_variables.tsx | 3 + .../mustache_text_field_wrapper.tsx | 4 +- .../text_field_with_message_variables.tsx | 3 + 11 files changed, 291 insertions(+), 69 deletions(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/action_variables.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/validator.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts index 6f0b0a56868fd9..ad4f05651b9d6b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts @@ -38,18 +38,20 @@ export const ExternalIncidentServiceConfiguration = { } ), updateIncidentJson: schema.string(), - createCommentUrl: schema.string(), - createCommentMethod: schema.oneOf( - [ - schema.literal(CasesWebhookMethods.POST), - schema.literal(CasesWebhookMethods.PUT), - schema.literal(CasesWebhookMethods.PATCH), - ], - { - defaultValue: CasesWebhookMethods.PUT, - } + createCommentUrl: schema.maybe(schema.string()), + createCommentMethod: schema.maybe( + schema.oneOf( + [ + schema.literal(CasesWebhookMethods.POST), + schema.literal(CasesWebhookMethods.PUT), + schema.literal(CasesWebhookMethods.PATCH), + ], + { + defaultValue: CasesWebhookMethods.PUT, + } + ) ), - createCommentJson: schema.string(), + createCommentJson: schema.maybe(schema.string()), headers: nullableType(HeadersSchema), hasAuth: schema.boolean({ defaultValue: true }), }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/translations.ts index 28b4e79133c43a..4e2a76575432a0 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/translations.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/translations.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; export const NAME = i18n.translate('xpack.actions.builtin.cases.casesWebhookTitle', { - defaultMessage: 'Webhook - Incident Management', + defaultMessage: 'Webhook - Case Management', }); export const INVALID_URL = (err: string, url: string) => diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts index 45d0865b6f597e..449925238a95f3 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts @@ -36,17 +36,20 @@ const validateConfig = ( } catch (allowListError) { return i18n.CONFIG_ERR(allowListError.message); } - try { - new URL(createCommentUrl); - } catch (err) { - return i18n.INVALID_URL(err, 'createCommentUrl'); - } + if (createCommentUrl) { + try { + new URL(createCommentUrl); + } catch (err) { + return i18n.INVALID_URL(err, 'createCommentUrl'); + } - try { - configurationUtilities.ensureUriAllowed(createCommentUrl); - } catch (allowListError) { - return i18n.CONFIG_ERR(allowListError.message); + try { + configurationUtilities.ensureUriAllowed(createCommentUrl); + } catch (allowListError) { + return i18n.CONFIG_ERR(allowListError.message); + } } + try { new URL(incidentViewUrl); } catch (err) { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx index 08a824f53e4654..8d4e49c4e9ee8f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx @@ -19,12 +19,14 @@ import { ActionVariable } from '@kbn/alerting-plugin/common'; import { templateActionVariable } from '../lib'; interface Props { + buttonTitle?: string; messageVariables?: ActionVariable[]; paramsProperty: string; onSelectEventHandler: (variable: ActionVariable) => void; } export const AddMessageVariables: React.FunctionComponent = ({ + buttonTitle, messageVariables, paramsProperty, onSelectEventHandler, @@ -54,12 +56,14 @@ export const AddMessageVariables: React.FunctionComponent = ({ )); - const addVariableButtonTitle = i18n.translate( - 'xpack.triggersActionsUI.components.addMessageVariables.addRuleVariableTitle', - { - defaultMessage: 'Add rule variable', - } - ); + const addVariableButtonTitle = buttonTitle + ? buttonTitle + : i18n.translate( + 'xpack.triggersActionsUI.components.addMessageVariables.addRuleVariableTitle', + { + defaultMessage: 'Add rule variable', + } + ); if ((messageVariables?.length ?? 0) === 0) { return <>; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/action_variables.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/action_variables.ts new file mode 100644 index 00000000000000..ae81d63d26a6bb --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/action_variables.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ActionVariable } from '@kbn/alerting-plugin/common'; + +export const casesVars: ActionVariable[] = [ + { name: 'case.title', description: 'test title', useWithTripleBracesInTemplates: true }, + { + name: 'case.description', + description: 'test description', + useWithTripleBracesInTemplates: true, + }, + { name: 'case.tags', description: 'test tags', useWithTripleBracesInTemplates: true }, +]; + +export const commentVars: ActionVariable[] = [ + { name: 'case.comment', description: 'test comment', useWithTripleBracesInTemplates: true }, +]; + +export const urlVars: ActionVariable[] = [ + { name: 'external.system.id', description: 'test id', useWithTripleBracesInTemplates: true }, +]; + +export const urlVarsExt: ActionVariable[] = [ + ...urlVars, + { + name: 'external.system.title', + description: 'test title', + useWithTripleBracesInTemplates: true, + }, +]; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts index 603813a5505d40..040acaad9b7237 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts @@ -57,13 +57,13 @@ export const UPDATE_METHOD_REQUIRED = i18n.translate( export const CREATE_COMMENT_URL_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.error.requiredCreateCommentUrlText', { - defaultMessage: 'Create comment URL is required.', + defaultMessage: 'Create comment URL must be URL format.', } ); -export const CREATE_COMMENT_REQUIRED = i18n.translate( +export const CREATE_COMMENT_MESSAGE = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.error.requiredCreateCommentIncidentText', { - defaultMessage: 'Create comment object is required and must be valid JSON.', + defaultMessage: 'Create comment object must be valid JSON.', } ); @@ -226,7 +226,7 @@ export const CREATE_INCIDENT_JSON_HELP = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.createIncidentJsonHelpText', { defaultMessage: - 'JSON object to create incident. Sub $SUM where the summary/title should go, $DESC where the description should go, and $TAGS where tags should go (optional).', + 'JSON object to create incident. Use the Add Variable button to add Cases data to the payload.', } ); @@ -257,6 +257,20 @@ export const CREATE_INCIDENT_RESPONSE_KEY_HELP = i18n.translate( } ); +export const ADD_CASES_VARIABLE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.addVariable', + { + defaultMessage: 'Add cases variable', + } +); + +export const ADD_EXTERNAL_VARIABLE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.addExtVariable', + { + defaultMessage: 'Add external system variable', + } +); + export const GET_INCIDENT_URL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.getIncidentUrlTextFieldLabel', { @@ -267,7 +281,7 @@ export const GET_INCIDENT_URL_HELP = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.getIncidentUrlHelp', { defaultMessage: - 'API URL to GET incident details JSON from external system. Use $ID and Kibana will dynamically update the url with the external incident id.', + 'API URL to GET incident details JSON from external system. Use the Add Variable button to add External System Id to the url.', } ); @@ -322,7 +336,7 @@ export const EXTERNAL_INCIDENT_VIEW_URL_HELP = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.incidentViewUrlHelp', { defaultMessage: - 'URL to view incident in external system. Use $ID or $TITLE and Kibana will dynamically update the url with the external incident id or external incident title.', + 'URL to view incident in external system. Use the Add Variable button to add External System Id or External System Title to the url.', } ); @@ -343,7 +357,7 @@ export const UPDATE_INCIDENT_URL_HELP = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.updateIncidentUrlHelp', { defaultMessage: - 'API URL to update incident. Use $ID and Kibana will dynamically update the url with the external incident id.', + 'API URL to update incident. Use the Add Variable button to add External System Id to the url.', } ); @@ -357,7 +371,7 @@ export const UPDATE_INCIDENT_JSON_HELP = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.updateIncidentJsonHelpl', { defaultMessage: - 'JSON object to update incident. Sub $SUM where the summary/title should go, $DESC where the description should go, and $TAGS where tags should go (optional).', + 'JSON object to update incident. Use the Add Variable button to add Cases data to the payload.', } ); @@ -378,7 +392,7 @@ export const CREATE_COMMENT_URL_HELP = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.createCommentUrlHelp', { defaultMessage: - 'API URL to add comment to incident. Use $ID and Kibana will dynamically update the url with the external incident id.', + 'API URL to add comment to incident. Use the Add Variable button to add External System Id to the url.', } ); @@ -391,7 +405,8 @@ export const CREATE_COMMENT_JSON = i18n.translate( export const CREATE_COMMENT_JSON_HELP = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.createCommentJsonHelp', { - defaultMessage: 'JSON object to update incident. Sub $COMMENT where the comment should go', + defaultMessage: + 'JSON object to create a comment. Use the Add Variable button to add Cases data to the payload.', } ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/validator.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/validator.ts new file mode 100644 index 00000000000000..7f7d8a75b2b8fb --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/validator.ts @@ -0,0 +1,158 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ERROR_CODE } from '@kbn/es-ui-shared-plugin/static/forms/helpers/field_validators/types'; +import { + ValidationError, + ValidationFunc, +} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { containsChars, isUrl } from '@kbn/es-ui-shared-plugin/static/validators/string'; +import { casesVars, commentVars, urlVars, urlVarsExt } from './action_variables'; +import { templateActionVariable } from '../../../lib'; + +const errorCode: ERROR_CODE = 'ERR_FIELD_MISSING'; + +const missingVariable = (path: string, variable: string, plural?: boolean) => ({ + code: errorCode, + path, + message: `Missing required ${variable} variable${plural ? 's' : ''}`, +}); + +export const containsTitleAndDesc = + () => + (...args: Parameters): ReturnType> => { + const [{ value, path }] = args; + const title = templateActionVariable( + casesVars.find((actionVariable) => actionVariable.name === 'case.title')! + ); + const description = templateActionVariable( + casesVars.find((actionVariable) => actionVariable.name === 'case.description')! + ); + let error; + if (typeof value === 'string') { + // will always be true, but this keeps my doesContain var contained! + if (!error) { + const { doesContain } = containsChars(title)(value); + if (!doesContain) { + error = missingVariable(path, title); + } + } + if (!error) { + const { doesContain } = containsChars(description)(value); + error = !doesContain ? missingVariable(path, description) : undefined; + } else { + const { doesContain } = containsChars(description)(value); + error = !doesContain + ? missingVariable(path, `${title} and ${description}`, true) + : missingVariable(path, title); + } + } + return error; + }; + +export const containsExternalId = + () => + (...args: Parameters): ReturnType> => { + const [{ value, path }] = args; + + const id = templateActionVariable( + urlVars.find((actionVariable) => actionVariable.name === 'external.system.id')! + ); + let error; + if (typeof value === 'string') { + const { doesContain } = containsChars(id)(value); + if (!doesContain) { + error = missingVariable(path, id); + } + } + return error; + }; + +export const containsExternalIdOrTitle = + () => + (...args: Parameters): ReturnType> => { + const [{ value, path }] = args; + + const id = templateActionVariable( + urlVars.find((actionVariable) => actionVariable.name === 'external.system.id')! + ); + const title = templateActionVariable( + urlVarsExt.find((actionVariable) => actionVariable.name === 'external.system.title')! + ); + const error = missingVariable(path, `${id} or ${title}`); + if (typeof value === 'string') { + const { doesContain: doesContainId } = containsChars(id)(value); + const { doesContain: doesContainTitle } = containsChars(title)(value); + if (doesContainId || doesContainTitle) { + return undefined; + } + } + return error; + }; + +export const containsIdOrEmpty = + (message: string) => + (...args: Parameters): ReturnType> => { + const [{ value }] = args; + if (typeof value !== 'string') { + return { + code: 'ERR_FIELD_FORMAT', + formatType: 'STRING', + message, + }; + } + if (value.length === 0) { + return undefined; + } + return containsExternalId()(...args); + }; + +export const containsCommentsOrEmpty = + (message: string) => + (...args: Parameters): ReturnType> => { + const [{ value, path }] = args; + if (typeof value !== 'string') { + return { + code: 'ERR_FIELD_FORMAT', + formatType: 'STRING', + message, + }; + } + if (value.length === 0) { + return undefined; + } + + const comment = templateActionVariable( + commentVars.find((actionVariable) => actionVariable.name === 'case.comment')! + ); + let error; + if (typeof value === 'string') { + const { doesContain } = containsChars(comment)(value); + if (!doesContain) { + error = missingVariable(path, comment); + } + } + return error; + }; + +export const isUrlButCanBeEmpty = + (message: string) => + (...args: Parameters) => { + const [{ value }] = args; + const error: ValidationError = { + code: 'ERR_FIELD_FORMAT', + formatType: 'URL', + message, + }; + if (typeof value !== 'string') { + return error; + } + if (value.length === 0) { + return undefined; + } + return isUrl(value) ? undefined : error; + }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx index dabfabcd1111d4..eef76fefe81a24 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx @@ -24,34 +24,24 @@ import { } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { Field, TextField } from '@kbn/es-ui-shared-plugin/static/forms/components'; import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; -import { ActionVariable } from '@kbn/alerting-plugin/common'; +import { + containsCommentsOrEmpty, + containsExternalId, + containsExternalIdOrTitle, + containsIdOrEmpty, + containsTitleAndDesc, + isUrlButCanBeEmpty, +} from './validator'; import { MustacheTextFieldWrapper } from '../../mustache_text_field_wrapper'; import { JsonFieldWrapper } from '../../json_field_wrapper'; import { PasswordField } from '../../password_field'; import { ActionConnectorFieldsProps } from '../../../../types'; import * as i18n from './translations'; +import { casesVars, commentVars, urlVars, urlVarsExt } from './action_variables'; const { emptyField, urlField } = fieldValidators; const HTTP_VERBS = ['post', 'put', 'patch']; -const casesVars: ActionVariable[] = [ - { name: 'case.title', description: 'test title', useWithTripleBracesInTemplates: true }, - { - name: 'case.description', - description: 'test description', - useWithTripleBracesInTemplates: true, - }, - { name: 'case.tags', description: 'test tags', useWithTripleBracesInTemplates: true }, -]; - -const commentVars: ActionVariable[] = [ - { name: 'case.comment', description: 'test comment', useWithTripleBracesInTemplates: true }, -]; - -const urlVars: ActionVariable[] = [ - { name: 'external.system.id', description: 'test id', useWithTripleBracesInTemplates: true }, -]; - const CasesWebhookActionConnectorFields: React.FunctionComponent = ({ readOnly, }) => { @@ -124,8 +114,10 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent @@ -175,6 +168,7 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent @@ -261,6 +256,7 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent @@ -318,6 +308,7 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent @@ -344,6 +336,9 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent @@ -393,8 +389,9 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent @@ -419,7 +417,7 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.tsx index ba3470d4a1dfd2..6cb966195fcb24 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.tsx @@ -34,6 +34,7 @@ const NO_EDITOR_ERROR_MESSAGE = i18n.translate( ); interface Props { + buttonTitle?: string; messageVariables?: ActionVariable[]; paramsProperty: string; inputTargetValue?: string; @@ -53,6 +54,7 @@ const { useXJsonMode } = XJson; const EDITOR_SOURCE = 'json-editor-with-message-variables'; export const JsonEditorWithMessageVariables: React.FunctionComponent = ({ + buttonTitle, messageVariables, paramsProperty, inputTargetValue, @@ -148,6 +150,7 @@ export const JsonEditorWithMessageVariables: React.FunctionComponent = ({ label={label} labelAppend={ ; messageVariables?: ActionVariable[]; paramsProperty: string; - euiCodeEditorProps?: { [key: string]: any }; + euiFieldProps: { [key: string]: any; paramsProperty: string }; [key: string]: any; } -export const MustacheTextFieldWrapper = ({ field, euiFieldProps = {}, idAria, ...rest }: Props) => { +export const MustacheTextFieldWrapper = ({ field, euiFieldProps, idAria, ...rest }: Props) => { const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); const { value, onChange } = field; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx index e8d43107bc11cc..73199e212739cf 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx @@ -13,6 +13,7 @@ import { AddMessageVariables } from './add_message_variables'; import { templateActionVariable } from '../lib'; interface Props { + buttonTitle?: string; messageVariables?: ActionVariable[]; paramsProperty: string; index: number; @@ -23,6 +24,7 @@ interface Props { } export const TextFieldWithMessageVariables: React.FunctionComponent = ({ + buttonTitle, messageVariables, paramsProperty, index, @@ -68,6 +70,7 @@ export const TextFieldWithMessageVariables: React.FunctionComponent = ({ }} append={ Date: Wed, 6 Jul 2022 11:15:33 -0600 Subject: [PATCH 34/97] create comment optional --- .../cases_webhook/service.ts | 3 +++ .../cases_webhook/types.ts | 4 ++-- .../cases_webhook/utils.ts | 4 +++- .../public/components/connectors/card.tsx | 24 +++++++++++++++++-- .../connectors/cases_webhook/case_fields.tsx | 3 +++ .../components/connectors/translations.ts | 22 +++++++++++++++++ 6 files changed, 55 insertions(+), 5 deletions(-) create mode 100644 x-pack/plugins/cases/public/components/connectors/translations.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts index 434d939c233625..83168658e5150b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts @@ -217,6 +217,9 @@ export const createExternalService = ( const createComment = async ({ externalId, comment }: CreateCommentParams): Promise => { try { + if (!createCommentUrl || !createCommentJson) { + return {}; + } const res = await request({ axios: axiosInstance, method: createCommentMethod, diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts index c8d27e8b9f12c7..b205fbeca6e434 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts @@ -34,8 +34,8 @@ export type CasesWebhookSecretConfigurationType = TypeOf< export type CasesWebhookActionParamsType = TypeOf; export interface ExternalServiceCredentials { - config: Record; - secrets: Record; + config: CasesWebhookPublicConfigurationType; + secrets: CasesWebhookSecretConfigurationType; } export interface ExternalServiceValidation { diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts index 9d94b0e8f8fea1..aa5f6dcf23e163 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts @@ -14,7 +14,9 @@ export const createServiceError = (error: AxiosError, message: string) => new Error( getErrorMessage( i18n.NAME, - `${message}. Error: ${error.message} Reason: ${error.response?.statusText}` + `${message}. Error: ${error.message} ${ + error.response?.statusText != null ? `Reason: ${error.response?.statusText}` : '' + }` ) ); diff --git a/x-pack/plugins/cases/public/components/connectors/card.tsx b/x-pack/plugins/cases/public/components/connectors/card.tsx index d6ef92572e5069..749c7211e3303c 100644 --- a/x-pack/plugins/cases/public/components/connectors/card.tsx +++ b/x-pack/plugins/cases/public/components/connectors/card.tsx @@ -6,18 +6,27 @@ */ import React, { memo, useMemo } from 'react'; -import { EuiCard, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLoadingSpinner } from '@elastic/eui'; +import { + EuiCallOut, + EuiCard, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiLoadingSpinner, +} from '@elastic/eui'; import styled from 'styled-components'; import { ConnectorTypes } from '../../../common/api'; import { useKibana } from '../../common/lib/kibana'; import { getConnectorIcon } from '../utils'; +import * as i18n from './translations'; interface ConnectorCardProps { connectorType: ConnectorTypes; title: string; listItems: Array<{ title: string; description: React.ReactNode }>; isLoading: boolean; + showCommentsWarning: boolean; } const StyledText = styled.span` @@ -31,6 +40,7 @@ const ConnectorCardDisplay: React.FC = ({ title, listItems, isLoading, + showCommentsWarning = false, }) => { const { triggersActionsUi } = useKibana().services; @@ -69,7 +79,17 @@ const ConnectorCardDisplay: React.FC = ({ paddingSize="none" title={title} titleSize="xs" - /> + > + {showCommentsWarning && ( + +

{i18n.CREATE_COMMENT_WARNING_DESC(title)}

+
+ )} + {icon}
diff --git a/x-pack/plugins/cases/public/components/connectors/cases_webhook/case_fields.tsx b/x-pack/plugins/cases/public/components/connectors/cases_webhook/case_fields.tsx index 94dc7aed771dfc..d8f20184ee793e 100644 --- a/x-pack/plugins/cases/public/components/connectors/cases_webhook/case_fields.tsx +++ b/x-pack/plugins/cases/public/components/connectors/cases_webhook/case_fields.tsx @@ -21,6 +21,9 @@ const CasesWebhookComponent: React.FunctionComponent< isLoading={false} listItems={[]} title={connector.name} + showCommentsWarning={ + !connector.config?.createCommentUrl || !connector.config?.createCommentJson + } /> )} diff --git a/x-pack/plugins/cases/public/components/connectors/translations.ts b/x-pack/plugins/cases/public/components/connectors/translations.ts new file mode 100644 index 00000000000000..a97a8c06866ad0 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/translations.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const CREATE_COMMENT_WARNING_TITLE = i18n.translate( + 'xpack.cases.connectors.card.createCommentWarningTitle', + { + defaultMessage: 'Incomplete connector', + } +); + +export const CREATE_COMMENT_WARNING_DESC = (connectorName: string) => + i18n.translate('xpack.cases.connectors.card.createCommentWarningDesc', { + values: { connectorName }, + defaultMessage: + 'In order to update comments in external service, the {connectorName} connector needs to be updated with Create Comment URL and JSON', + }); From 1760fd5bd6daacefbd2914976f3af819ddf0847d Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 6 Jul 2022 12:37:47 -0600 Subject: [PATCH 35/97] add tags and comment to test form --- .../configure_cases/translations.ts | 23 +---- .../public/components/connectors/card.tsx | 2 +- .../cases_webhook/types.ts | 6 ++ .../cases_webhook/webhook.tsx | 2 +- .../cases_webhook/webhook_params.tsx | 83 +++++++++++++++++-- .../text_area_with_message_variables.tsx | 67 +++++++++++---- 6 files changed, 137 insertions(+), 46 deletions(-) diff --git a/x-pack/plugins/cases/public/components/configure_cases/translations.ts b/x-pack/plugins/cases/public/components/configure_cases/translations.ts index 070c934bbde46e..ada2690ab7b038 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/translations.ts +++ b/x-pack/plugins/cases/public/components/configure_cases/translations.ts @@ -110,13 +110,6 @@ export const FIELD_MAPPING_THIRD_COL = i18n.translate( } ); -export const FIELD_MAPPING_EDIT_APPEND = i18n.translate( - 'xpack.cases.configureCases.fieldMappingEditAppend', - { - defaultMessage: 'Append', - } -); - export const CANCEL = i18n.translate('xpack.cases.configureCases.cancelButton', { defaultMessage: 'Cancel', }); @@ -125,10 +118,6 @@ export const SAVE = i18n.translate('xpack.cases.configureCases.saveButton', { defaultMessage: 'Save', }); -export const SAVE_CLOSE = i18n.translate('xpack.cases.configureCases.saveAndCloseButton', { - defaultMessage: 'Save & close', -}); - export const WARNING_NO_CONNECTOR_TITLE = i18n.translate( 'xpack.cases.configureCases.warningTitle', { @@ -139,16 +128,6 @@ export const WARNING_NO_CONNECTOR_TITLE = i18n.translate( export const COMMENT = i18n.translate('xpack.cases.configureCases.commentMapping', { defaultMessage: 'Comments', }); -export const REQUIRED_MAPPINGS = (connectorName: string, fields: string): string => - i18n.translate('xpack.cases.configureCases.requiredMappings', { - values: { connectorName, fields }, - defaultMessage: - 'At least one Case field needs to be mapped to the following required { connectorName } fields: { fields }', - }); - -export const UPDATE_FIELD_MAPPINGS = i18n.translate('xpack.cases.configureCases.updateConnector', { - defaultMessage: 'Update field mappings', -}); export const UPDATE_SELECTED_CONNECTOR = (connectorName: string): string => i18n.translate('xpack.cases.configureCases.updateSelectedConnector', { @@ -178,6 +157,6 @@ export const CASES_WEBHOOK_MAPPINGS = i18n.translate( 'xpack.cases.configureCases.casesWebhookMappings', { defaultMessage: - 'Cases Webhook field mappings are configured in the connector settings in the third-party REST API JSON.', + 'Webhook - Case Management field mappings are configured in the connector settings in the third-party REST API JSON.', } ); diff --git a/x-pack/plugins/cases/public/components/connectors/card.tsx b/x-pack/plugins/cases/public/components/connectors/card.tsx index 749c7211e3303c..845e64f9e137da 100644 --- a/x-pack/plugins/cases/public/components/connectors/card.tsx +++ b/x-pack/plugins/cases/public/components/connectors/card.tsx @@ -26,7 +26,7 @@ interface ConnectorCardProps { title: string; listItems: Array<{ title: string; description: React.ReactNode }>; isLoading: boolean; - showCommentsWarning: boolean; + showCommentsWarning?: boolean; } const StyledText = styled.span` diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/types.ts index 17b826b5a326ef..b1867c28263e47 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/types.ts @@ -12,6 +12,7 @@ import { CasesWebhookSecretConfigurationType, ExecutorSubActionPushParams, } from '@kbn/actions-plugin/server/builtin_action_types/cases_webhook/types'; +import { UserConfiguredActionConnector } from '../../../../types'; export interface CasesWebhookActionParams { subAction: string; @@ -21,3 +22,8 @@ export interface CasesWebhookActionParams { export type CasesWebhookConfig = CasesWebhookPublicConfigurationType; export type CasesWebhookSecrets = CasesWebhookSecretConfigurationType; + +export type CasesWebhookActionConnector = UserConfiguredActionConnector< + CasesWebhookConfig, + CasesWebhookSecrets +>; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx index c42a4cd525b2dd..a933bba7308016 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx @@ -28,7 +28,7 @@ export function getActionType(): ActionTypeModel< actionTypeTitle: i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.actionTypeTitle', { - defaultMessage: 'Cases Webhook data', + defaultMessage: 'Webhook - Case Management data', } ), validateParams: async ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_params.tsx index a02f871f114dd6..d3c53d9d57be7e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_params.tsx @@ -7,20 +7,21 @@ import React, { useCallback, useEffect, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiFormRow } from '@elastic/eui'; +import { EuiComboBox, EuiFormRow } from '@elastic/eui'; import { ActionParamsProps } from '../../../../types'; -import { CasesWebhookActionParams } from './types'; +import { CasesWebhookActionConnector, CasesWebhookActionParams } from './types'; import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables'; import { TextFieldWithMessageVariables } from '../../text_field_with_message_variables'; const WebhookParamsFields: React.FunctionComponent> = ({ + actionConnector, actionParams, editAction, + errors, index, messageVariables, - errors, }) => { - const { incident } = useMemo( + const { incident, comments } = useMemo( () => actionParams.subActionParams ?? ({ @@ -29,13 +30,35 @@ const WebhookParamsFields: React.FunctionComponent (incident.labels ? incident.labels.map((label: string) => ({ label })) : []), + [incident.labels] + ); const editSubActionProperty = useCallback( (key: string, value: any) => { return editAction( 'subActionParams', { incident: { ...incident, [key]: value }, - comments: [], + comments, + }, + index + ); + }, + [comments, editAction, incident, index] + ); + const editComment = useCallback( + (key, value) => { + return editAction( + 'subActionParams', + { + incident, + comments: [{ commentId: '1', comment: value }], }, index ); @@ -99,6 +122,56 @@ const WebhookParamsFields: React.FunctionComponent + + { + const newOptions = [...labelOptions, { label: searchValue }]; + editSubActionProperty( + 'labels', + newOptions.map((newOption) => newOption.label) + ); + }} + onChange={(selectedOptions: Array<{ label: string }>) => { + editSubActionProperty( + 'labels', + selectedOptions.map((selectedOption) => selectedOption.label) + ); + }} + onBlur={() => { + if (!incident.labels) { + editSubActionProperty('labels', []); + } + }} + isClearable={true} + data-test-subj="tagsComboBox" + /> + + 0 ? comments[0].comment : undefined} + label={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhook.commentsTextAreaFieldLabel', + { + defaultMessage: 'Additional comments', + } + )} + /> ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx index 77938165cf264c..cacd098aa7ecfb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx @@ -6,17 +6,32 @@ */ import React, { useState } from 'react'; -import { EuiTextArea, EuiFormRow } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { EuiCallOut, EuiTextArea, EuiFormRow, EuiSpacer } from '@elastic/eui'; import './add_message_variables.scss'; import { ActionVariable } from '@kbn/alerting-plugin/common'; import { AddMessageVariables } from './add_message_variables'; import { templateActionVariable } from '../lib'; +const CREATE_COMMENT_WARNING_TITLE = i18n.translate( + 'xpack.triggersActionsUI.components.textAreaWithMessageVariable.createCommentWarningTitle', + { + defaultMessage: 'Incomplete connector', + } +); +const CREATE_COMMENT_WARNING_DESC = i18n.translate( + 'xpack.triggersActionsUI.components.textAreaWithMessageVariable.createCommentWarningDesc', + { + defaultMessage: + 'In order to update comments in external service, the connector needs to be updated with Create Comment URL and JSON', + } +); interface Props { messageVariables?: ActionVariable[]; paramsProperty: string; index: number; inputTargetValue?: string; + isDisabled?: boolean; editAction: (property: string, value: any, index: number) => void; label: string; errors?: string[]; @@ -27,6 +42,7 @@ export const TextAreaWithMessageVariables: React.FunctionComponent = ({ paramsProperty, index, inputTargetValue, + isDisabled = false, editAction, label, errors, @@ -52,6 +68,7 @@ export const TextAreaWithMessageVariables: React.FunctionComponent = ({ 0 && inputTargetValue !== undefined} label={label} labelAppend={ @@ -62,22 +79,38 @@ export const TextAreaWithMessageVariables: React.FunctionComponent = ({ /> } > - 0 && inputTargetValue !== undefined} - name={paramsProperty} - value={inputTargetValue || ''} - data-test-subj={`${paramsProperty}TextArea`} - onChange={(e: React.ChangeEvent) => onChangeWithMessageVariable(e)} - onFocus={(e: React.FocusEvent) => { - setCurrentTextElement(e.target); - }} - onBlur={() => { - if (!inputTargetValue) { - editAction(paramsProperty, '', index); - } - }} - /> + <> + {isDisabled && paramsProperty === 'comments' && ( + <> + +

{CREATE_COMMENT_WARNING_DESC}

+
+ + + )} + 0 && inputTargetValue !== undefined} + name={paramsProperty} + value={inputTargetValue || ''} + data-test-subj={`${paramsProperty}TextArea`} + onChange={(e: React.ChangeEvent) => onChangeWithMessageVariable(e)} + onFocus={(e: React.FocusEvent) => { + setCurrentTextElement(e.target); + }} + onBlur={() => { + if (!inputTargetValue) { + editAction(paramsProperty, '', index); + } + }} + /> +
); }; From ac980a494630048260401f9d925e8c1aa84cd56c Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 7 Jul 2022 13:14:17 -0600 Subject: [PATCH 36/97] make steps --- .../cases_webhook/steps/auth.tsx | 180 +++++ .../cases_webhook/steps/create.tsx | 126 ++++ .../cases_webhook/steps/get.tsx | 142 ++++ .../cases_webhook/steps/index.ts | 11 + .../cases_webhook/steps/update.tsx | 194 +++++ .../cases_webhook/translations.ts | 42 ++ .../cases_webhook/webhook_connectors.tsx | 703 ++++-------------- 7 files changed, 856 insertions(+), 542 deletions(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/auth.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/create.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/get.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/index.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/update.tsx diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/auth.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/auth.tsx new file mode 100644 index 00000000000000..244fd80a2c5197 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/auth.tsx @@ -0,0 +1,180 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FunctionComponent } from 'react'; +import { + EuiButtonEmpty, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; +import { + FIELD_TYPES, + UseArray, + UseField, + useFormContext, + useFormData, +} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { Field, TextField } from '@kbn/es-ui-shared-plugin/static/forms/components'; +import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; +import { PasswordField } from '../../../password_field'; +import * as i18n from '../translations'; +const { emptyField } = fieldValidators; + +interface Props { + display: boolean; + readOnly: boolean; +} + +export const AuthStep: FunctionComponent = ({ display, readOnly }) => { + const { getFieldDefaultValue } = useFormContext(); + const [{ config, __internal__ }] = useFormData({ + watch: ['config.hasAuth', '__internal__.hasHeaders'], + }); + + const hasHeadersDefaultValue = !!getFieldDefaultValue('config.headers'); + + const hasAuth = config == null ? true : config.hasAuth; + const hasHeaders = __internal__ != null ? __internal__.hasHeaders : false; + + return ( + + + + +

{i18n.AUTH_TITLE}

+
+ + +
+
+ {hasAuth ? ( + + + + + + + + + ) : null} + + + + {hasHeaders ? ( + + {({ items, addItem, removeItem }) => { + return ( + <> + +
{i18n.HEADERS_TITLE}
+
+ + {items.map((item) => ( + + + + + + + + + removeItem(item.id)} + iconType="minusInCircle" + aria-label={i18n.DELETE_BUTTON} + style={{ marginTop: '28px' }} + /> + + + ))} + + + {i18n.ADD_BUTTON} + + + + ); + }} +
+ ) : null} +
+ ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/create.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/create.tsx new file mode 100644 index 00000000000000..34dbac41646f73 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/create.tsx @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FunctionComponent } from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { FIELD_TYPES, UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { Field } from '@kbn/es-ui-shared-plugin/static/forms/components'; +import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; +import { containsTitleAndDesc } from '../validator'; +import { JsonFieldWrapper } from '../../../json_field_wrapper'; +import { casesVars } from '../action_variables'; +import { HTTP_VERBS } from '../webhook_connectors'; +import * as i18n from '../translations'; +const { emptyField, urlField } = fieldValidators; + +interface Props { + display: boolean; + readOnly: boolean; +} + +export const CreateStep: FunctionComponent = ({ display, readOnly }) => ( + + + + ({ text: verb.toUpperCase(), value: verb })), + readOnly, + }, + }} + /> + + + + + + + + + + + + + + + + +); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/get.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/get.tsx new file mode 100644 index 00000000000000..a762d1372d8251 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/get.tsx @@ -0,0 +1,142 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FunctionComponent } from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { Field } from '@kbn/es-ui-shared-plugin/static/forms/components'; +import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; +import { MustacheTextFieldWrapper } from '../../../mustache_text_field_wrapper'; +import { containsExternalId, containsExternalIdOrTitle } from '../validator'; +import { urlVars, urlVarsExt } from '../action_variables'; +import * as i18n from '../translations'; +const { emptyField, urlField } = fieldValidators; + +interface Props { + display: boolean; + readOnly: boolean; +} + +export const GetStep: FunctionComponent = ({ display, readOnly }) => ( + + + + + + + + + + + + + + + + + + + +); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/index.ts new file mode 100644 index 00000000000000..513f4ce45ca410 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './auth'; +export * from './create'; +export * from './get'; +export * from './update'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/update.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/update.tsx new file mode 100644 index 00000000000000..0220ea31a3ea5d --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/update.tsx @@ -0,0 +1,194 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FunctionComponent } from 'react'; +import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { FIELD_TYPES, UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { Field } from '@kbn/es-ui-shared-plugin/static/forms/components'; +import { + containsCommentsOrEmpty, + containsExternalId, + containsIdOrEmpty, + containsTitleAndDesc, + isUrlButCanBeEmpty, +} from '../validator'; +import { MustacheTextFieldWrapper } from '../../../mustache_text_field_wrapper'; +import { casesVars, commentVars, urlVars } from '../action_variables'; +import { JsonFieldWrapper } from '../../../json_field_wrapper'; +import { HTTP_VERBS } from '../webhook_connectors'; +import * as i18n from '../translations'; +const { emptyField, urlField } = fieldValidators; + +interface Props { + display: boolean; + readOnly: boolean; +} + +export const UpdateStep: FunctionComponent = ({ display, readOnly }) => ( + + + + ({ text: verb.toUpperCase(), value: verb })), + readOnly, + }, + }} + /> + + + + + + + + + + + + + ({ text: verb.toUpperCase(), value: verb })), + readOnly, + }, + }} + /> + + + + + + + + + + + +); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts index 040acaad9b7237..d975c72b9066b6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts @@ -458,3 +458,45 @@ export const AUTH_TITLE = i18n.translate( defaultMessage: 'Authentication', } ); + +export const STEP_1 = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.step1', + { + defaultMessage: 'Set up connector', + } +); + +export const STEP_2 = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.step2', + { + defaultMessage: 'Create incident in external system', + } +); + +export const STEP_3 = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.step3', + { + defaultMessage: 'Get incident information from external system', + } +); + +export const STEP_4 = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.step4', + { + defaultMessage: 'Add comments and updates in external system', + } +); + +export const NEXT = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.next', + { + defaultMessage: 'Next', + } +); + +export const PREVIOUS = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.previous', + { + defaultMessage: 'Previous', + } +); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx index eef76fefe81a24..895545cf95450b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx @@ -5,568 +5,187 @@ * 2.0. */ -import React from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useFormContext } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { - EuiButtonEmpty, - EuiButtonIcon, + EuiButton, EuiFlexGroup, EuiFlexItem, EuiSpacer, - EuiTitle, + EuiStepsHorizontal, + EuiStepStatus, } from '@elastic/eui'; -import { - FIELD_TYPES, - UseArray, - UseField, - useFormContext, - useFormData, -} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; -import { Field, TextField } from '@kbn/es-ui-shared-plugin/static/forms/components'; -import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; -import { - containsCommentsOrEmpty, - containsExternalId, - containsExternalIdOrTitle, - containsIdOrEmpty, - containsTitleAndDesc, - isUrlButCanBeEmpty, -} from './validator'; -import { MustacheTextFieldWrapper } from '../../mustache_text_field_wrapper'; -import { JsonFieldWrapper } from '../../json_field_wrapper'; -import { PasswordField } from '../../password_field'; import { ActionConnectorFieldsProps } from '../../../../types'; import * as i18n from './translations'; -import { casesVars, commentVars, urlVars, urlVarsExt } from './action_variables'; -const { emptyField, urlField } = fieldValidators; - -const HTTP_VERBS = ['post', 'put', 'patch']; +import { AuthStep, CreateStep, GetStep, UpdateStep } from './steps'; +export const HTTP_VERBS = ['post', 'put', 'patch']; +const fields = { + step1: [ + 'config.hasAuth', + 'secrets.user', + 'secrets.password', + '__internal__.hasHeaders', + 'config.headers', + ], + step2: [ + 'config.createIncidentMethod', + 'config.createIncidentUrl', + 'config.createIncidentJson', + 'config.createIncidentResponseKey', + ], + step3: [ + 'config.getIncidentUrl', + 'config.getIncidentResponseExternalTitleKey', + 'config.getIncidentResponseCreatedDateKey', + 'config.getIncidentResponseUpdatedDateKey', + 'config.incidentViewUrl', + ], + step4: [ + 'config.updateIncidentMethod', + 'config.updateIncidentUrl', + 'config.updateIncidentJson', + 'config.createCommentMethod', + 'config.createCommentUrl', + 'config.createCommentJson', + ], +}; +type PossibleStepNumbers = 1 | 2 | 3 | 4; const CasesWebhookActionConnectorFields: React.FunctionComponent = ({ readOnly, }) => { - const { getFieldDefaultValue } = useFormContext(); - const [{ config, __internal__ }] = useFormData({ - watch: ['config.hasAuth', '__internal__.hasHeaders'], + const context = useFormContext(); + const { isValid, validateFields } = context; + const [currentStep, setCurrentStep] = useState(1); + const [stati, setStati] = useState>({ + step1: 'incomplete', + step2: 'incomplete', + step3: 'incomplete', + step4: 'incomplete', }); + const updateStatus = useCallback(async () => { + const steps: PossibleStepNumbers[] = [1, 2, 3, 4]; + const statuses = await Promise.all( + steps.map(async (index) => { + if (typeof isValid !== 'undefined' && !isValid) { + const { areFieldsValid } = await validateFields(fields[`step${index}`]); + return { + [`step${index}`]: areFieldsValid ? 'complete' : ('danger' as EuiStepStatus), + }; + } + return { + [`step${index}`]: + currentStep === index + ? 'current' + : currentStep > index + ? 'complete' + : ('incomplete' as EuiStepStatus), + }; + }) + ); + setStati(statuses.reduce((acc: Record, i) => ({ ...acc, ...i }), {})); + }, [currentStep, isValid, validateFields]); + + useEffect(() => { + updateStatus(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isValid, currentStep]); - const hasHeadersDefaultValue = !!getFieldDefaultValue('config.headers'); + const [hasConfigurationErrors, setHasConfigurationError] = useState(false); - const hasAuth = config == null ? true : config.hasAuth; - const hasHeaders = __internal__ != null ? __internal__.hasHeaders : false; + const onNextStep = useCallback( + async (selectedStep?: PossibleStepNumbers) => { + const nextStep = + selectedStep != null + ? selectedStep + : currentStep === 4 + ? currentStep + : ((currentStep + 1) as PossibleStepNumbers); + setHasConfigurationError(false); + const fieldsToValidate: string[] = + nextStep === 2 + ? fields.step1 + : nextStep === 3 + ? [...fields.step1, ...fields.step2] + : nextStep === 4 + ? [...fields.step1, ...fields.step2, ...fields.step3] + : []; + const { areFieldsValid } = await validateFields(fieldsToValidate); + + if (!areFieldsValid) { + setHasConfigurationError(true); + return; + } + if (nextStep < 5) { + setCurrentStep(nextStep); + } + }, + [currentStep, validateFields] + ); + + const horizontalSteps = useMemo(() => { + return [ + { + title: i18n.STEP_1, + status: stati.step1, + onClick: () => setCurrentStep(1), + }, + { + title: i18n.STEP_2, + status: stati.step2, + onClick: () => onNextStep(2), + }, + { + title: i18n.STEP_3, + status: stati.step3, + onClick: () => onNextStep(3), + }, + { + title: i18n.STEP_4, + status: stati.step4, + onClick: () => onNextStep(4), + }, + ]; + }, [onNextStep, stati]); return ( <> - {/* start CREATE INCIDENT INPUTS */} - - - ({ text: verb.toUpperCase(), value: verb })), - readOnly, - }, - }} - /> - - - - - - - - - - - - - - - - {/* end CREATE INCIDENT INPUTS */} - {/* start GET INCIDENT INPUTS */} - - - - - - - - - - - - - - - - - - {/* end GET INCIDENT INPUTS */} - {/* start UPDATE INCIDENT INPUTS */} - - - ({ text: verb.toUpperCase(), value: verb })), - readOnly, - }, - }} - /> - - - - - - - - - - - {/* end UPDATE INCIDENT INPUTS */} - {/* start CREATE COMMENT INCIDENT INPUTS */} - - - ({ text: verb.toUpperCase(), value: verb })), - readOnly, - }, - }} - /> - - - - - - - - - - - {/* end CREATE COMMENT INCIDENT INPUTS */} - - - -

{i18n.AUTH_TITLE}

-
- - -
-
- {hasAuth ? ( - - - + + {hasConfigurationErrors &&

{'hasConfigurationErrors!!!!!'}

} + + + + + + + + {currentStep < 4 && ( + + onNextStep()} + > + {i18n.NEXT} + - - + )} + {currentStep > 1 && ( + + onNextStep((currentStep - 1) as PossibleStepNumbers)} + > + {i18n.PREVIOUS} + - - ) : null} - - - - {hasHeaders ? ( - - {({ items, addItem, removeItem }) => { - return ( - <> - -
{i18n.HEADERS_TITLE}
-
- - {items.map((item) => ( - - - - - - - - - removeItem(item.id)} - iconType="minusInCircle" - aria-label={i18n.DELETE_BUTTON} - style={{ marginTop: '28px' }} - /> - - - ))} - - - {i18n.ADD_BUTTON} - - - - ); - }} -
- ) : null} + )} +
); }; From 8486691d8d14b4d41483b58348e4c94fe9861268 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Fri, 8 Jul 2022 10:42:36 -0600 Subject: [PATCH 37/97] hide cases webhook from rule actions --- .../sections/action_connector_form/action_form.tsx | 7 ++++++- .../triggers_actions_ui/public/common/constants/index.ts | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx index ea57ca44f52e3c..1c1ecc4572fd99 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx @@ -35,7 +35,11 @@ import { ActionTypeForm } from './action_type_form'; import { AddConnectorInline } from './connector_add_inline'; import { actionTypeCompare } from '../../lib/action_type_compare'; import { checkActionFormActionTypeEnabled } from '../../lib/check_action_type_enabled'; -import { VIEW_LICENSE_OPTIONS_LINK, DEFAULT_HIDDEN_ACTION_TYPES } from '../../../common/constants'; +import { + VIEW_LICENSE_OPTIONS_LINK, + DEFAULT_HIDDEN_ACTION_TYPES, + RULES_HIDDEN_ACTION_TYPES, +} from '../../../common/constants'; import { useKibana } from '../../../common/lib/kibana'; import { DefaultActionParamsGetter } from '../../lib/get_defaults_for_action_params'; import { ConnectorAddModal } from '.'; @@ -237,6 +241,7 @@ export const ActionForm = ({ * If actionTypes are set, hidden connectors are filtered out. Otherwise, they are not. */ .filter(({ id }) => actionTypes ?? !DEFAULT_HIDDEN_ACTION_TYPES.includes(id)) + .filter(({ id }) => actionTypes ?? !RULES_HIDDEN_ACTION_TYPES.includes(id)) .filter((item) => actionTypesIndex[item.id]) .filter((item) => !!item.actionParamsFields) .sort((a, b) => diff --git a/x-pack/plugins/triggers_actions_ui/public/common/constants/index.ts b/x-pack/plugins/triggers_actions_ui/public/common/constants/index.ts index 8de3491dccb96a..0f920f16fb9370 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/constants/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/common/constants/index.ts @@ -12,5 +12,6 @@ export { builtInGroupByTypes } from './group_by_types'; export const VIEW_LICENSE_OPTIONS_LINK = 'https://www.elastic.co/subscriptions'; // TODO: Remove when cases connector is available across Kibana. Issue: https://github.com/elastic/kibana/issues/82502. export const DEFAULT_HIDDEN_ACTION_TYPES = ['.case']; +export const RULES_HIDDEN_ACTION_TYPES = ['.cases-webhook']; export const PLUGIN_ID = 'triggersActions'; From afaf444578625977431275fd2c2e57e3672561b1 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Fri, 8 Jul 2022 10:44:39 -0600 Subject: [PATCH 38/97] fix i18n --- x-pack/plugins/translations/translations/fr-FR.json | 4 ---- x-pack/plugins/translations/translations/ja-JP.json | 4 ---- x-pack/plugins/translations/translations/zh-CN.json | 4 ---- 3 files changed, 12 deletions(-) diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 3e708842f11750..f635021fd8f9f4 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -9976,7 +9976,6 @@ "xpack.cases.configureCases.deprecatedTooltipText": "déclassé", "xpack.cases.configureCases.fieldMappingDesc": "Mappez les champs des cas aux champs { thirdPartyName } lors de la transmission de données à { thirdPartyName }. Les mappings de champs requièrent une connexion établie à { thirdPartyName }.", "xpack.cases.configureCases.fieldMappingDescErr": "Impossible de récupérer les mappings pour { thirdPartyName }.", - "xpack.cases.configureCases.fieldMappingEditAppend": "Ajouter", "xpack.cases.configureCases.fieldMappingFirstCol": "Champ de cas Kibana", "xpack.cases.configureCases.fieldMappingSecondCol": "Champ { thirdPartyName }", "xpack.cases.configureCases.fieldMappingThirdCol": "Lors de la modification et de la mise à jour", @@ -9985,10 +9984,7 @@ "xpack.cases.configureCases.incidentManagementSystemDesc": "Connectez vos cas à un système de gestion des incidents externes. Vous pouvez ensuite transmettre les données de cas en tant qu'incident dans un système tiers.", "xpack.cases.configureCases.incidentManagementSystemLabel": "Système de gestion des incidents", "xpack.cases.configureCases.incidentManagementSystemTitle": "Système de gestion des incidents externes", - "xpack.cases.configureCases.requiredMappings": "Au moins un champ de cas doit être mappé aux champs { connectorName } requis suivants : { fields }", - "xpack.cases.configureCases.saveAndCloseButton": "Enregistrer et fermer", "xpack.cases.configureCases.saveButton": "Enregistrer", - "xpack.cases.configureCases.updateConnector": "Mettre à jour les mappings de champs", "xpack.cases.configureCases.updateSelectedConnector": "Mettre à jour { connectorName }", "xpack.cases.configureCases.warningMessage": "Le connecteur utilisé pour envoyer des mises à jour au service externe a été supprimé ou vous ne disposez pas de la {appropriateLicense} pour l'utiliser. Pour mettre à jour des cas dans des systèmes externes, sélectionnez un autre connecteur ou créez-en un nouveau.", "xpack.cases.configureCases.warningTitle": "Avertissement", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 9dec2d86b0dc7e..a467d8de9917cc 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9968,7 +9968,6 @@ "xpack.cases.configureCases.deprecatedTooltipText": "廃止予定", "xpack.cases.configureCases.fieldMappingDesc": "データを{ thirdPartyName }にプッシュするときに、ケースフィールドを{ thirdPartyName }フィールドにマッピングします。フィールドマッピングでは、{ thirdPartyName } への接続を確立する必要があります。", "xpack.cases.configureCases.fieldMappingDescErr": "{ thirdPartyName }のマッピングを取得できませんでした。", - "xpack.cases.configureCases.fieldMappingEditAppend": "末尾に追加", "xpack.cases.configureCases.fieldMappingFirstCol": "Kibanaケースフィールド", "xpack.cases.configureCases.fieldMappingSecondCol": "{ thirdPartyName } フィールド", "xpack.cases.configureCases.fieldMappingThirdCol": "編集時と更新時", @@ -9977,10 +9976,7 @@ "xpack.cases.configureCases.incidentManagementSystemDesc": "ケースを外部のインシデント管理システムに接続します。その後にサードパーティシステムでケースデータをインシデントとしてプッシュできます。", "xpack.cases.configureCases.incidentManagementSystemLabel": "インシデント管理システム", "xpack.cases.configureCases.incidentManagementSystemTitle": "外部インシデント管理システム", - "xpack.cases.configureCases.requiredMappings": "1 つ以上のケースフィールドを次の { connectorName } フィールドにマッピングする必要があります:{ fields }", - "xpack.cases.configureCases.saveAndCloseButton": "保存して閉じる", "xpack.cases.configureCases.saveButton": "保存", - "xpack.cases.configureCases.updateConnector": "フィールドマッピングを更新", "xpack.cases.configureCases.updateSelectedConnector": "{ connectorName }を更新", "xpack.cases.configureCases.warningMessage": "更新を外部サービスに送信するために使用されるコネクターが削除されたか、使用するための{appropriateLicense}がありません。外部システムでケースを更新するには、別のコネクターを選択するか、新しいコネクターを作成してください。", "xpack.cases.configureCases.warningTitle": "警告", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 98dca0f4644408..0753e1f6233c17 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9982,7 +9982,6 @@ "xpack.cases.configureCases.deprecatedTooltipText": "已过时", "xpack.cases.configureCases.fieldMappingDesc": "将数据推送到 { thirdPartyName } 时,将案例字段映射到 { thirdPartyName } 字段。字段映射需要与 { thirdPartyName } 建立连接。", "xpack.cases.configureCases.fieldMappingDescErr": "无法检索 { thirdPartyName } 的映射。", - "xpack.cases.configureCases.fieldMappingEditAppend": "追加", "xpack.cases.configureCases.fieldMappingFirstCol": "Kibana 案例字段", "xpack.cases.configureCases.fieldMappingSecondCol": "{ thirdPartyName } 字段", "xpack.cases.configureCases.fieldMappingThirdCol": "编辑和更新时", @@ -9991,10 +9990,7 @@ "xpack.cases.configureCases.incidentManagementSystemDesc": "将您的案例连接到外部事件管理系统。然后,您便可以将案例数据推送为第三方系统中的事件。", "xpack.cases.configureCases.incidentManagementSystemLabel": "事件管理系统", "xpack.cases.configureCases.incidentManagementSystemTitle": "外部事件管理系统", - "xpack.cases.configureCases.requiredMappings": "至少有一个案例字段需要映射到以下所需的 { connectorName } 字段:{ fields }", - "xpack.cases.configureCases.saveAndCloseButton": "保存并关闭", "xpack.cases.configureCases.saveButton": "保存", - "xpack.cases.configureCases.updateConnector": "更新字段映射", "xpack.cases.configureCases.updateSelectedConnector": "更新 { connectorName }", "xpack.cases.configureCases.warningMessage": "用于将更新发送到外部服务的连接器已删除,或您没有{appropriateLicense}来使用它。要在外部系统中更新案例,请选择不同的连接器或创建新的连接器。", "xpack.cases.configureCases.warningTitle": "警告", From 35d12889a6e482bc880a89ba86dec4ac0255b921 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Fri, 8 Jul 2022 13:36:14 -0600 Subject: [PATCH 39/97] monina changes --- .../components/add_message_variables.tsx | 42 +++++-- .../cases_webhook/steps/create.tsx | 1 + .../cases_webhook/steps/get.tsx | 6 +- .../cases_webhook/steps/update.tsx | 8 +- .../cases_webhook/translations.ts | 23 ++-- .../cases_webhook/webhook_connectors.tsx | 4 +- .../json_editor_with_message_variables.tsx | 3 + .../mustache_text_field_wrapper.tsx | 36 +++--- .../text_field_with_message_variables.tsx | 110 ++++++++++++------ 9 files changed, 146 insertions(+), 87 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx index b386e1d1d97bd5..0d8775853c215a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiPopover, @@ -13,6 +13,7 @@ import { EuiContextMenuPanel, EuiContextMenuItem, EuiText, + EuiButtonEmpty, } from '@elastic/eui'; import './add_message_variables.scss'; import { ActionVariable } from '@kbn/alerting-plugin/common'; @@ -23,6 +24,7 @@ interface Props { messageVariables?: ActionVariable[]; paramsProperty: string; onSelectEventHandler: (variable: ActionVariable) => void; + showButtonTitle?: boolean; } export const AddMessageVariables: React.FunctionComponent = ({ @@ -30,6 +32,7 @@ export const AddMessageVariables: React.FunctionComponent = ({ messageVariables, paramsProperty, onSelectEventHandler, + showButtonTitle = false, }) => { const [isVariablesPopoverOpen, setIsVariablesPopoverOpen] = useState(false); @@ -65,13 +68,25 @@ export const AddMessageVariables: React.FunctionComponent = ({ } ); - if ((messageVariables?.length ?? 0) === 0) { - return <>; - } - - return ( - + showButtonTitle ? ( + setIsVariablesPopoverOpen(true)} + iconType="indexOpen" + aria-label={i18n.translate( + 'xpack.triggersActionsUI.components.addMessageVariables.addVariablePopoverButton', + { + defaultMessage: 'Add variable', + } + )} + > + {addVariableButtonTitle} + + ) : ( = ({ } )} /> - } + ), + [addVariableButtonTitle, paramsProperty, showButtonTitle] + ); + if ((messageVariables?.length ?? 0) === 0) { + return <>; + } + + return ( + setIsVariablesPopoverOpen(false)} panelPaddingSize="none" diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/create.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/create.tsx index 34dbac41646f73..27934a7dba36c9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/create.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/create.tsx @@ -95,6 +95,7 @@ export const CreateStep: FunctionComponent = ({ display, readOnly }) => ( messageVariables: casesVars, paramsProperty: 'createIncidentJson', buttonTitle: i18n.ADD_CASES_VARIABLE, + showButtonTitle: true, }} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/get.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/get.tsx index a762d1372d8251..65151742860882 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/get.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/get.tsx @@ -44,7 +44,8 @@ export const GetStep: FunctionComponent = ({ display, readOnly }) => ( 'data-test-subj': 'webhookGetUrlText', messageVariables: urlVars, paramsProperty: 'getIncidentUrl', - buttonTitle: i18n.ADD_EXTERNAL_VARIABLE, + buttonTitle: i18n.ADD_CASES_VARIABLE, + showButtonTitle: true, }, }} /> @@ -132,7 +133,8 @@ export const GetStep: FunctionComponent = ({ display, readOnly }) => ( 'data-test-subj': 'incidentViewUrlText', messageVariables: urlVarsExt, paramsProperty: 'incidentViewUrl', - buttonTitle: i18n.ADD_EXTERNAL_VARIABLE, + buttonTitle: i18n.ADD_CASES_VARIABLE, + showButtonTitle: true, }, }} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/update.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/update.tsx index 0220ea31a3ea5d..484bfb4461179c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/update.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/update.tsx @@ -75,7 +75,8 @@ export const UpdateStep: FunctionComponent = ({ display, readOnly }) => ( 'data-test-subj': 'webhookUpdateUrlText', messageVariables: urlVars, paramsProperty: 'updateIncidentUrl', - buttonTitle: i18n.ADD_EXTERNAL_VARIABLE, + buttonTitle: i18n.ADD_CASES_VARIABLE, + showButtonTitle: true, }, }} /> @@ -108,6 +109,7 @@ export const UpdateStep: FunctionComponent = ({ display, readOnly }) => ( messageVariables: casesVars, paramsProperty: 'updateIncidentJson', buttonTitle: i18n.ADD_CASES_VARIABLE, + showButtonTitle: true, }} /> @@ -156,7 +158,8 @@ export const UpdateStep: FunctionComponent = ({ display, readOnly }) => ( 'data-test-subj': 'webhookCreateCommentUrlText', messageVariables: urlVars, paramsProperty: 'createCommentUrl', - buttonTitle: i18n.ADD_EXTERNAL_VARIABLE, + buttonTitle: i18n.ADD_CASES_VARIABLE, + showButtonTitle: true, }, }} /> @@ -186,6 +189,7 @@ export const UpdateStep: FunctionComponent = ({ display, readOnly }) => ( messageVariables: commentVars, paramsProperty: 'createCommentJson', buttonTitle: i18n.ADD_CASES_VARIABLE, + showButtonTitle: true, }} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts index d975c72b9066b6..14f573a56b7012 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts @@ -226,7 +226,7 @@ export const CREATE_INCIDENT_JSON_HELP = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.createIncidentJsonHelpText', { defaultMessage: - 'JSON object to create incident. Use the Add Variable button to add Cases data to the payload.', + 'JSON object to create incident. Use the variable selector to add Cases data to the payload.', } ); @@ -260,14 +260,7 @@ export const CREATE_INCIDENT_RESPONSE_KEY_HELP = i18n.translate( export const ADD_CASES_VARIABLE = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.addVariable', { - defaultMessage: 'Add cases variable', - } -); - -export const ADD_EXTERNAL_VARIABLE = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.addExtVariable', - { - defaultMessage: 'Add external system variable', + defaultMessage: 'Add variable', } ); @@ -281,7 +274,7 @@ export const GET_INCIDENT_URL_HELP = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.getIncidentUrlHelp', { defaultMessage: - 'API URL to GET incident details JSON from external system. Use the Add Variable button to add External System Id to the url.', + 'API URL to GET incident details JSON from external system. Use the variable selector to add external system id to the url.', } ); @@ -336,7 +329,7 @@ export const EXTERNAL_INCIDENT_VIEW_URL_HELP = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.incidentViewUrlHelp', { defaultMessage: - 'URL to view incident in external system. Use the Add Variable button to add External System Id or External System Title to the url.', + 'URL to view incident in external system. Use the variable selector to add external system id or external system title to the url.', } ); @@ -357,7 +350,7 @@ export const UPDATE_INCIDENT_URL_HELP = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.updateIncidentUrlHelp', { defaultMessage: - 'API URL to update incident. Use the Add Variable button to add External System Id to the url.', + 'API URL to update incident. Use the variable selector to add external system id to the url.', } ); @@ -371,7 +364,7 @@ export const UPDATE_INCIDENT_JSON_HELP = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.updateIncidentJsonHelpl', { defaultMessage: - 'JSON object to update incident. Use the Add Variable button to add Cases data to the payload.', + 'JSON object to update incident. Use the variable selector to add Cases data to the payload.', } ); @@ -392,7 +385,7 @@ export const CREATE_COMMENT_URL_HELP = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.createCommentUrlHelp', { defaultMessage: - 'API URL to add comment to incident. Use the Add Variable button to add External System Id to the url.', + 'API URL to add comment to incident. Use the variable selector to add external system id to the url.', } ); @@ -406,7 +399,7 @@ export const CREATE_COMMENT_JSON_HELP = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.createCommentJsonHelp', { defaultMessage: - 'JSON object to create a comment. Use the Add Variable button to add Cases data to the payload.', + 'JSON object to create a comment. Use the variable selector to add Cases data to the payload.', } ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx index 895545cf95450b..a0a029ee9130de 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx @@ -161,7 +161,7 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent {currentStep < 4 && ( - + )} {currentStep > 1 && ( - + void; helpText?: JSX.Element; onBlur?: () => void; + showButtonTitle?: boolean; } const { useXJsonMode } = XJson; @@ -64,6 +65,7 @@ export const JsonEditorWithMessageVariables: React.FunctionComponent = ({ onDocumentsChange, helpText, onBlur, + showButtonTitle, }) => { const editorRef = useRef(); const editorDisposables = useRef([]); @@ -154,6 +156,7 @@ export const JsonEditorWithMessageVariables: React.FunctionComponent = ({ messageVariables={messageVariables} onSelectEventHandler={onSelectMessageVariable} paramsProperty={paramsProperty} + showButtonTitle={showButtonTitle} /> } helpText={helpText} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/mustache_text_field_wrapper.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/mustache_text_field_wrapper.tsx index 1fe9e004021639..e1bdb26fca7229 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/mustache_text_field_wrapper.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/mustache_text_field_wrapper.tsx @@ -12,7 +12,6 @@ import { import React, { useCallback } from 'react'; import './json_field_wrapper.scss'; import { ActionVariable } from '@kbn/alerting-plugin/common'; -import { EuiFormRow } from '@elastic/eui'; import { TextFieldWithMessageVariables } from './text_field_with_message_variables'; interface Props { @@ -41,23 +40,22 @@ export const MustacheTextFieldWrapper = ({ field, euiFieldProps, idAria, ...rest ); return ( - - - + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx index 73199e212739cf..35b58c28062a87 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import React, { useState } from 'react'; -import { EuiFieldText } from '@elastic/eui'; +import React, { useCallback, useMemo, useState } from 'react'; +import { EuiFieldText, EuiFormRow } from '@elastic/eui'; import './add_message_variables.scss'; import { ActionVariable } from '@kbn/alerting-plugin/common'; import { AddMessageVariables } from './add_message_variables'; @@ -21,6 +21,16 @@ interface Props { editAction: (property: string, value: any, index: number) => void; errors?: string[]; defaultValue?: string | number | string[]; + wrapField?: boolean; + formRowProps?: { + describedByIds?: string[]; + error: string | null; + fullWidth: boolean; + helpText: string; + isInvalid: boolean; + label?: string; + }; + showButtonTitle?: boolean; } export const TextFieldWithMessageVariables: React.FunctionComponent = ({ @@ -31,51 +41,75 @@ export const TextFieldWithMessageVariables: React.FunctionComponent = ({ inputTargetValue, editAction, errors, + formRowProps, defaultValue, + wrapField = false, + showButtonTitle, }) => { const [currentTextElement, setCurrentTextElement] = useState(null); - const onSelectMessageVariable = (variable: ActionVariable) => { - const templatedVar = templateActionVariable(variable); - const startPosition = currentTextElement?.selectionStart ?? 0; - const endPosition = currentTextElement?.selectionEnd ?? 0; - const newValue = - (inputTargetValue ?? '').substring(0, startPosition) + - templatedVar + - (inputTargetValue ?? '').substring(endPosition, (inputTargetValue ?? '').length); - editAction(paramsProperty, newValue, index); - }; + const onSelectMessageVariable = useCallback( + (variable: ActionVariable) => { + const templatedVar = templateActionVariable(variable); + const startPosition = currentTextElement?.selectionStart ?? 0; + const endPosition = currentTextElement?.selectionEnd ?? 0; + const newValue = + (inputTargetValue ?? '').substring(0, startPosition) + + templatedVar + + (inputTargetValue ?? '').substring(endPosition, (inputTargetValue ?? '').length); + editAction(paramsProperty, newValue, index); + }, + [currentTextElement, editAction, index, inputTargetValue, paramsProperty] + ); const onChangeWithMessageVariable = (e: React.ChangeEvent) => { editAction(paramsProperty, e.target.value, index); }; + const VariableButton = useMemo( + () => ( + + ), + [buttonTitle, messageVariables, onSelectMessageVariable, paramsProperty, showButtonTitle] + ); + const Wrapper = useCallback( + ({ children }: { children: React.ReactElement }) => + wrapField ? ( + + {children} + + ) : ( + <>{children} + ), + [VariableButton, formRowProps, wrapField] + ); return ( - 0 && inputTargetValue !== undefined} - data-test-subj={`${paramsProperty}Input`} - value={inputTargetValue || ''} - defaultValue={defaultValue} - onChange={(e: React.ChangeEvent) => onChangeWithMessageVariable(e)} - onFocus={(e: React.FocusEvent) => { - setCurrentTextElement(e.target); - }} - onBlur={(e: React.FocusEvent) => { - if (!inputTargetValue) { - editAction(paramsProperty, '', index); - } - }} - append={ - - } - /> + + 0 && inputTargetValue !== undefined} + data-test-subj={`${paramsProperty}Input`} + value={inputTargetValue || ''} + defaultValue={defaultValue} + onChange={(e: React.ChangeEvent) => onChangeWithMessageVariable(e)} + onFocus={(e: React.FocusEvent) => { + setCurrentTextElement(e.target); + }} + onBlur={(e: React.FocusEvent) => { + if (!inputTargetValue) { + editAction(paramsProperty, '', index); + } + }} + append={wrapField ? undefined : VariableButton} + /> + ); }; From c60ec733fca6edf7f4cc4f0e0dae17e64503d973 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 11 Jul 2022 13:20:30 -0600 Subject: [PATCH 40/97] test fixing --- .../cases_webhook/webhook_connectors.test.tsx | 73 ++++++++++--------- .../cases_webhook/webhook_connectors.tsx | 3 +- .../cases_webhook/webhook_params.test.tsx | 38 ++++++++++ 3 files changed, 76 insertions(+), 38 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx index 8fb18c787a15fd..3882998dd7c924 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx @@ -47,7 +47,7 @@ const actionConnector = { config, }; describe('CasesWebhookActionConnectorFields renders', () => { - test('all connector fields is rendered', async () => { + test.only('all connector fields is rendered', async () => { const wrapper = mountWithIntl( { ); await waitForComponentToUpdate(); - expect(wrapper.find('[data-test-subj="webhookCreateMethodSelect"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="webhookCreateUrlText"]').length > 0).toBeTruthy(); - expect( - wrapper.find('[data-test-subj="createIncidentResponseKeyText"]').length > 0 - ).toBeTruthy(); - expect(wrapper.find('[data-test-subj="webhookCreateUrlText"]').length > 0).toBeTruthy(); - expect( - wrapper.find('[data-test-subj="getIncidentResponseExternalTitleKeyText"]').length > 0 - ).toBeTruthy(); - expect( - wrapper.find('[data-test-subj="getIncidentResponseCreatedDateKeyText"]').length > 0 - ).toBeTruthy(); - expect( - wrapper.find('[data-test-subj="getIncidentResponseUpdatedDateKeyText"]').length > 0 - ).toBeTruthy(); - expect(wrapper.find('[data-test-subj="incidentViewUrlText"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="webhookUpdateMethodSelect"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="webhookUpdateUrlText"]').length > 0).toBeTruthy(); - expect( - wrapper.find('[data-test-subj="webhookCreateCommentMethodSelect"]').length > 0 - ).toBeTruthy(); - expect(wrapper.find('[data-test-subj="webhookUpdateUrlText"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="webhookUserInput"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="webhookPasswordInput"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="webhookViewHeadersSwitch"]').length > 0).toBeTruthy(); - - expect(wrapper.find('[data-test-subj="webhookViewHeadersSwitch"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="webhookHeaderText"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="webhookAddHeaderButton"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="webhookHeadersKeyInput"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="webhookHeadersValueInput"]').length > 0).toBeTruthy(); - wrapper.find('[data-test-subj="webhookViewHeadersSwitch"]').last().simulate('click'); - expect(wrapper.find('[data-test-subj="webhookAddHeaderButton"]').length > 0).toBeFalsy(); - expect(wrapper.find('[data-test-subj="webhookHeadersKeyInput"]').length > 0).toBeFalsy(); - expect(wrapper.find('[data-test-subj="webhookHeadersValueInput"]').length > 0).toBeFalsy(); + expect(true).toBeTruthy(); + // expect(wrapper.find('[data-test-subj="webhookCreateMethodSelect"]').length > 0).toBeTruthy(); + // expect(wrapper.find('[data-test-subj="webhookCreateUrlText"]').length > 0).toBeTruthy(); + // expect( + // wrapper.find('[data-test-subj="createIncidentResponseKeyText"]').length > 0 + // ).toBeTruthy(); + // expect(wrapper.find('[data-test-subj="webhookCreateUrlText"]').length > 0).toBeTruthy(); + // expect( + // wrapper.find('[data-test-subj="getIncidentResponseExternalTitleKeyText"]').length > 0 + // ).toBeTruthy(); + // expect( + // wrapper.find('[data-test-subj="getIncidentResponseCreatedDateKeyText"]').length > 0 + // ).toBeTruthy(); + // expect( + // wrapper.find('[data-test-subj="getIncidentResponseUpdatedDateKeyText"]').length > 0 + // ).toBeTruthy(); + // expect(wrapper.find('[data-test-subj="incidentViewUrlText"]').length > 0).toBeTruthy(); + // expect(wrapper.find('[data-test-subj="webhookUpdateMethodSelect"]').length > 0).toBeTruthy(); + // expect(wrapper.find('[data-test-subj="webhookUpdateUrlText"]').length > 0).toBeTruthy(); + // expect( + // wrapper.find('[data-test-subj="webhookCreateCommentMethodSelect"]').length > 0 + // ).toBeTruthy(); + // expect(wrapper.find('[data-test-subj="webhookUpdateUrlText"]').length > 0).toBeTruthy(); + // expect(wrapper.find('[data-test-subj="webhookUserInput"]').length > 0).toBeTruthy(); + // expect(wrapper.find('[data-test-subj="webhookPasswordInput"]').length > 0).toBeTruthy(); + // expect(wrapper.find('[data-test-subj="webhookViewHeadersSwitch"]').length > 0).toBeTruthy(); + // + // expect(wrapper.find('[data-test-subj="webhookViewHeadersSwitch"]').length > 0).toBeTruthy(); + // expect(wrapper.find('[data-test-subj="webhookHeaderText"]').length > 0).toBeTruthy(); + // expect(wrapper.find('[data-test-subj="webhookAddHeaderButton"]').length > 0).toBeTruthy(); + // expect(wrapper.find('[data-test-subj="webhookHeadersKeyInput"]').length > 0).toBeTruthy(); + // expect(wrapper.find('[data-test-subj="webhookHeadersValueInput"]').length > 0).toBeTruthy(); + // wrapper.find('[data-test-subj="webhookViewHeadersSwitch"]').last().simulate('click'); + // expect(wrapper.find('[data-test-subj="webhookAddHeaderButton"]').length > 0).toBeFalsy(); + // expect(wrapper.find('[data-test-subj="webhookHeadersKeyInput"]').length > 0).toBeFalsy(); + // expect(wrapper.find('[data-test-subj="webhookHeadersValueInput"]').length > 0).toBeFalsy(); }); describe('Validation', () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx index a0a029ee9130de..57a74ce8b27d2d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx @@ -55,8 +55,7 @@ type PossibleStepNumbers = 1 | 2 | 3 | 4; const CasesWebhookActionConnectorFields: React.FunctionComponent = ({ readOnly, }) => { - const context = useFormContext(); - const { isValid, validateFields } = context; + const { isValid, validateFields } = useFormContext(); const [currentStep, setCurrentStep] = useState(1); const [stati, setStati] = useState>({ step1: 'incomplete', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_params.test.tsx index b17c9e2780e78c..630e910fc832cf 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_params.test.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import WebhookParamsFields from './webhook_params'; import { MockCodeEditor } from '../../../code_editor.mock'; +import { CasesWebhookActionConnector } from './types'; const kibanaReactPath = '../../../../../../../../src/plugins/kibana_react/public'; @@ -35,10 +36,18 @@ const actionParams = { }, }; +const actionConnector = { + config: { + createCommentUrl: 'https://elastic.co', + createCommentJson: {}, + }, +} as unknown as CasesWebhookActionConnector; + describe('WebhookParamsFields renders', () => { test('all params fields is rendered', () => { const wrapper = mountWithIntl( {}} @@ -54,5 +63,34 @@ describe('WebhookParamsFields renders', () => { ); expect(wrapper.find('[data-test-subj="summaryInput"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="descriptionTextArea"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="tagsComboBox"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="commentsTextArea"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="commentsTextArea"]').first().prop('disabled')).toEqual( + false + ); + }); + test('comments field is disabled when comment data is missing', () => { + const actionConnectorNoComments = { + config: {}, + } as unknown as CasesWebhookActionConnector; + const wrapper = mountWithIntl( + {}} + index={0} + messageVariables={[ + { + name: 'myVar', + description: 'My variable description', + useWithTripleBracesInTemplates: true, + }, + ]} + /> + ); + expect(wrapper.find('[data-test-subj="commentsTextArea"]').first().prop('disabled')).toEqual( + true + ); }); }); From 98b876c8aa3447705e0ad8dc5a1436d83f545b17 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 12 Jul 2022 08:12:37 -0600 Subject: [PATCH 41/97] fix text field input --- .../text_field_with_message_variables.tsx | 39 +++++++++++++------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx index 35b58c28062a87..0efe53603085e4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx @@ -33,6 +33,32 @@ interface Props { showButtonTitle?: boolean; } +const Wrapper = ({ + children, + wrapField, + formRowProps, + button, +}: { + wrapField: boolean; + children: React.ReactElement; + button: React.ReactElement; + formRowProps?: { + describedByIds?: string[]; + error: string | null; + fullWidth: boolean; + helpText: string; + isInvalid: boolean; + label?: string; + }; +}) => + wrapField ? ( + + {children} + + ) : ( + <>{children} + ); + export const TextFieldWithMessageVariables: React.FunctionComponent = ({ buttonTitle, messageVariables, @@ -77,20 +103,9 @@ export const TextFieldWithMessageVariables: React.FunctionComponent = ({ ), [buttonTitle, messageVariables, onSelectMessageVariable, paramsProperty, showButtonTitle] ); - const Wrapper = useCallback( - ({ children }: { children: React.ReactElement }) => - wrapField ? ( - - {children} - - ) : ( - <>{children} - ), - [VariableButton, formRowProps, wrapField] - ); return ( - + Date: Tue, 12 Jul 2022 13:36:02 -0600 Subject: [PATCH 42/97] tests wip --- .../cases_webhook/steps/auth.tsx | 5 +- .../cases_webhook/steps/create.tsx | 2 +- .../cases_webhook/steps/get.tsx | 2 +- .../cases_webhook/steps/update.tsx | 2 +- .../cases_webhook/translations.ts | 53 +--- .../cases_webhook/validator.ts | 19 +- .../cases_webhook/webhook.test.tsx | 2 +- .../cases_webhook/webhook_connectors.test.tsx | 237 ++++++++++++++---- .../cases_webhook/webhook_connectors.tsx | 34 +-- .../json_editor_with_message_variables.tsx | 3 + 10 files changed, 236 insertions(+), 123 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/auth.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/auth.tsx index 244fd80a2c5197..b356fb716d06f1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/auth.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/auth.tsx @@ -44,7 +44,7 @@ export const AuthStep: FunctionComponent = ({ display, readOnly }) => { const hasHeaders = __internal__ != null ? __internal__.hasHeaders : false; return ( - + @@ -59,6 +59,7 @@ export const AuthStep: FunctionComponent = ({ display, readOnly }) => { euiFieldProps: { label: i18n.HAS_AUTH, disabled: readOnly, + 'data-test-subj': 'hasAuthToggle', }, }} /> @@ -120,7 +121,7 @@ export const AuthStep: FunctionComponent = ({ display, readOnly }) => { {items.map((item) => ( - + = ({ display, readOnly }) => ( - + = ({ display, readOnly }) => ( - + = ({ display, readOnly }) => ( - + +export const MISSING_VARIABLES = (variables: string[]) => i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.error.invalidUrlTextField', + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.error.missingVariables', { - defaultMessage: '{urlType} URL is invalid.', - values: { - urlType, - }, + defaultMessage: + 'Missing required {variableCount, plural, one {variable} other {variables}}: {variables}', + values: { variableCount: variables.length, variables: variables.join(', ') }, } ); @@ -123,27 +122,6 @@ export const USERNAME_REQUIRED = i18n.translate( } ); -export const PASSWORD_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.sections.addAction.casesWebhookAction.error.requiredAuthPasswordText', - { - defaultMessage: 'Password is required.', - } -); - -export const PASSWORD_REQUIRED_FOR_USER = i18n.translate( - 'xpack.triggersActionsUI.sections.addAction.casesWebhookAction.error.requiredPasswordText', - { - defaultMessage: 'Password is required when username is used.', - } -); - -export const USERNAME_REQUIRED_FOR_PASSWORD = i18n.translate( - 'xpack.triggersActionsUI.sections.addAction.casesWebhookAction.error.requiredUserText', - { - defaultMessage: 'Username is required when password is used.', - } -); - export const SUMMARY_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredWebhookSummaryText', { @@ -151,27 +129,6 @@ export const SUMMARY_REQUIRED = i18n.translate( } ); -export const KEY_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.sections.addAction.casesWebhookAction.error.requiredHeaderKeyText', - { - defaultMessage: 'Key is required.', - } -); - -export const VALUE_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.sections.addAction.casesWebhookAction.error.requiredHeaderValueText', - { - defaultMessage: 'Value is required.', - } -); - -export const ADD_HEADER = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.addHeader', - { - defaultMessage: 'Add header', - } -); - export const KEY_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.keyTextFieldLabel', { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/validator.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/validator.ts index 7f7d8a75b2b8fb..a06aa61a0f1936 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/validator.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/validator.ts @@ -11,15 +11,16 @@ import { ValidationFunc, } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { containsChars, isUrl } from '@kbn/es-ui-shared-plugin/static/validators/string'; +import * as i18n from './translations'; import { casesVars, commentVars, urlVars, urlVarsExt } from './action_variables'; import { templateActionVariable } from '../../../lib'; const errorCode: ERROR_CODE = 'ERR_FIELD_MISSING'; -const missingVariable = (path: string, variable: string, plural?: boolean) => ({ +const missingVariable = (path: string, variables: string[]) => ({ code: errorCode, path, - message: `Missing required ${variable} variable${plural ? 's' : ''}`, + message: i18n.MISSING_VARIABLES(variables), }); export const containsTitleAndDesc = @@ -38,17 +39,17 @@ export const containsTitleAndDesc = if (!error) { const { doesContain } = containsChars(title)(value); if (!doesContain) { - error = missingVariable(path, title); + error = missingVariable(path, [title]); } } if (!error) { const { doesContain } = containsChars(description)(value); - error = !doesContain ? missingVariable(path, description) : undefined; + error = !doesContain ? missingVariable(path, [description]) : undefined; } else { const { doesContain } = containsChars(description)(value); error = !doesContain - ? missingVariable(path, `${title} and ${description}`, true) - : missingVariable(path, title); + ? missingVariable(path, [title, description]) + : missingVariable(path, [title]); } } return error; @@ -66,7 +67,7 @@ export const containsExternalId = if (typeof value === 'string') { const { doesContain } = containsChars(id)(value); if (!doesContain) { - error = missingVariable(path, id); + error = missingVariable(path, [id]); } } return error; @@ -83,7 +84,7 @@ export const containsExternalIdOrTitle = const title = templateActionVariable( urlVarsExt.find((actionVariable) => actionVariable.name === 'external.system.title')! ); - const error = missingVariable(path, `${id} or ${title}`); + const error = missingVariable(path, [id, title]); if (typeof value === 'string') { const { doesContain: doesContainId } = containsChars(id)(value); const { doesContain: doesContainTitle } = containsChars(title)(value); @@ -133,7 +134,7 @@ export const containsCommentsOrEmpty = if (typeof value === 'string') { const { doesContain } = containsChars(comment)(value); if (!doesContain) { - error = missingVariable(path, comment); + error = missingVariable(path, [comment]); } } return error; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.test.tsx index a834e361791fd1..86cfe366f9751a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.test.tsx @@ -25,7 +25,7 @@ beforeAll(() => { describe('actionTypeRegistry.get() works', () => { test('action type static data is as expected', () => { expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID); - expect(actionTypeModel.iconClass).toEqual('indexManagementApp'); + expect(actionTypeModel.iconClass).toEqual('logoWebhook'); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx index 3882998dd7c924..04936ffb5feccc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx @@ -6,19 +6,32 @@ */ import React from 'react'; -import { mountWithIntl } from '@kbn/test-jest-helpers'; import CasesWebhookActionConnectorFields from './webhook_connectors'; import { ConnectorFormTestProvider, waitForComponentToUpdate } from '../test_utils'; import { act, render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -jest.mock('../../../../common/lib/kibana'); +import { MockCodeEditor } from '../../../code_editor.mock'; +import * as i18n from './translations'; +const kibanaReactPath = '../../../../../../../../src/plugins/kibana_react/public'; +jest.mock(kibanaReactPath, () => { + const original = jest.requireActual(kibanaReactPath); + return { + ...original, + CodeEditor: (props: any) => { + return ; + }, + }; +}); + +const invalidJson = + '{"fields":{"summary":"wrong","description":"wrong","project":{"key":"ROC"},"issuetype":{"id":"10024"}}}'; const config = { - createCommentJson: '{"body":"$COMMENT"}', + createCommentJson: '{"body":{{{case.comment}}}}', createCommentMethod: 'post', createCommentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/$ID/comment', createIncidentJson: - '{"fields":{"summary":"$SUM","description":"$DESC","project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', + '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', createIncidentMethod: 'post', createIncidentResponseKey: 'id', createIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue', @@ -30,7 +43,7 @@ const config = { incidentViewUrl: 'https://siem-kibana.atlassian.net/browse/$TITLE', getIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/$ID', updateIncidentJson: - '{"fields":{"summary":"$SUM","description":"$DESC","project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', + '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', updateIncidentMethod: 'put', updateIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/$ID', }; @@ -46,9 +59,20 @@ const actionConnector = { name: 'cases webhook', config, }; + +const completeStep1 = async (getByTestId: (id: string) => HTMLElement) => { + await act(async () => { + userEvent.click(getByTestId('hasAuthToggle')); + userEvent.click(getByTestId('webhookViewHeadersSwitch')); + }); + await act(async () => { + userEvent.click(getByTestId('casesWebhookNext')); + }); +}; + describe('CasesWebhookActionConnectorFields renders', () => { - test.only('all connector fields is rendered', async () => { - const wrapper = mountWithIntl( + test('All inputs are properly rendered', async () => { + const { getByTestId } = render( { /> ); - await waitForComponentToUpdate(); - expect(true).toBeTruthy(); - // expect(wrapper.find('[data-test-subj="webhookCreateMethodSelect"]').length > 0).toBeTruthy(); - // expect(wrapper.find('[data-test-subj="webhookCreateUrlText"]').length > 0).toBeTruthy(); - // expect( - // wrapper.find('[data-test-subj="createIncidentResponseKeyText"]').length > 0 - // ).toBeTruthy(); - // expect(wrapper.find('[data-test-subj="webhookCreateUrlText"]').length > 0).toBeTruthy(); - // expect( - // wrapper.find('[data-test-subj="getIncidentResponseExternalTitleKeyText"]').length > 0 - // ).toBeTruthy(); - // expect( - // wrapper.find('[data-test-subj="getIncidentResponseCreatedDateKeyText"]').length > 0 - // ).toBeTruthy(); - // expect( - // wrapper.find('[data-test-subj="getIncidentResponseUpdatedDateKeyText"]').length > 0 - // ).toBeTruthy(); - // expect(wrapper.find('[data-test-subj="incidentViewUrlText"]').length > 0).toBeTruthy(); - // expect(wrapper.find('[data-test-subj="webhookUpdateMethodSelect"]').length > 0).toBeTruthy(); - // expect(wrapper.find('[data-test-subj="webhookUpdateUrlText"]').length > 0).toBeTruthy(); - // expect( - // wrapper.find('[data-test-subj="webhookCreateCommentMethodSelect"]').length > 0 - // ).toBeTruthy(); - // expect(wrapper.find('[data-test-subj="webhookUpdateUrlText"]').length > 0).toBeTruthy(); - // expect(wrapper.find('[data-test-subj="webhookUserInput"]').length > 0).toBeTruthy(); - // expect(wrapper.find('[data-test-subj="webhookPasswordInput"]').length > 0).toBeTruthy(); - // expect(wrapper.find('[data-test-subj="webhookViewHeadersSwitch"]').length > 0).toBeTruthy(); - // - // expect(wrapper.find('[data-test-subj="webhookViewHeadersSwitch"]').length > 0).toBeTruthy(); - // expect(wrapper.find('[data-test-subj="webhookHeaderText"]').length > 0).toBeTruthy(); - // expect(wrapper.find('[data-test-subj="webhookAddHeaderButton"]').length > 0).toBeTruthy(); - // expect(wrapper.find('[data-test-subj="webhookHeadersKeyInput"]').length > 0).toBeTruthy(); - // expect(wrapper.find('[data-test-subj="webhookHeadersValueInput"]').length > 0).toBeTruthy(); - // wrapper.find('[data-test-subj="webhookViewHeadersSwitch"]').last().simulate('click'); - // expect(wrapper.find('[data-test-subj="webhookAddHeaderButton"]').length > 0).toBeFalsy(); - // expect(wrapper.find('[data-test-subj="webhookHeadersKeyInput"]').length > 0).toBeFalsy(); - // expect(wrapper.find('[data-test-subj="webhookHeadersValueInput"]').length > 0).toBeFalsy(); + expect(getByTestId('webhookUserInput')).toBeInTheDocument(); + expect(getByTestId('webhookPasswordInput')).toBeInTheDocument(); + expect(getByTestId('webhookHeadersKeyInput')).toBeInTheDocument(); + expect(getByTestId('webhookHeadersValueInput')).toBeInTheDocument(); + expect(getByTestId('webhookCreateMethodSelect')).toBeInTheDocument(); + expect(getByTestId('webhookCreateUrlText')).toBeInTheDocument(); + expect(getByTestId('webhookCreateIncidentJson')).toBeInTheDocument(); + expect(getByTestId('createIncidentResponseKeyText')).toBeInTheDocument(); + expect(getByTestId('getIncidentUrlInput')).toBeInTheDocument(); + expect(getByTestId('getIncidentResponseExternalTitleKeyText')).toBeInTheDocument(); + expect(getByTestId('getIncidentResponseCreatedDateKeyText')).toBeInTheDocument(); + expect(getByTestId('getIncidentResponseUpdatedDateKeyText')).toBeInTheDocument(); + expect(getByTestId('incidentViewUrlInput')).toBeInTheDocument(); + expect(getByTestId('webhookUpdateMethodSelect')).toBeInTheDocument(); + expect(getByTestId('updateIncidentUrlInput')).toBeInTheDocument(); + expect(getByTestId('webhookUpdateIncidentJson')).toBeInTheDocument(); + expect(getByTestId('webhookCreateCommentMethodSelect')).toBeInTheDocument(); + expect(getByTestId('createCommentUrlInput')).toBeInTheDocument(); + expect(getByTestId('webhookCreateCommentJson')).toBeInTheDocument(); + }); + describe('Step 1 Validation', () => { + test('Step 1 toggles work properly', async () => { + const { getByTestId, queryByTestId } = render( + + {}} + /> + + ); + await waitForComponentToUpdate(); + expect(getByTestId('hasAuthToggle')).toHaveAttribute('aria-checked', 'true'); + await act(async () => { + userEvent.click(getByTestId('hasAuthToggle')); + }); + expect(getByTestId('hasAuthToggle')).toHaveAttribute('aria-checked', 'false'); + expect(queryByTestId('webhookUserInput')).not.toBeInTheDocument(); + expect(queryByTestId('webhookPasswordInput')).not.toBeInTheDocument(); + + expect(getByTestId('webhookViewHeadersSwitch')).toHaveAttribute('aria-checked', 'true'); + await act(async () => { + userEvent.click(getByTestId('webhookViewHeadersSwitch')); + }); + expect(getByTestId('webhookViewHeadersSwitch')).toHaveAttribute('aria-checked', 'false'); + expect(queryByTestId('webhookHeadersKeyInput')).not.toBeInTheDocument(); + expect(queryByTestId('webhookHeadersValueInput')).not.toBeInTheDocument(); + }); + test('Step 1 is properly validated', async () => { + const { getByTestId } = render( + + {}} + /> + + ); + await waitForComponentToUpdate(); + + expect(getByTestId('authStep')).toHaveAttribute('style', 'display: block;'); + expect(getByTestId('createStep')).toHaveAttribute('style', 'display: none;'); + expect(getByTestId('getStep')).toHaveAttribute('style', 'display: none;'); + expect(getByTestId('updateStep')).toHaveAttribute('style', 'display: none;'); + expect(getByTestId('horizontalStep1-current')).toBeInTheDocument(); + + await act(async () => { + await userEvent.type(getByTestId('webhookUserInput'), `{selectall}{backspace}`); + await userEvent.type(getByTestId('webhookPasswordInput'), `{selectall}{backspace}`); + userEvent.click(getByTestId('casesWebhookNext')); + }); + + expect(getByTestId('horizontalStep1-danger')).toBeInTheDocument(); + expect(getByTestId('horizontalStep2-incomplete')).toBeInTheDocument(); + + await completeStep1(getByTestId); + + expect(getByTestId('horizontalStep1-complete')).toBeInTheDocument(); + expect(getByTestId('horizontalStep2-current')).toBeInTheDocument(); + }); + }); + describe('Step 2 Validation', () => { + test('Step 2 is properly validated when url field is missing', async () => { + const incompleteActionConnector = { + ...actionConnector, + config: { + ...actionConnector.config, + createIncidentUrl: '', + }, + }; + const { getByText, getByTestId } = render( + + {}} + /> + + ); + await waitForComponentToUpdate(); + expect(getByTestId('horizontalStep2-incomplete')).toBeInTheDocument(); + await completeStep1(getByTestId); + expect(getByTestId('authStep')).toHaveAttribute('style', 'display: none;'); + expect(getByTestId('createStep')).toHaveAttribute('style', 'display: block;'); + expect(getByTestId('getStep')).toHaveAttribute('style', 'display: none;'); + expect(getByTestId('updateStep')).toHaveAttribute('style', 'display: none;'); + await act(async () => { + userEvent.click(getByTestId('casesWebhookNext')); + }); + getByText(i18n.CREATE_URL_REQUIRED); + expect(getByTestId('horizontalStep2-danger')).toBeInTheDocument(); + expect(getByTestId('horizontalStep3-incomplete')).toBeInTheDocument(); + await act(async () => { + await userEvent.type( + getByTestId('webhookCreateUrlText'), + `{selectall}{backspace}${config.createIncidentUrl}`, + { + delay: 10, + } + ); + }); + await act(async () => { + userEvent.click(getByTestId('casesWebhookNext')); + }); + expect(getByTestId('horizontalStep2-complete')).toBeInTheDocument(); + expect(getByTestId('horizontalStep3-current')).toBeInTheDocument(); + await act(async () => { + userEvent.click(getByTestId('horizontalStep2-complete')); + }); + expect(getByTestId('horizontalStep2-current')).toBeInTheDocument(); + expect(getByTestId('horizontalStep3-incomplete')).toBeInTheDocument(); + }); + test('Step 2 is properly validated when json is missing case variables', async () => { + const incompleteActionConnector = { + ...actionConnector, + config: { + ...actionConnector.config, + createIncidentJson: invalidJson, + }, + }; + const { getByTestId, getByText } = render( + + {}} + /> + + ); + await waitForComponentToUpdate(); + await completeStep1(getByTestId); + expect(getByTestId('horizontalStep2-current')).toBeInTheDocument(); + await act(async () => { + userEvent.click(getByTestId('casesWebhookNext')); + }); + expect(getByTestId('horizontalStep2-danger')).toBeInTheDocument(); + expect( + getByText(i18n.MISSING_VARIABLES(['{{{case.title}}}', '{{{case.description}}}'])) + ).toBeInTheDocument(); + }); }); - describe('Validation', () => { + describe.skip('Validation', () => { const onSubmit = jest.fn(); beforeEach(() => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx index 57a74ce8b27d2d..cbb35fda495a0b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx @@ -57,7 +57,7 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent { const { isValid, validateFields } = useFormContext(); const [currentStep, setCurrentStep] = useState(1); - const [stati, setStati] = useState>({ + const [status, setStatus] = useState>({ step1: 'incomplete', step2: 'incomplete', step3: 'incomplete', @@ -83,7 +83,7 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent, i) => ({ ...acc, ...i }), {})); + setStatus(statuses.reduce((acc: Record, i) => ({ ...acc, ...i }), {})); }, [currentStep, isValid, validateFields]); useEffect(() => { @@ -91,8 +91,6 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent { const nextStep = @@ -101,7 +99,6 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent ({ + ...currentStatus, + [`step${currentStep}`]: 'danger', + })); return; } if (nextStep < 5) { @@ -123,35 +123,39 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent { - return [ + const horizontalSteps = useMemo( + () => [ { title: i18n.STEP_1, - status: stati.step1, + status: status.step1, onClick: () => setCurrentStep(1), + ['data-test-subj']: `horizontalStep1-${status.step1}`, }, { title: i18n.STEP_2, - status: stati.step2, + status: status.step2, onClick: () => onNextStep(2), + ['data-test-subj']: `horizontalStep2-${status.step2}`, }, { title: i18n.STEP_3, - status: stati.step3, + status: status.step3, onClick: () => onNextStep(3), + ['data-test-subj']: `horizontalStep3-${status.step3}`, }, { title: i18n.STEP_4, - status: stati.step4, + status: status.step4, onClick: () => onNextStep(4), + ['data-test-subj']: `horizontalStep4-${status.step4}`, }, - ]; - }, [onNextStep, stati]); + ], + [onNextStep, status] + ); return ( <> - {hasConfigurationErrors &&

{'hasConfigurationErrors!!!!!'}

} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.tsx index 9d3ac0880ec4e5..5bdc25fd48752f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.tsx @@ -45,6 +45,7 @@ interface Props { helpText?: JSX.Element; onBlur?: () => void; showButtonTitle?: boolean; + euiCodeEditorProps?: { [key: string]: any }; } const { useXJsonMode } = XJson; @@ -66,6 +67,7 @@ export const JsonEditorWithMessageVariables: React.FunctionComponent = ({ helpText, onBlur, showButtonTitle, + euiCodeEditorProps = {}, }) => { const editorRef = useRef(); const editorDisposables = useRef([]); @@ -189,6 +191,7 @@ export const JsonEditorWithMessageVariables: React.FunctionComponent = ({ // Keep the documents in sync with the editor content onDocumentsChange(convertToJson(xjson)); }} + {...euiCodeEditorProps} /> From 60d5be333c7a75b98e8bb220d98cf9e02c4cce94 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 12 Jul 2022 14:59:06 -0600 Subject: [PATCH 43/97] finish webhook connectors test --- .../cases_webhook/webhook_connectors.test.tsx | 247 +++++++++++++----- 1 file changed, 187 insertions(+), 60 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx index 04936ffb5feccc..70c6fa5f5f2b56 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx @@ -24,12 +24,13 @@ jest.mock(kibanaReactPath, () => { }; }); -const invalidJson = - '{"fields":{"summary":"wrong","description":"wrong","project":{"key":"ROC"},"issuetype":{"id":"10024"}}}'; +const invalidJsonTitle = `{"fields":{"summary":"wrong","description":{{{case.description}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}`; +const invalidJsonBoth = `{"fields":{"summary":"wrong","description":"wrong","project":{"key":"ROC"},"issuetype":{"id":"10024"}}}`; const config = { createCommentJson: '{"body":{{{case.comment}}}}', createCommentMethod: 'post', - createCommentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/$ID/comment', + createCommentUrl: + 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}/comment', createIncidentJson: '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', createIncidentMethod: 'post', @@ -40,12 +41,12 @@ const config = { getIncidentResponseUpdatedDateKey: 'fields.udpated', hasAuth: true, headers: [{ key: 'content-type', value: 'text' }], - incidentViewUrl: 'https://siem-kibana.atlassian.net/browse/$TITLE', - getIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/$ID', + incidentViewUrl: 'https://siem-kibana.atlassian.net/browse/{{{external.system.title}}}', + getIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}', updateIncidentJson: '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', updateIncidentMethod: 'put', - updateIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/$ID', + updateIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}', }; const actionConnector = { secrets: { @@ -60,16 +61,6 @@ const actionConnector = { config, }; -const completeStep1 = async (getByTestId: (id: string) => HTMLElement) => { - await act(async () => { - userEvent.click(getByTestId('hasAuthToggle')); - userEvent.click(getByTestId('webhookViewHeadersSwitch')); - }); - await act(async () => { - userEvent.click(getByTestId('casesWebhookNext')); - }); -}; - describe('CasesWebhookActionConnectorFields renders', () => { test('All inputs are properly rendered', async () => { const { getByTestId } = render( @@ -102,9 +93,36 @@ describe('CasesWebhookActionConnectorFields renders', () => { expect(getByTestId('createCommentUrlInput')).toBeInTheDocument(); expect(getByTestId('webhookCreateCommentJson')).toBeInTheDocument(); }); - describe('Step 1 Validation', () => { - test('Step 1 toggles work properly', async () => { - const { getByTestId, queryByTestId } = render( + test('Toggles work properly', async () => { + const { getByTestId, queryByTestId } = render( + + {}} + /> + + ); + await waitForComponentToUpdate(); + expect(getByTestId('hasAuthToggle')).toHaveAttribute('aria-checked', 'true'); + await act(async () => { + userEvent.click(getByTestId('hasAuthToggle')); + }); + expect(getByTestId('hasAuthToggle')).toHaveAttribute('aria-checked', 'false'); + expect(queryByTestId('webhookUserInput')).not.toBeInTheDocument(); + expect(queryByTestId('webhookPasswordInput')).not.toBeInTheDocument(); + + expect(getByTestId('webhookViewHeadersSwitch')).toHaveAttribute('aria-checked', 'true'); + await act(async () => { + userEvent.click(getByTestId('webhookViewHeadersSwitch')); + }); + expect(getByTestId('webhookViewHeadersSwitch')).toHaveAttribute('aria-checked', 'false'); + expect(queryByTestId('webhookHeadersKeyInput')).not.toBeInTheDocument(); + expect(queryByTestId('webhookHeadersValueInput')).not.toBeInTheDocument(); + }); + describe('Step Validation', () => { + test('Steps work correctly when all fields valid', async () => { + const { queryByTestId, getByTestId } = render( { ); await waitForComponentToUpdate(); - expect(getByTestId('hasAuthToggle')).toHaveAttribute('aria-checked', 'true'); + expect(getByTestId('horizontalStep1-current')).toBeInTheDocument(); + expect(getByTestId('horizontalStep2-incomplete')).toBeInTheDocument(); + expect(getByTestId('horizontalStep3-incomplete')).toBeInTheDocument(); + expect(getByTestId('horizontalStep4-incomplete')).toBeInTheDocument(); + expect(getByTestId('authStep')).toHaveAttribute('style', 'display: block;'); + expect(getByTestId('createStep')).toHaveAttribute('style', 'display: none;'); + expect(getByTestId('getStep')).toHaveAttribute('style', 'display: none;'); + expect(getByTestId('updateStep')).toHaveAttribute('style', 'display: none;'); + expect(queryByTestId('casesWebhookBack')).not.toBeInTheDocument(); await act(async () => { - userEvent.click(getByTestId('hasAuthToggle')); + userEvent.click(getByTestId('casesWebhookNext')); }); - expect(getByTestId('hasAuthToggle')).toHaveAttribute('aria-checked', 'false'); - expect(queryByTestId('webhookUserInput')).not.toBeInTheDocument(); - expect(queryByTestId('webhookPasswordInput')).not.toBeInTheDocument(); - - expect(getByTestId('webhookViewHeadersSwitch')).toHaveAttribute('aria-checked', 'true'); + expect(getByTestId('horizontalStep1-complete')).toBeInTheDocument(); + expect(getByTestId('horizontalStep2-current')).toBeInTheDocument(); + expect(getByTestId('horizontalStep3-incomplete')).toBeInTheDocument(); + expect(getByTestId('horizontalStep4-incomplete')).toBeInTheDocument(); + expect(getByTestId('authStep')).toHaveAttribute('style', 'display: none;'); + expect(getByTestId('createStep')).toHaveAttribute('style', 'display: block;'); + expect(getByTestId('getStep')).toHaveAttribute('style', 'display: none;'); + expect(getByTestId('updateStep')).toHaveAttribute('style', 'display: none;'); await act(async () => { - userEvent.click(getByTestId('webhookViewHeadersSwitch')); + userEvent.click(getByTestId('casesWebhookNext')); + }); + expect(getByTestId('horizontalStep1-complete')).toBeInTheDocument(); + expect(getByTestId('horizontalStep2-complete')).toBeInTheDocument(); + expect(getByTestId('horizontalStep3-current')).toBeInTheDocument(); + expect(getByTestId('horizontalStep4-incomplete')).toBeInTheDocument(); + expect(getByTestId('authStep')).toHaveAttribute('style', 'display: none;'); + expect(getByTestId('createStep')).toHaveAttribute('style', 'display: none;'); + expect(getByTestId('getStep')).toHaveAttribute('style', 'display: block;'); + expect(getByTestId('updateStep')).toHaveAttribute('style', 'display: none;'); + await act(async () => { + userEvent.click(getByTestId('casesWebhookNext')); }); - expect(getByTestId('webhookViewHeadersSwitch')).toHaveAttribute('aria-checked', 'false'); - expect(queryByTestId('webhookHeadersKeyInput')).not.toBeInTheDocument(); - expect(queryByTestId('webhookHeadersValueInput')).not.toBeInTheDocument(); + expect(getByTestId('horizontalStep1-complete')).toBeInTheDocument(); + expect(getByTestId('horizontalStep2-complete')).toBeInTheDocument(); + expect(getByTestId('horizontalStep3-complete')).toBeInTheDocument(); + expect(getByTestId('horizontalStep4-current')).toBeInTheDocument(); + expect(getByTestId('authStep')).toHaveAttribute('style', 'display: none;'); + expect(getByTestId('createStep')).toHaveAttribute('style', 'display: none;'); + expect(getByTestId('getStep')).toHaveAttribute('style', 'display: none;'); + expect(getByTestId('updateStep')).toHaveAttribute('style', 'display: block;'); + expect(queryByTestId('casesWebhookNext')).not.toBeInTheDocument(); }); test('Step 1 is properly validated', async () => { + const incompleteActionConnector = { + ...actionConnector, + secrets: { + user: '', + password: '', + }, + }; const { getByTestId } = render( - + { ); await waitForComponentToUpdate(); - expect(getByTestId('authStep')).toHaveAttribute('style', 'display: block;'); - expect(getByTestId('createStep')).toHaveAttribute('style', 'display: none;'); - expect(getByTestId('getStep')).toHaveAttribute('style', 'display: none;'); - expect(getByTestId('updateStep')).toHaveAttribute('style', 'display: none;'); expect(getByTestId('horizontalStep1-current')).toBeInTheDocument(); await act(async () => { - await userEvent.type(getByTestId('webhookUserInput'), `{selectall}{backspace}`); - await userEvent.type(getByTestId('webhookPasswordInput'), `{selectall}{backspace}`); userEvent.click(getByTestId('casesWebhookNext')); }); + await waitForComponentToUpdate(); expect(getByTestId('horizontalStep1-danger')).toBeInTheDocument(); - expect(getByTestId('horizontalStep2-incomplete')).toBeInTheDocument(); - await completeStep1(getByTestId); + await act(async () => { + userEvent.click(getByTestId('hasAuthToggle')); + userEvent.click(getByTestId('webhookViewHeadersSwitch')); + }); + await act(async () => { + userEvent.click(getByTestId('casesWebhookNext')); + }); expect(getByTestId('horizontalStep1-complete')).toBeInTheDocument(); expect(getByTestId('horizontalStep2-current')).toBeInTheDocument(); }); - }); - describe('Step 2 Validation', () => { - test('Step 2 is properly validated when url field is missing', async () => { + test('Step 2 is properly validated', async () => { const incompleteActionConnector = { ...actionConnector, config: { ...actionConnector.config, - createIncidentUrl: '', + createIncidentUrl: undefined, }, }; const { getByText, getByTestId } = render( @@ -183,17 +234,14 @@ describe('CasesWebhookActionConnectorFields renders', () => { ); await waitForComponentToUpdate(); expect(getByTestId('horizontalStep2-incomplete')).toBeInTheDocument(); - await completeStep1(getByTestId); - expect(getByTestId('authStep')).toHaveAttribute('style', 'display: none;'); - expect(getByTestId('createStep')).toHaveAttribute('style', 'display: block;'); - expect(getByTestId('getStep')).toHaveAttribute('style', 'display: none;'); - expect(getByTestId('updateStep')).toHaveAttribute('style', 'display: none;'); + await act(async () => { + userEvent.click(getByTestId('casesWebhookNext')); + }); await act(async () => { userEvent.click(getByTestId('casesWebhookNext')); }); getByText(i18n.CREATE_URL_REQUIRED); expect(getByTestId('horizontalStep2-danger')).toBeInTheDocument(); - expect(getByTestId('horizontalStep3-incomplete')).toBeInTheDocument(); await act(async () => { await userEvent.type( getByTestId('webhookCreateUrlText'), @@ -214,15 +262,15 @@ describe('CasesWebhookActionConnectorFields renders', () => { expect(getByTestId('horizontalStep2-current')).toBeInTheDocument(); expect(getByTestId('horizontalStep3-incomplete')).toBeInTheDocument(); }); - test('Step 2 is properly validated when json is missing case variables', async () => { + test('Step 3 is properly validated', async () => { const incompleteActionConnector = { ...actionConnector, config: { ...actionConnector.config, - createIncidentJson: invalidJson, + getIncidentResponseExternalTitleKey: undefined, }, }; - const { getByTestId, getByText } = render( + const { getByText, getByTestId } = render( { ); await waitForComponentToUpdate(); - await completeStep1(getByTestId); - expect(getByTestId('horizontalStep2-current')).toBeInTheDocument(); + expect(getByTestId('horizontalStep2-incomplete')).toBeInTheDocument(); await act(async () => { userEvent.click(getByTestId('casesWebhookNext')); }); - expect(getByTestId('horizontalStep2-danger')).toBeInTheDocument(); - expect( - getByText(i18n.MISSING_VARIABLES(['{{{case.title}}}', '{{{case.description}}}'])) - ).toBeInTheDocument(); + await act(async () => { + userEvent.click(getByTestId('casesWebhookNext')); + }); + await act(async () => { + userEvent.click(getByTestId('casesWebhookNext')); + }); + getByText(i18n.GET_RESPONSE_EXTERNAL_TITLE_KEY_REQUIRED); + expect(getByTestId('horizontalStep3-danger')).toBeInTheDocument(); + await act(async () => { + await userEvent.type( + getByTestId('getIncidentResponseExternalTitleKeyText'), + `{selectall}{backspace}${config.getIncidentResponseExternalTitleKey}`, + { + delay: 10, + } + ); + }); + await act(async () => { + userEvent.click(getByTestId('casesWebhookNext')); + }); + expect(getByTestId('horizontalStep3-complete')).toBeInTheDocument(); + expect(getByTestId('horizontalStep4-current')).toBeInTheDocument(); + await act(async () => { + userEvent.click(getByTestId('horizontalStep3-complete')); + }); + expect(getByTestId('horizontalStep3-current')).toBeInTheDocument(); + expect(getByTestId('horizontalStep4-incomplete')).toBeInTheDocument(); }); + + // step 4 is not validated like the others since it is the last step + // this validation is tested in the main validation section }); - describe.skip('Validation', () => { + describe('Validation', () => { const onSubmit = jest.fn(); beforeEach(() => { @@ -255,6 +328,30 @@ describe('CasesWebhookActionConnectorFields renders', () => { ['webhookCreateUrlText', 'not-valid'], ['webhookUserInput', ''], ['webhookPasswordInput', ''], + ['incidentViewUrlInput', 'https://missingexternalid.com'], + ['createIncidentResponseKeyText', ''], + ['getIncidentUrlInput', 'https://missingexternalid.com'], + ['getIncidentResponseExternalTitleKeyText', ''], + ['getIncidentResponseCreatedDateKeyText', ''], + ['getIncidentResponseUpdatedDateKeyText', ''], + ['updateIncidentUrlInput', 'https://missingexternalid.com'], + ['createCommentUrlInput', 'https://missingexternalid.com'], + ]; + + const mustacheTests: Array<[string, string, string[]]> = [ + ['createIncidentJson', invalidJsonTitle, ['{{{case.title}}}']], + ['createIncidentJson', invalidJsonBoth, ['{{{case.title}}}', '{{{case.description}}}']], + ['updateIncidentJson', invalidJsonTitle, ['{{{case.title}}}']], + ['updateIncidentJson', invalidJsonBoth, ['{{{case.title}}}', '{{{case.description}}}']], + ['createCommentJson', invalidJsonBoth, ['{{{case.comment}}}']], + ['updateIncidentUrl', 'https://missingexternalid.com', ['{{{external.system.id}}}']], + [ + 'incidentViewUrl', + 'https://missingexternalid.com', + ['{{{external.system.id}}}', '{{{external.system.title}}}'], + ], + ['getIncidentUrl', 'https://missingexternalid.com', ['{{{external.system.id}}}']], + ['createCommentUrl', 'https://missingexternalid.com', ['{{{external.system.id}}}']], ]; it('connector validation succeeds when connector config is valid', async () => { @@ -271,7 +368,6 @@ describe('CasesWebhookActionConnectorFields renders', () => { await act(async () => { userEvent.click(getByTestId('form-test-provide-submit')); }); - const { isPreconfigured, ...rest } = actionConnector; expect(onSubmit).toBeCalledWith({ @@ -418,5 +514,36 @@ describe('CasesWebhookActionConnectorFields renders', () => { expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false }); }); + + it.each(mustacheTests)( + 'validates mustache field correctly %p', + async (field, value, missingVariables) => { + const connector = { + ...actionConnector, + config: { + ...actionConnector.config, + [field]: value, + headers: [], + }, + }; + + const res = render( + + {}} + /> + + ); + + await act(async () => { + userEvent.click(res.getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false }); + expect(res.getByText(i18n.MISSING_VARIABLES(missingVariables))).toBeInTheDocument(); + } + ); }); }); From 18dd9059bcf389ebdca547df9234d887738bed55 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 12 Jul 2022 15:02:27 -0600 Subject: [PATCH 44/97] fix comment verbiage --- .../cases/public/components/connectors/translations.ts | 4 ++-- .../components/text_area_with_message_variables.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/cases/public/components/connectors/translations.ts b/x-pack/plugins/cases/public/components/connectors/translations.ts index a97a8c06866ad0..2a66acf62b780f 100644 --- a/x-pack/plugins/cases/public/components/connectors/translations.ts +++ b/x-pack/plugins/cases/public/components/connectors/translations.ts @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; export const CREATE_COMMENT_WARNING_TITLE = i18n.translate( 'xpack.cases.connectors.card.createCommentWarningTitle', { - defaultMessage: 'Incomplete connector', + defaultMessage: 'Unable to share case comments', } ); @@ -18,5 +18,5 @@ export const CREATE_COMMENT_WARNING_DESC = (connectorName: string) => i18n.translate('xpack.cases.connectors.card.createCommentWarningDesc', { values: { connectorName }, defaultMessage: - 'In order to update comments in external service, the {connectorName} connector needs to be updated with Create Comment URL and JSON', + 'Configure the Create Comment URL and Create Comment Objects fields for the {connectorName} connector to share comments externally.', }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx index cacd098aa7ecfb..528e19981e3ebd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx @@ -15,7 +15,7 @@ import { templateActionVariable } from '../lib'; const CREATE_COMMENT_WARNING_TITLE = i18n.translate( 'xpack.triggersActionsUI.components.textAreaWithMessageVariable.createCommentWarningTitle', { - defaultMessage: 'Incomplete connector', + defaultMessage: 'Unable to share case comments', } ); @@ -23,7 +23,7 @@ const CREATE_COMMENT_WARNING_DESC = i18n.translate( 'xpack.triggersActionsUI.components.textAreaWithMessageVariable.createCommentWarningDesc', { defaultMessage: - 'In order to update comments in external service, the connector needs to be updated with Create Comment URL and JSON', + 'Configure the Create Comment URL and Create Comment Objects fields for the connector to share comments externally.', } ); interface Props { From eb15ea47aad5df36a27e28c56a66dbff84769a0e Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 12 Jul 2022 15:27:28 -0600 Subject: [PATCH 45/97] api tests --- .../cases_webhook/api.test.ts | 249 ++++++++++++++++++ .../builtin_action_types/cases_webhook/api.ts | 4 +- .../cases_webhook/mock.ts | 85 ++++++ .../cases_webhook/service.ts | 14 +- .../cases_webhook/types.ts | 4 +- 5 files changed, 345 insertions(+), 11 deletions(-) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/cases_webhook/api.test.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/cases_webhook/mock.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/api.test.ts new file mode 100644 index 00000000000000..824726bbdd5d70 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/api.test.ts @@ -0,0 +1,249 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Logger } from '@kbn/core/server'; +import { externalServiceMock, apiParams } from './mock'; +import { ExternalService } from './types'; +import { api } from './api'; +let mockedLogger: jest.Mocked; + +describe('api', () => { + let externalService: jest.Mocked; + + beforeEach(() => { + externalService = externalServiceMock.create(); + }); + + describe('create incident - cases', () => { + test('it creates an incident', async () => { + const params = { ...apiParams, externalId: null }; + const res = await api.pushToService({ + externalService, + params, + logger: mockedLogger, + }); + + expect(res).toEqual({ + id: 'incident-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, + params, + logger: mockedLogger, + }); + + expect(res).toEqual({ + id: 'incident-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, incident: { ...apiParams.incident, externalId: null } }; + await api.pushToService({ externalService, params, logger: mockedLogger }); + + expect(externalService.createIncident).toHaveBeenCalledWith({ + incident: { + labels: ['kibana', 'elastic'], + description: 'Incident description', + summary: 'Incident title', + }, + }); + expect(externalService.updateIncident).not.toHaveBeenCalled(); + }); + + test('it calls createIncident correctly without mapping', async () => { + const params = { ...apiParams, incident: { ...apiParams.incident, externalId: null } }; + await api.pushToService({ externalService, params, logger: mockedLogger }); + + expect(externalService.createIncident).toHaveBeenCalledWith({ + incident: { + description: 'Incident description', + summary: 'Incident title', + labels: ['kibana', 'elastic'], + }, + }); + expect(externalService.updateIncident).not.toHaveBeenCalled(); + }); + + test('it calls createComment correctly', async () => { + const params = { ...apiParams, incident: { ...apiParams.incident, externalId: null } }; + await api.pushToService({ externalService, params, logger: mockedLogger }); + expect(externalService.createComment).toHaveBeenCalledTimes(2); + expect(externalService.createComment).toHaveBeenNthCalledWith(1, { + incidentId: 'incident-1', + comment: { + commentId: 'case-comment-1', + comment: 'A comment', + }, + }); + + expect(externalService.createComment).toHaveBeenNthCalledWith(2, { + incidentId: 'incident-1', + comment: { + commentId: 'case-comment-2', + comment: 'Another comment', + }, + }); + }); + + test('it calls createComment correctly without mapping', async () => { + const params = { ...apiParams, incident: { ...apiParams.incident, externalId: null } }; + await api.pushToService({ externalService, params, logger: mockedLogger }); + expect(externalService.createComment).toHaveBeenCalledTimes(2); + expect(externalService.createComment).toHaveBeenNthCalledWith(1, { + incidentId: 'incident-1', + comment: { + commentId: 'case-comment-1', + comment: 'A comment', + }, + }); + + expect(externalService.createComment).toHaveBeenNthCalledWith(2, { + incidentId: 'incident-1', + comment: { + commentId: 'case-comment-2', + comment: 'Another comment', + }, + }); + }); + }); + + describe('update incident', () => { + test('it updates an incident', async () => { + const res = await api.pushToService({ + externalService, + params: apiParams, + logger: mockedLogger, + }); + + expect(res).toEqual({ + id: 'incident-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 updates an incident without comments', async () => { + const params = { ...apiParams, comments: [] }; + const res = await api.pushToService({ + externalService, + params, + logger: mockedLogger, + }); + + expect(res).toEqual({ + id: 'incident-1', + title: 'CK-1', + 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, params, logger: mockedLogger }); + + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + labels: ['kibana', 'elastic'], + description: 'Incident description', + summary: 'Incident title', + }, + }); + expect(externalService.createIncident).not.toHaveBeenCalled(); + }); + + test('it calls updateIncident correctly without mapping', async () => { + const params = { ...apiParams }; + await api.pushToService({ externalService, params, logger: mockedLogger }); + + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + description: 'Incident description', + summary: 'Incident title', + labels: ['kibana', 'elastic'], + }, + }); + expect(externalService.createIncident).not.toHaveBeenCalled(); + }); + + test('it calls createComment correctly', async () => { + const params = { ...apiParams }; + await api.pushToService({ externalService, params, logger: mockedLogger }); + expect(externalService.createComment).toHaveBeenCalledTimes(2); + expect(externalService.createComment).toHaveBeenNthCalledWith(1, { + incidentId: 'incident-1', + comment: { + commentId: 'case-comment-1', + comment: 'A comment', + }, + }); + + expect(externalService.createComment).toHaveBeenNthCalledWith(2, { + incidentId: 'incident-1', + comment: { + commentId: 'case-comment-2', + comment: 'Another comment', + }, + }); + }); + + test('it calls createComment correctly without mapping', async () => { + const params = { ...apiParams }; + await api.pushToService({ externalService, params, logger: mockedLogger }); + expect(externalService.createComment).toHaveBeenCalledTimes(2); + expect(externalService.createComment).toHaveBeenNthCalledWith(1, { + incidentId: 'incident-1', + comment: { + commentId: 'case-comment-1', + comment: 'A comment', + }, + }); + + expect(externalService.createComment).toHaveBeenNthCalledWith(2, { + incidentId: 'incident-1', + comment: { + commentId: 'case-comment-2', + comment: 'Another comment', + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/api.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/api.ts index bae26a5a213139..059f2c4fd13ae6 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/api.ts @@ -24,7 +24,7 @@ const pushToServiceHandler = async ({ if (externalId != null) { res = await externalService.updateIncident({ - externalId, + incidentId: externalId, incident, }); } else { @@ -40,7 +40,7 @@ const pushToServiceHandler = async ({ continue; } await externalService.createComment({ - externalId: res.id, + incidentId: res.id, comment: currentComment, }); res.comments = [ diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/mock.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/mock.ts new file mode 100644 index 00000000000000..0fa28a16f29e04 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/mock.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ExternalService, ExecutorSubActionPushParams, PushToServiceApiParams } from './types'; + +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({ + commentId: 'case-comment-1', + pushedDate: '2020-04-27T10:59:46.202Z', + externalCommentId: '1', + }) + ); + + service.createComment.mockImplementationOnce(() => + Promise.resolve({ + commentId: 'case-comment-2', + pushedDate: '2020-04-27T10:59:46.202Z', + externalCommentId: '2', + }) + ); + + return service; +}; + +export const externalServiceMock = { + create: createMock, +}; + +const executorParams: ExecutorSubActionPushParams = { + incident: { + externalId: 'incident-3', + summary: 'Incident title', + description: 'Incident description', + labels: ['kibana', 'elastic'], + }, + comments: [ + { + commentId: 'case-comment-1', + comment: 'A comment', + }, + { + commentId: 'case-comment-2', + comment: 'Another comment', + }, + ], +}; + +export const apiParams: PushToServiceApiParams = { + ...executorParams, +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts index 83168658e5150b..9b7b390c136e01 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts @@ -164,7 +164,7 @@ export const createExternalService = ( }; const updateIncident = async ({ - externalId, + incidentId, incident, }: UpdateIncidentParams): Promise => { try { @@ -175,7 +175,7 @@ export const createExternalService = ( url: renderMustacheStringNoEscape(updateIncidentUrl, { external: { system: { - id: externalId, + id: incidentId, }, }, }), @@ -195,15 +195,15 @@ export const createExternalService = ( res, }); - const updatedIncident = await getIncident(externalId as string); + const updatedIncident = await getIncident(incidentId as string); return { - id: externalId, + id: incidentId, title: updatedIncident.title, url: renderMustacheStringNoEscape(incidentViewUrl, { external: { system: { - id: externalId, + id: incidentId, title: updatedIncident.title, }, }, @@ -215,7 +215,7 @@ export const createExternalService = ( } }; - const createComment = async ({ externalId, comment }: CreateCommentParams): Promise => { + const createComment = async ({ incidentId, comment }: CreateCommentParams): Promise => { try { if (!createCommentUrl || !createCommentJson) { return {}; @@ -226,7 +226,7 @@ export const createExternalService = ( url: renderMustacheStringNoEscape(createCommentUrl, { external: { system: { - id: externalId, + id: incidentId, }, }, }), diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts index b205fbeca6e434..89eb915ee83638 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts @@ -69,7 +69,7 @@ export interface CreateIncidentParams { incident: Incident; } export interface UpdateIncidentParams { - externalId: string; + incidentId: string; incident: Partial; } export interface SimpleComment { @@ -78,7 +78,7 @@ export interface SimpleComment { } export interface CreateCommentParams { - externalId: string; + incidentId: string; comment: SimpleComment; } From c35d2c26bd74e1ae93559942c2fd7b690355c829 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 13 Jul 2022 13:12:20 -0600 Subject: [PATCH 46/97] action service tests --- .../builtin_action_types/cases_webhook/api.ts | 7 +- .../cases_webhook/service.test.ts | 490 ++++++++++++++++++ .../cases_webhook/service.ts | 16 +- .../cases_webhook/utils.ts | 19 +- .../cases_webhook/webhook_connectors.test.tsx | 2 +- 5 files changed, 518 insertions(+), 16 deletions(-) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/api.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/api.ts index 059f2c4fd13ae6..109c1afa920b67 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/api.ts @@ -16,9 +16,10 @@ const pushToServiceHandler = async ({ externalService, params, }: PushToServiceApiHandlerArgs): Promise => { - // const { comments } = params; - const { externalId, ...rest } = params.incident; - const { comments } = params; + const { + incident: { externalId, ...rest }, + comments, + } = params; const incident: Incident = rest; let res: PushToServiceResponse; diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts new file mode 100644 index 00000000000000..5148becbec2253 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts @@ -0,0 +1,490 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import axios, { AxiosError, AxiosResponse } from 'axios'; + +import { createExternalService } from './service'; +import { request, createAxiosResponse } from '../lib/axios_utils'; +import { CasesWebhookMethods, CasesWebhookPublicConfigurationType, ExternalService } from './types'; +import { Logger } from '@kbn/core/server'; +import { loggingSystemMock } from '@kbn/core/server/mocks'; +import { actionsConfigMock } from '../../actions_config.mock'; +const logger = loggingSystemMock.create().get() as jest.Mocked; + +jest.mock('../lib/axios_utils', () => { + const originalUtils = jest.requireActual('../lib/axios_utils'); + return { + ...originalUtils, + request: jest.fn(), + }; +}); + +axios.create = jest.fn(() => axios); +const requestMock = request as jest.Mock; +const configurationUtilities = actionsConfigMock.create(); + +const config: CasesWebhookPublicConfigurationType = { + createCommentJson: '{"body":{{{case.comment}}}}', + createCommentMethod: CasesWebhookMethods.POST, + createCommentUrl: + 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}/comment', + createIncidentJson: + '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"labels":{{{case.tags}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', + createIncidentMethod: CasesWebhookMethods.POST, + createIncidentResponseKey: 'id', + createIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue', + getIncidentResponseCreatedDateKey: 'fields.created', + getIncidentResponseExternalTitleKey: 'key', + getIncidentResponseUpdatedDateKey: 'fields.updated', + hasAuth: true, + headers: { ['content-type']: 'application/json' }, + incidentViewUrl: 'https://siem-kibana.atlassian.net/browse/{{{external.system.title}}}', + getIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}', + updateIncidentJson: + '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"labels":{{{case.tags}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', + updateIncidentMethod: CasesWebhookMethods.PUT, + updateIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}', +}; +const secrets = { + user: 'user', + password: 'pass', +}; +const actionId = '1234'; +describe('Cases webhook service', () => { + let service: ExternalService; + + beforeAll(() => { + service = createExternalService( + actionId, + { + config, + secrets, + }, + logger, + configurationUtilities + ); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('createExternalService', () => { + const requiredUrls = [ + 'createIncidentUrl', + 'incidentViewUrl', + 'getIncidentUrl', + 'updateIncidentUrl', + ]; + test.each(requiredUrls)('throws without url %p', (url) => { + expect(() => + createExternalService( + actionId, + { + config: { ...config, [url]: '' }, + secrets, + }, + logger, + configurationUtilities + ) + ).toThrow(); + }); + + test('throws if hasAuth and no user/pass', () => { + expect(() => + createExternalService( + actionId, + { + config, + secrets: { user: '', password: '' }, + }, + logger, + configurationUtilities + ) + ).toThrow(); + }); + + test('does not throw if hasAuth=false and no user/pass', () => { + expect(() => + createExternalService( + actionId, + { + config: { ...config, hasAuth: false }, + secrets: { user: '', password: '' }, + }, + logger, + configurationUtilities + ) + ).not.toThrow(); + }); + }); + + describe('getIncident', () => { + const axiosRes = { + data: { + id: '1', + key: 'CK-1', + fields: { + summary: 'title', + description: 'description', + created: '2021-10-20T19:41:02.754+0300', + updated: '2021-10-20T19:41:02.754+0300', + }, + }, + }; + + test('it returns the incident correctly', async () => { + requestMock.mockImplementation(() => createAxiosResponse(axiosRes)); + const res = await service.getIncident('1'); + expect(res).toEqual({ + id: '1', + title: 'CK-1', + created: '2021-10-20T19:41:02.754+0300', + updated: '2021-10-20T19:41:02.754+0300', + }); + }); + + test('it should call request with correct arguments', async () => { + requestMock.mockImplementation(() => createAxiosResponse(axiosRes)); + + await service.getIncident('1'); + expect(requestMock).toHaveBeenCalledWith({ + axios, + url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1', + logger, + configurationUtilities, + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementation(() => { + const error: AxiosError = new Error('An error has occurred') as AxiosError; + error.response = { statusText: 'Required field' } as AxiosResponse; + throw error; + }); + await expect(service.getIncident('1')).rejects.toThrow( + '[Action][Webhook - Case Management]: Unable to get incident. Error: An error has occurred. Reason: Required field' + ); + }); + + test('it should throw if the request is not a JSON', async () => { + requestMock.mockImplementation(() => + createAxiosResponse({ ...axiosRes, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.getIncident('1')).rejects.toThrow( + '[Action][Webhook - Case Management]: Unable to get incident. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json' + ); + }); + + test('it should throw if the required attributes are not there', async () => { + requestMock.mockImplementation(() => + createAxiosResponse({ data: { fields: { notRequired: 'test' } } }) + ); + + await expect(service.getIncident('1')).rejects.toThrow( + '[Action][Webhook - Case Management]: Unable to get incident. Error: Response is missing the expected fields: fields.created, key, fields.updated' + ); + }); + }); + + describe('createIncident', () => { + const incident = { + incident: { + summary: 'title', + description: 'desc', + labels: ['hello', 'world'], + issueType: '10006', + priority: 'High', + parent: 'RJ-107', + }, + }; + + test('it creates the incident correctly', async () => { + requestMock.mockImplementationOnce(() => + createAxiosResponse({ + data: { id: '1', key: 'CK-1', fields: { summary: 'title', description: 'description' } }, + }) + ); + + requestMock.mockImplementationOnce(() => + createAxiosResponse({ + data: { + id: '1', + key: 'CK-1', + fields: { created: '2020-04-27T10:59:46.202Z', updated: '2020-04-27T10:59:46.202Z' }, + }, + }) + ); + + const res = await service.createIncident(incident); + + expect(requestMock.mock.calls[0][0].data).toEqual( + `{"fields":{"summary":"title","description":"desc","labels":["hello","world"],"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}` + ); + + 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.mockImplementationOnce(() => + createAxiosResponse({ + data: { + id: '1', + key: 'CK-1', + fields: { created: '2020-04-27T10:59:46.202Z' }, + }, + }) + ); + + requestMock.mockImplementationOnce(() => + createAxiosResponse({ + data: { + id: '1', + key: 'CK-1', + fields: { created: '2020-04-27T10:59:46.202Z', updated: '2020-04-27T10:59:46.202Z' }, + }, + }) + ); + + await service.createIncident(incident); + + expect(requestMock.mock.calls[0][0]).toEqual({ + axios, + url: 'https://siem-kibana.atlassian.net/rest/api/2/issue', + logger, + method: CasesWebhookMethods.POST, + configurationUtilities, + data: `{"fields":{"summary":"title","description":"desc","labels":["hello","world"],"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}`, + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementation(() => { + const error: AxiosError = new Error('An error has occurred') as AxiosError; + error.response = { statusText: 'Required field' } as AxiosResponse; + throw error; + }); + + await expect(service.createIncident(incident)).rejects.toThrow( + '[Action][Webhook - Case Management]: Unable to create incident. Error: An error has occurred. Reason: Required field' + ); + }); + + test('it should throw if the request is not a JSON', async () => { + requestMock.mockImplementation(() => + createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.createIncident(incident)).rejects.toThrow( + '[Action][Webhook - Case Management]: Unable to create incident. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json.' + ); + }); + + test('it should throw if the required attributes are not there', async () => { + requestMock.mockImplementation(() => createAxiosResponse({ data: { notRequired: 'test' } })); + + await expect(service.createIncident(incident)).rejects.toThrow( + '[Action][Webhook - Case Management]: Unable to create incident. Error: Response is missing the expected field: id.' + ); + }); + }); + + describe('updateIncident', () => { + const incident = { + incidentId: '1', + incident: { + summary: 'title', + description: 'desc', + labels: ['hello', 'world'], + }, + }; + + test('it updates the incident correctly', async () => { + requestMock.mockImplementation(() => + createAxiosResponse({ + data: { + id: '1', + key: 'CK-1', + fields: { created: '2020-04-27T10:59:46.202Z', updated: '2020-04-27T10:59:46.202Z' }, + }, + }) + ); + + const res = await service.updateIncident(incident); + + 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(() => + createAxiosResponse({ + data: { + id: '1', + key: 'CK-1', + fields: { created: '2020-04-27T10:59:46.202Z', updated: '2020-04-27T10:59:46.202Z' }, + }, + }) + ); + + await service.updateIncident(incident); + + expect(requestMock.mock.calls[0][0]).toEqual({ + axios, + logger, + method: CasesWebhookMethods.PUT, + configurationUtilities, + url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1', + data: JSON.stringify({ + fields: { + summary: 'title', + description: 'desc', + labels: ['hello', 'world'], + project: { key: 'ROC' }, + issuetype: { id: '10024' }, + }, + }), + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementation(() => { + const error: AxiosError = new Error('An error has occurred') as AxiosError; + error.response = { statusText: 'Required field' } as AxiosResponse; + throw error; + }); + + await expect(service.updateIncident(incident)).rejects.toThrow( + '[Action][Webhook - Case Management]: Unable to update incident with id 1. Error: An error has occurred. Reason: Required field' + ); + }); + + test('it should throw if the request is not a JSON', async () => { + requestMock.mockImplementation(() => + createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.updateIncident(incident)).rejects.toThrow( + '[Action][Webhook - Case Management]: Unable to update incident with id 1. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json.' + ); + }); + }); + + describe('createComment', () => { + const commentReq = { + incidentId: '1', + comment: { + comment: 'comment', + commentId: 'comment-1', + }, + }; + test('it creates the comment correctly', async () => { + requestMock.mockImplementation(() => + createAxiosResponse({ + data: { + id: '1', + key: 'CK-1', + created: '2020-04-27T10:59:46.202Z', + }, + }) + ); + + const res = await service.createComment(commentReq); + + expect(res).toEqual({ + id: '1', + key: 'CK-1', + created: '2020-04-27T10:59:46.202Z', + }); + }); + + test('it should call request with correct arguments', async () => { + requestMock.mockImplementation(() => + createAxiosResponse({ + data: { + id: '1', + key: 'CK-1', + created: '2020-04-27T10:59:46.202Z', + }, + }) + ); + + await service.createComment(commentReq); + + expect(requestMock).toHaveBeenCalledWith({ + axios, + logger, + method: CasesWebhookMethods.POST, + configurationUtilities, + url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1/comment', + data: `{"body":"comment"}`, + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementation(() => { + const error: AxiosError = new Error('An error has occurred') as AxiosError; + error.response = { statusText: 'Required field' } as AxiosResponse; + throw error; + }); + + await expect(service.createComment(commentReq)).rejects.toThrow( + '[Action][Webhook - Case Management]: Unable to create comment at incident with id 1. Error: An error has occurred. Reason: Required field' + ); + }); + + test('it should throw if the request is not a JSON', async () => { + requestMock.mockImplementation(() => + createAxiosResponse({ data: { id: '1' }, headers: { ['content-type']: 'text/html' } }) + ); + + await expect(service.createComment(commentReq)).rejects.toThrow( + '[Action][Webhook - Case Management]: Unable to create comment at incident with id 1. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json.' + ); + }); + + test('it fails silently if createCommentUrl is missing', async () => { + service = createExternalService( + actionId, + { + config: { ...config, createCommentUrl: '' }, + secrets, + }, + logger, + configurationUtilities + ); + const res = await service.createComment(commentReq); + expect(requestMock).not.toHaveBeenCalled(); + expect(res).toEqual({}); + }); + + test('it fails silently if createCommentJson is missing', async () => { + service = createExternalService( + actionId, + { + config: { ...config, createCommentJson: '' }, + secrets, + }, + logger, + configurationUtilities + ); + const res = await service.createComment(commentReq); + expect(requestMock).not.toHaveBeenCalled(); + expect(res).toEqual({}); + }); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts index 9b7b390c136e01..4708e988e3c569 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts @@ -59,7 +59,13 @@ export const createExternalService = ( updateIncidentUrl, } = config as CasesWebhookPublicConfigurationType; const { password, user } = secrets as CasesWebhookSecretConfigurationType; - if (!getIncidentUrl || !password || !user) { + if ( + !getIncidentUrl || + !createIncidentUrlConfig || + !incidentViewUrl || + !updateIncidentUrl || + (hasAuth && (!password || !user)) + ) { throw Error(`[Action]${i18n.NAME}: Wrong configuration.`); } @@ -116,7 +122,6 @@ export const createExternalService = ( incident, }: CreateIncidentParams): Promise => { const { labels, summary, description } = incident; - try { const res: AxiosResponse = await request({ axios: axiosInstance, @@ -211,7 +216,7 @@ export const createExternalService = ( pushedDate: getPushedDate(updatedIncident.updated), }; } catch (error) { - throw createServiceError(error, 'Unable to update incident'); + throw createServiceError(error, `Unable to update incident with id ${incidentId}`); } }; @@ -241,10 +246,9 @@ export const createExternalService = ( throwIfResponseIsNotValidSpecial({ res, }); - - return res; + return res.data; } catch (error) { - throw createServiceError(error, 'Unable to post comment'); + throw createServiceError(error, `Unable to create comment at incident with id ${incidentId}`); } }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts index aa5f6dcf23e163..69b3ad44a87ab5 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts @@ -14,7 +14,7 @@ export const createServiceError = (error: AxiosError, message: string) => new Error( getErrorMessage( i18n.NAME, - `${message}. Error: ${error.message} ${ + `${message}. Error: ${error.message}. ${ error.response?.statusText != null ? `Reason: ${error.response?.statusText}` : '' }` ) @@ -91,20 +91,27 @@ export const throwIfResponseIsNotValidSpecial = ({ } if (requiredAttributesToBeInTheResponse.length > 0) { - const requiredAttributesError = (attr: string) => - new Error(`Response is missing at the expected field: ${attr}`); + const requiredAttributesError = (attrs: string[]) => + new Error( + `Response is missing the expected ${attrs.length > 1 ? `fields` : `field`}: ${attrs.join( + ', ' + )}` + ); + const errorAttributes: string[] = []; /** * If the response is an array and requiredAttributesToBeInTheResponse - * are not empty then we thrown an error assuming that the consumer - * expects an object response and not an array. + * are not empty then we throw an error if we are missing data for the given attributes */ requiredAttributesToBeInTheResponse.forEach((attr) => { // Check only for undefined as null is a valid value if (getObjectValueByKey(data, attr) === undefined) { - throw requiredAttributesError(attr); + errorAttributes.push(attr); } }); + if (errorAttributes.length) { + throw requiredAttributesError(errorAttributes); + } } }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx index 70c6fa5f5f2b56..db1f78ea1dd2ea 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx @@ -38,7 +38,7 @@ const config = { createIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue', getIncidentResponseCreatedDateKey: 'fields.created', getIncidentResponseExternalTitleKey: 'key', - getIncidentResponseUpdatedDateKey: 'fields.udpated', + getIncidentResponseUpdatedDateKey: 'fields.updated', hasAuth: true, headers: [{ key: 'content-type', value: 'text' }], incidentViewUrl: 'https://siem-kibana.atlassian.net/browse/{{{external.system.title}}}', From 8d144102717dd5b598e90d4d43095b2ed7dcb1b7 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 13 Jul 2022 13:43:54 -0600 Subject: [PATCH 47/97] starting api tests --- .../builtin_action_types/cases_webhook.ts | 102 ++++++++++++++++++ .../basic/tests/actions/index.ts | 1 + .../alerting_api_integration/common/config.ts | 1 + 3 files changed, 104 insertions(+) create mode 100644 x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/cases_webhook.ts diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/cases_webhook.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/cases_webhook.ts new file mode 100644 index 00000000000000..b8fe069ef4b627 --- /dev/null +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/cases_webhook.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import http from 'http'; +import getPort from 'get-port'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { getWebhookServer } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; + +// eslint-disable-next-line import/no-default-export +export default function casesWebhookTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + const config = { + createCommentJson: '{"body":{{{case.comment}}}}', + createCommentMethod: 'post', + createCommentUrl: + 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}/comment', + createIncidentJson: + '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"labels":{{{case.tags}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', + createIncidentMethod: 'post', + createIncidentResponseKey: 'id', + createIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue', + getIncidentResponseCreatedDateKey: 'fields.created', + getIncidentResponseExternalTitleKey: 'key', + getIncidentResponseUpdatedDateKey: 'fields.updated', + hasAuth: true, + headers: { ['content-type']: 'application/json' }, + incidentViewUrl: 'https://siem-kibana.atlassian.net/browse/{{{external.system.title}}}', + getIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}', + updateIncidentJson: + '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"labels":{{{case.tags}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', + updateIncidentMethod: 'put', + updateIncidentUrl: + 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}', + }; + + const mockCasesWebhook = { + config, + secrets: { + user: 'user', + password: 'pass', + }, + params: { + incident: { + summary: 'a title', + description: 'a description', + labels: ['kibana'], + }, + comments: [ + { + commentId: '456', + comment: 'first comment', + }, + ], + }, + }; + describe('casesWebhook', () => { + let webhookSimulatorURL: string = ''; + let webhookServer: http.Server; + // need to wait for kibanaServer to settle ... + before(async () => { + webhookServer = await getWebhookServer(); + const availablePort = await getPort({ port: 9000 }); + webhookServer.listen(availablePort); + webhookSimulatorURL = `http://localhost:${availablePort}`; + }); + + it('should return 403 when creating a cases webhook action', async () => { + await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A cases webhook action', + actionTypeId: '.cases-webhook', + config: { + ...config, + createCommentUrl: `${webhookSimulatorURL}/{{{external.system.id}}}`, + createIncidentUrl: webhookSimulatorURL, + incidentViewUrl: `${webhookSimulatorURL}/{{{external.system.id}}}`, + getIncidentUrl: `${webhookSimulatorURL}/{{{external.system.id}}}`, + updateIncidentUrl: `${webhookSimulatorURL}/{{{external.system.id}}}`, + }, + secrets: mockCasesWebhook.secrets, + }) + .expect(403, { + statusCode: 403, + error: 'Forbidden', + message: + 'Action type .cases-webhook is disabled because your basic license does not support it. Please upgrade your license.', + }); + }); + + after(() => { + webhookServer.close(); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/index.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/index.ts index 21cb0db3057bbe..8251a1b6fb3f19 100644 --- a/x-pack/test/alerting_api_integration/basic/tests/actions/index.ts +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/index.ts @@ -10,6 +10,7 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function actionsTests({ loadTestFile }: FtrProviderContext) { describe('Actions', () => { + loadTestFile(require.resolve('./builtin_action_types/cases_webhook')); loadTestFile(require.resolve('./builtin_action_types/email')); loadTestFile(require.resolve('./builtin_action_types/es_index')); loadTestFile(require.resolve('./builtin_action_types/jira')); diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 0c5f95189ae902..f0cbe467736453 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -30,6 +30,7 @@ interface CreateTestConfigOptions { // test.not-enabled is specifically not enabled const enabledActionTypes = [ + '.cases-webhook', '.email', '.index', '.pagerduty', From 1dd846e28b681d5949895f6bbff6b92f4335ab77 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 14 Jul 2022 09:08:29 -0600 Subject: [PATCH 48/97] api tests --- .../builtin_action_types/cases_webhook.ts | 36 +- .../builtin_action_types/cases_webhook.ts | 438 ++++++++++++++++++ .../group2/tests/actions/index.ts | 2 +- 3 files changed, 455 insertions(+), 21 deletions(-) create mode 100644 x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/cases_webhook.ts diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/cases_webhook.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/cases_webhook.ts index b8fe069ef4b627..f767382a779ac2 100644 --- a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/cases_webhook.ts +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/cases_webhook.ts @@ -5,15 +5,17 @@ * 2.0. */ -import http from 'http'; -import getPort from 'get-port'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { getWebhookServer } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; +import { + ExternalServiceSimulator, + getExternalServiceSimulatorPath, +} from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; // eslint-disable-next-line import/no-default-export export default function casesWebhookTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); + const kibanaServer = getService('kibanaServer'); const config = { createCommentJson: '{"body":{{{case.comment}}}}', @@ -60,14 +62,12 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { }, }; describe('casesWebhook', () => { - let webhookSimulatorURL: string = ''; - let webhookServer: http.Server; - // need to wait for kibanaServer to settle ... - before(async () => { - webhookServer = await getWebhookServer(); - const availablePort = await getPort({ port: 9000 }); - webhookServer.listen(availablePort); - webhookSimulatorURL = `http://localhost:${availablePort}`; + let casesWebhookSimulatorURL: string = ''; + before(() => { + // use jira because cases webhook works with any third party case management system + casesWebhookSimulatorURL = kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.JIRA) + ); }); it('should return 403 when creating a cases webhook action', async () => { @@ -79,11 +79,11 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { actionTypeId: '.cases-webhook', config: { ...config, - createCommentUrl: `${webhookSimulatorURL}/{{{external.system.id}}}`, - createIncidentUrl: webhookSimulatorURL, - incidentViewUrl: `${webhookSimulatorURL}/{{{external.system.id}}}`, - getIncidentUrl: `${webhookSimulatorURL}/{{{external.system.id}}}`, - updateIncidentUrl: `${webhookSimulatorURL}/{{{external.system.id}}}`, + createCommentUrl: `${casesWebhookSimulatorURL}/{{{external.system.id}}}/comments`, + createIncidentUrl: casesWebhookSimulatorURL, + incidentViewUrl: `${casesWebhookSimulatorURL}/{{{external.system.title}}}`, + getIncidentUrl: `${casesWebhookSimulatorURL}/{{{external.system.id}}}`, + updateIncidentUrl: `${casesWebhookSimulatorURL}/{{{external.system.id}}}`, }, secrets: mockCasesWebhook.secrets, }) @@ -94,9 +94,5 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { 'Action type .cases-webhook is disabled because your basic license does not support it. Please upgrade your license.', }); }); - - after(() => { - webhookServer.close(); - }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/cases_webhook.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/cases_webhook.ts new file mode 100644 index 00000000000000..00a1b3a5ef0538 --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/cases_webhook.ts @@ -0,0 +1,438 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import httpProxy from 'http-proxy'; +import expect from '@kbn/expect'; + +import { getHttpProxyServer } from '../../../../../common/lib/get_proxy_server'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; + +import { + getExternalServiceSimulatorPath, + ExternalServiceSimulator, +} from '../../../../../common/fixtures/plugins/actions_simulators/server/plugin'; + +// eslint-disable-next-line import/no-default-export +export default function casesWebhookTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const kibanaServer = getService('kibanaServer'); + const configService = getService('config'); + const config = { + createCommentJson: '{"body":{{{case.comment}}}}', + createCommentMethod: 'post', + createCommentUrl: + 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}/comment', + createIncidentJson: + '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"labels":{{{case.tags}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', + createIncidentMethod: 'post', + createIncidentResponseKey: 'id', + createIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue', + getIncidentResponseCreatedDateKey: 'fields.created', + getIncidentResponseExternalTitleKey: 'key', + getIncidentResponseUpdatedDateKey: 'fields.updated', + hasAuth: true, + headers: { ['content-type']: 'application/json' }, + incidentViewUrl: 'https://siem-kibana.atlassian.net/browse/{{{external.system.title}}}', + getIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}', + updateIncidentJson: + '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"labels":{{{case.tags}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', + updateIncidentMethod: 'put', + updateIncidentUrl: + 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}', + }; + const requiredFields = [ + 'createIncidentJson', + 'createIncidentResponseKey', + 'createIncidentUrl', + 'getIncidentResponseCreatedDateKey', + 'getIncidentResponseExternalTitleKey', + 'getIncidentResponseUpdatedDateKey', + 'incidentViewUrl', + 'getIncidentUrl', + 'updateIncidentJson', + 'updateIncidentUrl', + ]; + const secrets = { + user: 'user', + password: 'pass', + }; + const mockCasesWebhook = { + config, + secrets, + params: { + subAction: 'pushToService', + subActionParams: { + incident: { + summary: 'a title', + description: 'a description', + externalId: null, + }, + comments: [ + { + comment: 'first comment', + commentId: '456', + }, + ], + }, + }, + }; + + let casesWebhookSimulatorURL: string = ''; + let simulatorConfig: Record>; + describe('CasesWebhook', () => { + before(() => { + // use jira because cases webhook works with any third party case management system + casesWebhookSimulatorURL = kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.JIRA) + ); + + simulatorConfig = { + ...mockCasesWebhook.config, + createCommentUrl: `${casesWebhookSimulatorURL}/rest/api/2/issue/{{{external.system.id}}}/comment`, + createIncidentUrl: `${casesWebhookSimulatorURL}/rest/api/2/issue`, + incidentViewUrl: `${casesWebhookSimulatorURL}/browse/{{{external.system.title}}}`, + getIncidentUrl: `${casesWebhookSimulatorURL}/rest/api/2/issue/{{{external.system.id}}}`, + updateIncidentUrl: `${casesWebhookSimulatorURL}/rest/api/2/issue/{{{external.system.id}}}`, + }; + }); + describe('CasesWebhook - Action Creation', () => { + it('should return 200 when creating a casesWebhook action successfully', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A casesWebhook action', + connector_type_id: '.cases-webhook', + config: simulatorConfig, + secrets, + }) + .expect(200); + + expect(createdAction).to.eql({ + id: createdAction.id, + is_preconfigured: false, + is_deprecated: false, + name: 'A casesWebhook action', + connector_type_id: '.cases-webhook', + is_missing_secrets: false, + config: simulatorConfig, + }); + + const { body: fetchedAction } = await supertest + .get(`/api/actions/connector/${createdAction.id}`) + .expect(200); + + expect(fetchedAction).to.eql({ + id: fetchedAction.id, + is_preconfigured: false, + is_deprecated: false, + name: 'A casesWebhook action', + connector_type_id: '.cases-webhook', + is_missing_secrets: false, + config: simulatorConfig, + }); + }); + describe('400s for all required fields when missing', () => { + requiredFields.forEach((field) => { + it(`should respond with a 400 Bad Request when creating a casesWebhook action with no ${field}`, async () => { + const incompleteConfig = { ...simulatorConfig }; + delete incompleteConfig[field]; + await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A casesWebhook action', + connector_type_id: '.cases-webhook', + config: incompleteConfig, + secrets, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: `error validating action type config: [${field}]: expected value of type [string] but got [undefined]`, + }); + }); + }); + }); + }); + + it('should respond with a 400 Bad Request when creating a casesWebhook action with a not present in allowedHosts apiUrl', async () => { + const badUrl = 'http://casesWebhook.mynonexistent.com'; + await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A casesWebhook action', + connector_type_id: '.cases-webhook', + config: { + ...mockCasesWebhook.config, + createCommentUrl: `${badUrl}/{{{external.system.id}}}/comments`, + createIncidentUrl: badUrl, + incidentViewUrl: `${badUrl}/{{{external.system.title}}}`, + getIncidentUrl: `${badUrl}/{{{external.system.id}}}`, + updateIncidentUrl: `${badUrl}/{{{external.system.id}}}`, + }, + secrets, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type config: error configuring cases webhook action: target url "http://casesWebhook.mynonexistent.com" is not added to the Kibana config xpack.actions.allowedHosts', + }); + }); + }); + + it('should respond with a 400 Bad Request when creating a casesWebhook action without secrets when hasAuth = true', async () => { + await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A casesWebhook action', + connector_type_id: '.cases-webhook', + config: simulatorConfig, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type secrets: [user]: expected value of type [string] but got [undefined]', + }); + }); + }); + }); + + describe('CasesWebhook - Executor', () => { + let simulatedActionId: string; + let proxyServer: httpProxy | undefined; + let proxyHaveBeenCalled = false; + + before(async () => { + const { body } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A casesWebhook simulator', + connector_type_id: '.cases-webhook', + config: simulatorConfig, + secrets, + }); + simulatedActionId = body.id; + + proxyServer = await getHttpProxyServer( + kibanaServer.resolveUrl('/'), + configService.get('kbnTestServer.serverArgs'), + () => { + proxyHaveBeenCalled = true; + } + ); + }); + + describe('Validation', () => { + it('should handle failing with a simulated success without action', async () => { + await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: {}, + }) + .then((resp: any) => { + expect(Object.keys(resp.body)).to.eql(['status', 'message', 'retry', 'connector_id']); + expect(resp.body.connector_id).to.eql(simulatedActionId); + expect(resp.body.status).to.eql('error'); + }); + }); + + it('should handle failing with a simulated success without unsupported action', async () => { + await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { subAction: 'non-supported' }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + connector_id: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: [subAction]: expected value to equal [pushToService]', + }); + }); + }); + + it('should handle failing with a simulated success without subActionParams argument', async () => { + await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { subAction: 'pushToService' }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + connector_id: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: [subActionParams.incident.summary]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should handle failing with a simulated success without title', async () => { + await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockCasesWebhook.params, + subActionParams: { + incident: { + description: 'success', + }, + comments: [], + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + connector_id: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: [subActionParams.incident.summary]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should handle failing with a simulated success without commentId', async () => { + await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockCasesWebhook.params, + subActionParams: { + incident: { + ...mockCasesWebhook.params.subActionParams.incident, + description: 'success', + summary: 'success', + }, + comments: [{ comment: 'comment' }], + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + connector_id: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: [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]', + }); + }); + }); + + it('should handle failing with a simulated success without comment message', async () => { + await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockCasesWebhook.params, + subActionParams: { + incident: { + ...mockCasesWebhook.params.subActionParams.incident, + summary: 'success', + }, + comments: [{ commentId: 'success' }], + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + connector_id: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: [subActionParams.comments]: types that failed validation:\n- [subActionParams.comments.0.0.comment]: expected value of type [string] but got [undefined]\n- [subActionParams.comments.1]: expected value to equal [null]', + }); + }); + }); + + it('should handle failing with a simulated success when labels containing a space', async () => { + await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockCasesWebhook.params, + subActionParams: { + incident: { + ...mockCasesWebhook.params.subActionParams.incident, + labels: ['label with spaces'], + }, + comments: [], + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + connector_id: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: [subActionParams.incident.labels]: types that failed validation:\n- [subActionParams.incident.labels.0.0]: The label label with spaces cannot contain spaces\n- [subActionParams.incident.labels.1]: expected value to equal [null]', + }); + }); + }); + }); + + describe('Execution', () => { + it('should handle creating an incident without comments', async () => { + const { body } = await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockCasesWebhook.params, + subActionParams: { + incident: mockCasesWebhook.params.subActionParams.incident, + comments: [], + }, + }, + }) + .expect(200); + + expect(proxyHaveBeenCalled).to.equal(true); + expect(body).to.eql({ + status: 'ok', + connector_id: simulatedActionId, + data: { + id: '123', + title: 'CK-1', + pushedDate: '2020-04-27T14:17:45.490Z', + url: `${casesWebhookSimulatorURL}/browse/CK-1`, + }, + }); + }); + }); + + after(() => { + if (proxyServer) { + proxyServer.close(); + } + }); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/index.ts index 8175445b4f1c0c..922b3500266ff7 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/index.ts @@ -18,7 +18,7 @@ export default function actionsTests({ loadTestFile, getService }: FtrProviderCo after(async () => { await tearDown(getService); }); - + loadTestFile(require.resolve('./builtin_action_types/cases_webhook')); loadTestFile(require.resolve('./builtin_action_types/email')); loadTestFile(require.resolve('./builtin_action_types/es_index')); loadTestFile(require.resolve('./builtin_action_types/es_index_preconfigured')); From 36d35f4aa046e4586bdacfd40102f20feb1edfc8 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 14 Jul 2022 10:27:00 -0600 Subject: [PATCH 49/97] ues --- .../cases_api_integration/common/config.ts | 1 + .../cases_api_integration/common/lib/utils.ts | 30 ++++++++++++++++ .../tests/trial/configure/get_connectors.ts | 35 +++++++++++++++++++ .../tests/trial/configure/index.ts | 6 ++-- .../security_and_spaces/tests/trial/index.ts | 10 +++--- 5 files changed, 74 insertions(+), 8 deletions(-) diff --git a/x-pack/test/cases_api_integration/common/config.ts b/x-pack/test/cases_api_integration/common/config.ts index a20dd300a4e6ee..6d3f59f30dc75a 100644 --- a/x-pack/test/cases_api_integration/common/config.ts +++ b/x-pack/test/cases_api_integration/common/config.ts @@ -21,6 +21,7 @@ interface CreateTestConfigOptions { } const enabledActionTypes = [ + '.cases-webhook', '.email', '.index', '.jira', diff --git a/x-pack/test/cases_api_integration/common/lib/utils.ts b/x-pack/test/cases_api_integration/common/lib/utils.ts index fd2fc0ba413e01..5f68fafff85a45 100644 --- a/x-pack/test/cases_api_integration/common/lib/utils.ts +++ b/x-pack/test/cases_api_integration/common/lib/utils.ts @@ -250,6 +250,36 @@ export const getJiraConnector = () => ({ }, }); +export const getCasesWebhookConnector = () => ({ + name: 'Cases Webhook Connector', + connector_type_id: '.cases-webhook', + secrets: { + user: 'user', + password: 'pass', + }, + config: { + createCommentJson: '{"body":{{{case.comment}}}}', + createCommentMethod: 'post', + createCommentUrl: 'http://some.non.existent.com/{{{external.system.id}}}/comment', + createIncidentJson: + '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', + createIncidentMethod: 'post', + createIncidentResponseKey: 'id', + createIncidentUrl: 'http://some.non.existent.com/', + getIncidentResponseCreatedDateKey: 'fields.created', + getIncidentResponseExternalTitleKey: 'key', + getIncidentResponseUpdatedDateKey: 'fields.updated', + hasAuth: true, + headers: { [`content-type`]: 'application/json' }, + incidentViewUrl: 'http://some.non.existent.com/browse/{{{external.system.title}}}', + getIncidentUrl: 'http://some.non.existent.com/{{{external.system.id}}}', + updateIncidentJson: + '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', + updateIncidentMethod: 'put', + updateIncidentUrl: 'http://some.non.existent.com/{{{external.system.id}}}', + }, +}); + export const getMappings = () => [ { source: 'title', diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts index de72e0f343026b..fb39855e8d717e 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts @@ -40,6 +40,10 @@ export default ({ getService }: FtrProviderContext): void => { const jiraConnector = await createConnector({ supertest, req: getJiraConnector() }); const resilientConnector = await createConnector({ supertest, req: getResilientConnector() }); const sir = await createConnector({ supertest, req: getServiceNowSIRConnector() }); + // const casesWebhookConnector = await createConnector({ + // supertest, + // req: getCasesWebhookConnector(), + // }); actionsRemover.add('default', sir.id, 'action', 'actions'); actionsRemover.add('default', snConnector.id, 'action', 'actions'); @@ -47,11 +51,42 @@ export default ({ getService }: FtrProviderContext): void => { actionsRemover.add('default', emailConnector.id, 'action', 'actions'); actionsRemover.add('default', jiraConnector.id, 'action', 'actions'); actionsRemover.add('default', resilientConnector.id, 'action', 'actions'); + // actionsRemover.add('default', casesWebhookConnector.id, 'action', 'actions'); const connectors = await getCaseConnectors({ supertest }); const sortedConnectors = connectors.sort((a, b) => a.name.localeCompare(b.name)); expect(sortedConnectors).to.eql([ + // { + // id: casesWebhookConnector.id, + // actionTypeId: '.cases-webhook', + // name: 'Cases Webhook Connector', + // config: { + // createCommentJson: '{"body":{{{case.comment}}}}', + // createCommentMethod: 'post', + // createCommentUrl: 'http://some.non.existent.com/{{{external.system.id}}}/comment', + // createIncidentJson: + // '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', + // createIncidentMethod: 'post', + // createIncidentResponseKey: 'id', + // createIncidentUrl: 'http://some.non.existent.com/', + // getIncidentResponseCreatedDateKey: 'fields.created', + // getIncidentResponseExternalTitleKey: 'key', + // getIncidentResponseUpdatedDateKey: 'fields.updated', + // hasAuth: true, + // headers: { [`content-type`]: 'application/json' }, + // incidentViewUrl: 'http://some.non.existent.com/browse/{{{external.system.title}}}', + // getIncidentUrl: 'http://some.non.existent.com/{{{external.system.id}}}', + // updateIncidentJson: + // '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', + // updateIncidentMethod: 'put', + // updateIncidentUrl: 'http://some.non.existent.com/{{{external.system.id}}}', + // }, + // isPreconfigured: false, + // isDeprecated: false, + // isMissingSecrets: false, + // referencedByCount: 0, + // }, { id: jiraConnector.id, actionTypeId: '.jira', diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/configure/index.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/configure/index.ts index 0c8c3931d15774..63d4eab0b81b55 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/configure/index.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/configure/index.ts @@ -10,9 +10,9 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default ({ loadTestFile }: FtrProviderContext): void => { describe('configuration tests', function () { - loadTestFile(require.resolve('./get_configure')); + // loadTestFile(require.resolve('./get_configure')); loadTestFile(require.resolve('./get_connectors')); - loadTestFile(require.resolve('./patch_configure')); - loadTestFile(require.resolve('./post_configure')); + // loadTestFile(require.resolve('./patch_configure')); + // loadTestFile(require.resolve('./post_configure')); }); }; diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/index.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/index.ts index b1da7a9f4d06a2..c11effc7267957 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/index.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/index.ts @@ -20,16 +20,16 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => { }); // Trial - loadTestFile(require.resolve('./cases/push_case')); - loadTestFile(require.resolve('./cases/user_actions/get_all_user_actions')); + // loadTestFile(require.resolve('./cases/push_case')); + // loadTestFile(require.resolve('./cases/user_actions/get_all_user_actions')); loadTestFile(require.resolve('./configure')); // sub privileges are only available with a license above basic - loadTestFile(require.resolve('./delete_sub_privilege')); + // loadTestFile(require.resolve('./delete_sub_privilege')); // Common - loadTestFile(require.resolve('../common')); + // loadTestFile(require.resolve('../common')); // NOTE: These need to be at the end because they could delete the .kibana index and inadvertently remove the users and spaces - loadTestFile(require.resolve('../common/migrations')); + // loadTestFile(require.resolve('../common/migrations')); }); }; From 8a6d34a03e448ac9e6a30cb909ee86aeb77ac680 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 14 Jul 2022 12:22:29 -0600 Subject: [PATCH 50/97] add case connectors api tests --- .../tests/trial/configure/get_connectors.ts | 71 ++++++++++--------- .../tests/trial/configure/index.ts | 6 +- .../security_and_spaces/tests/trial/index.ts | 10 +-- .../tests/trial/configure/get_connectors.ts | 38 ++++++++++ .../tests/trial/configure/index.ts | 6 +- .../spaces_only/tests/trial/index.ts | 6 +- 6 files changed, 88 insertions(+), 49 deletions(-) diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts index fb39855e8d717e..4109913d9f4fb3 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts @@ -18,6 +18,7 @@ import { getServiceNowSIRConnector, getEmailConnector, getCaseConnectors, + getCasesWebhookConnector, } from '../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export @@ -40,10 +41,10 @@ export default ({ getService }: FtrProviderContext): void => { const jiraConnector = await createConnector({ supertest, req: getJiraConnector() }); const resilientConnector = await createConnector({ supertest, req: getResilientConnector() }); const sir = await createConnector({ supertest, req: getServiceNowSIRConnector() }); - // const casesWebhookConnector = await createConnector({ - // supertest, - // req: getCasesWebhookConnector(), - // }); + const casesWebhookConnector = await createConnector({ + supertest, + req: getCasesWebhookConnector(), + }); actionsRemover.add('default', sir.id, 'action', 'actions'); actionsRemover.add('default', snConnector.id, 'action', 'actions'); @@ -51,42 +52,42 @@ export default ({ getService }: FtrProviderContext): void => { actionsRemover.add('default', emailConnector.id, 'action', 'actions'); actionsRemover.add('default', jiraConnector.id, 'action', 'actions'); actionsRemover.add('default', resilientConnector.id, 'action', 'actions'); - // actionsRemover.add('default', casesWebhookConnector.id, 'action', 'actions'); + actionsRemover.add('default', casesWebhookConnector.id, 'action', 'actions'); const connectors = await getCaseConnectors({ supertest }); const sortedConnectors = connectors.sort((a, b) => a.name.localeCompare(b.name)); expect(sortedConnectors).to.eql([ - // { - // id: casesWebhookConnector.id, - // actionTypeId: '.cases-webhook', - // name: 'Cases Webhook Connector', - // config: { - // createCommentJson: '{"body":{{{case.comment}}}}', - // createCommentMethod: 'post', - // createCommentUrl: 'http://some.non.existent.com/{{{external.system.id}}}/comment', - // createIncidentJson: - // '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', - // createIncidentMethod: 'post', - // createIncidentResponseKey: 'id', - // createIncidentUrl: 'http://some.non.existent.com/', - // getIncidentResponseCreatedDateKey: 'fields.created', - // getIncidentResponseExternalTitleKey: 'key', - // getIncidentResponseUpdatedDateKey: 'fields.updated', - // hasAuth: true, - // headers: { [`content-type`]: 'application/json' }, - // incidentViewUrl: 'http://some.non.existent.com/browse/{{{external.system.title}}}', - // getIncidentUrl: 'http://some.non.existent.com/{{{external.system.id}}}', - // updateIncidentJson: - // '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', - // updateIncidentMethod: 'put', - // updateIncidentUrl: 'http://some.non.existent.com/{{{external.system.id}}}', - // }, - // isPreconfigured: false, - // isDeprecated: false, - // isMissingSecrets: false, - // referencedByCount: 0, - // }, + { + id: casesWebhookConnector.id, + actionTypeId: '.cases-webhook', + name: 'Cases Webhook Connector', + config: { + createCommentJson: '{"body":{{{case.comment}}}}', + createCommentMethod: 'post', + createCommentUrl: 'http://some.non.existent.com/{{{external.system.id}}}/comment', + createIncidentJson: + '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', + createIncidentMethod: 'post', + createIncidentResponseKey: 'id', + createIncidentUrl: 'http://some.non.existent.com/', + getIncidentResponseCreatedDateKey: 'fields.created', + getIncidentResponseExternalTitleKey: 'key', + getIncidentResponseUpdatedDateKey: 'fields.updated', + hasAuth: true, + headers: { [`content-type`]: 'application/json' }, + incidentViewUrl: 'http://some.non.existent.com/browse/{{{external.system.title}}}', + getIncidentUrl: 'http://some.non.existent.com/{{{external.system.id}}}', + updateIncidentJson: + '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', + updateIncidentMethod: 'put', + updateIncidentUrl: 'http://some.non.existent.com/{{{external.system.id}}}', + }, + isPreconfigured: false, + isDeprecated: false, + isMissingSecrets: false, + referencedByCount: 0, + }, { id: jiraConnector.id, actionTypeId: '.jira', diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/configure/index.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/configure/index.ts index 63d4eab0b81b55..0c8c3931d15774 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/configure/index.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/configure/index.ts @@ -10,9 +10,9 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default ({ loadTestFile }: FtrProviderContext): void => { describe('configuration tests', function () { - // loadTestFile(require.resolve('./get_configure')); + loadTestFile(require.resolve('./get_configure')); loadTestFile(require.resolve('./get_connectors')); - // loadTestFile(require.resolve('./patch_configure')); - // loadTestFile(require.resolve('./post_configure')); + loadTestFile(require.resolve('./patch_configure')); + loadTestFile(require.resolve('./post_configure')); }); }; diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/index.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/index.ts index c11effc7267957..b1da7a9f4d06a2 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/index.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/index.ts @@ -20,16 +20,16 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => { }); // Trial - // loadTestFile(require.resolve('./cases/push_case')); - // loadTestFile(require.resolve('./cases/user_actions/get_all_user_actions')); + loadTestFile(require.resolve('./cases/push_case')); + loadTestFile(require.resolve('./cases/user_actions/get_all_user_actions')); loadTestFile(require.resolve('./configure')); // sub privileges are only available with a license above basic - // loadTestFile(require.resolve('./delete_sub_privilege')); + loadTestFile(require.resolve('./delete_sub_privilege')); // Common - // loadTestFile(require.resolve('../common')); + loadTestFile(require.resolve('../common')); // NOTE: These need to be at the end because they could delete the .kibana index and inadvertently remove the users and spaces - // loadTestFile(require.resolve('../common/migrations')); + loadTestFile(require.resolve('../common/migrations')); }); }; diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/get_connectors.ts b/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/get_connectors.ts index b08b6f21390d41..c6d2165f2bf8b3 100644 --- a/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/get_connectors.ts +++ b/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/get_connectors.ts @@ -20,6 +20,7 @@ import { getCaseConnectors, getActionsSpace, getEmailConnector, + getCasesWebhookConnector, } from '../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export @@ -70,12 +71,19 @@ export default ({ getService }: FtrProviderContext): void => { auth: authSpace1, }); + const casesWebhookConnector = await createConnector({ + supertest: supertestWithoutAuth, + req: getCasesWebhookConnector(), + auth: authSpace1, + }); + actionsRemover.add(space, sir.id, 'action', 'actions'); actionsRemover.add(space, snConnector.id, 'action', 'actions'); actionsRemover.add(space, snOAuthConnector.id, 'action', 'actions'); actionsRemover.add(space, emailConnector.id, 'action', 'actions'); actionsRemover.add(space, jiraConnector.id, 'action', 'actions'); actionsRemover.add(space, resilientConnector.id, 'action', 'actions'); + actionsRemover.add(space, casesWebhookConnector.id, 'action', 'actions'); const connectors = await getCaseConnectors({ supertest: supertestWithoutAuth, @@ -84,6 +92,36 @@ export default ({ getService }: FtrProviderContext): void => { const sortedConnectors = connectors.sort((a, b) => a.name.localeCompare(b.name)); expect(sortedConnectors).to.eql([ + { + id: casesWebhookConnector.id, + actionTypeId: '.cases-webhook', + name: 'Cases Webhook Connector', + config: { + createCommentJson: '{"body":{{{case.comment}}}}', + createCommentMethod: 'post', + createCommentUrl: 'http://some.non.existent.com/{{{external.system.id}}}/comment', + createIncidentJson: + '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', + createIncidentMethod: 'post', + createIncidentResponseKey: 'id', + createIncidentUrl: 'http://some.non.existent.com/', + getIncidentResponseCreatedDateKey: 'fields.created', + getIncidentResponseExternalTitleKey: 'key', + getIncidentResponseUpdatedDateKey: 'fields.updated', + hasAuth: true, + headers: { [`content-type`]: 'application/json' }, + incidentViewUrl: 'http://some.non.existent.com/browse/{{{external.system.title}}}', + getIncidentUrl: 'http://some.non.existent.com/{{{external.system.id}}}', + updateIncidentJson: + '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', + updateIncidentMethod: 'put', + updateIncidentUrl: 'http://some.non.existent.com/{{{external.system.id}}}', + }, + isPreconfigured: false, + isDeprecated: false, + isMissingSecrets: false, + referencedByCount: 0, + }, { id: jiraConnector.id, actionTypeId: '.jira', diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/index.ts b/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/index.ts index 0c8c3931d15774..63d4eab0b81b55 100644 --- a/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/index.ts +++ b/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/index.ts @@ -10,9 +10,9 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default ({ loadTestFile }: FtrProviderContext): void => { describe('configuration tests', function () { - loadTestFile(require.resolve('./get_configure')); + // loadTestFile(require.resolve('./get_configure')); loadTestFile(require.resolve('./get_connectors')); - loadTestFile(require.resolve('./patch_configure')); - loadTestFile(require.resolve('./post_configure')); + // loadTestFile(require.resolve('./patch_configure')); + // loadTestFile(require.resolve('./post_configure')); }); }; diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/trial/index.ts b/x-pack/test/cases_api_integration/spaces_only/tests/trial/index.ts index cbf0f010048efd..59ad593ba367e9 100644 --- a/x-pack/test/cases_api_integration/spaces_only/tests/trial/index.ts +++ b/x-pack/test/cases_api_integration/spaces_only/tests/trial/index.ts @@ -19,9 +19,9 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => { await deleteSpaces(getService); }); - loadTestFile(require.resolve('../common')); - - loadTestFile(require.resolve('./cases/push_case')); + // loadTestFile(require.resolve('../common')); + // + // loadTestFile(require.resolve('./cases/push_case')); loadTestFile(require.resolve('./configure')); }); }; From a4df495e668e7e5916f81764adbf6883c4ca41a2 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 14 Jul 2022 13:15:59 -0600 Subject: [PATCH 51/97] uncomment test files --- .../spaces_only/tests/trial/configure/index.ts | 6 +++--- .../cases_api_integration/spaces_only/tests/trial/index.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/index.ts b/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/index.ts index 63d4eab0b81b55..0c8c3931d15774 100644 --- a/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/index.ts +++ b/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/index.ts @@ -10,9 +10,9 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default ({ loadTestFile }: FtrProviderContext): void => { describe('configuration tests', function () { - // loadTestFile(require.resolve('./get_configure')); + loadTestFile(require.resolve('./get_configure')); loadTestFile(require.resolve('./get_connectors')); - // loadTestFile(require.resolve('./patch_configure')); - // loadTestFile(require.resolve('./post_configure')); + loadTestFile(require.resolve('./patch_configure')); + loadTestFile(require.resolve('./post_configure')); }); }; diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/trial/index.ts b/x-pack/test/cases_api_integration/spaces_only/tests/trial/index.ts index 59ad593ba367e9..cbf0f010048efd 100644 --- a/x-pack/test/cases_api_integration/spaces_only/tests/trial/index.ts +++ b/x-pack/test/cases_api_integration/spaces_only/tests/trial/index.ts @@ -19,9 +19,9 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => { await deleteSpaces(getService); }); - // loadTestFile(require.resolve('../common')); - // - // loadTestFile(require.resolve('./cases/push_case')); + loadTestFile(require.resolve('../common')); + + loadTestFile(require.resolve('./cases/push_case')); loadTestFile(require.resolve('./configure')); }); }; From a3a5a223cd1912e20f16b43d078c4b0681ce85e0 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 14 Jul 2022 13:21:16 -0600 Subject: [PATCH 52/97] fix translations --- .../cases_webhook/action_variables.ts | 21 +++++++--- .../cases_webhook/translations.ts | 42 +++++++++++++++++++ 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/action_variables.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/action_variables.ts index ae81d63d26a6bb..7ec8b3e3d5f6d1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/action_variables.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/action_variables.ts @@ -6,30 +6,39 @@ */ import { ActionVariable } from '@kbn/alerting-plugin/common'; +import * as i18n from './translations'; export const casesVars: ActionVariable[] = [ - { name: 'case.title', description: 'test title', useWithTripleBracesInTemplates: true }, + { name: 'case.title', description: i18n.CASE_TITLE_DESC, useWithTripleBracesInTemplates: true }, { name: 'case.description', - description: 'test description', + description: i18n.CASE_DESCRIPTION_DESC, useWithTripleBracesInTemplates: true, }, - { name: 'case.tags', description: 'test tags', useWithTripleBracesInTemplates: true }, + { name: 'case.tags', description: i18n.CASE_TAGS_DESC, useWithTripleBracesInTemplates: true }, ]; export const commentVars: ActionVariable[] = [ - { name: 'case.comment', description: 'test comment', useWithTripleBracesInTemplates: true }, + { + name: 'case.comment', + description: i18n.CASE_COMMENT_DESC, + useWithTripleBracesInTemplates: true, + }, ]; export const urlVars: ActionVariable[] = [ - { name: 'external.system.id', description: 'test id', useWithTripleBracesInTemplates: true }, + { + name: 'external.system.id', + description: i18n.EXTERNAL_ID_DESC, + useWithTripleBracesInTemplates: true, + }, ]; export const urlVarsExt: ActionVariable[] = [ ...urlVars, { name: 'external.system.title', - description: 'test title', + description: i18n.EXTERNAL_TITLE_DESC, useWithTripleBracesInTemplates: true, }, ]; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts index af9f9fa8d6bbf3..e4ff63e2e278a5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts @@ -450,3 +450,45 @@ export const PREVIOUS = i18n.translate( defaultMessage: 'Previous', } ); + +export const CASE_TITLE_DESC = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.caseTitleDesc', + { + defaultMessage: 'Kibana case title', + } +); + +export const CASE_DESCRIPTION_DESC = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.caseDescriptionDesc', + { + defaultMessage: 'Kibana case description', + } +); + +export const CASE_TAGS_DESC = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.caseTagsDesc', + { + defaultMessage: 'Kibana case tags', + } +); + +export const CASE_COMMENT_DESC = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.caseCommentDesc', + { + defaultMessage: 'Kibana case comment', + } +); + +export const EXTERNAL_ID_DESC = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.externalIdDesc', + { + defaultMessage: 'External system id', + } +); + +export const EXTERNAL_TITLE_DESC = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.externalTitleDesc', + { + defaultMessage: 'External system title', + } +); From 2aa44a7fe3c73b9e332529485b6737e21edcf748 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 14 Jul 2022 14:40:35 -0600 Subject: [PATCH 53/97] couple pr fixes --- .../server/builtin_action_types/cases_webhook/mock.ts | 4 +--- .../plugins/cases/public/components/all_cases/columns.tsx | 1 - x-pack/plugins/cases/server/client/cases/push.ts | 1 + .../cases/server/connectors/cases_webook/mapping.ts | 7 ++----- 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/mock.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/mock.ts index 0fa28a16f29e04..4d7f08ad2ec436 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/mock.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/mock.ts @@ -80,6 +80,4 @@ const executorParams: ExecutorSubActionPushParams = { ], }; -export const apiParams: PushToServiceApiParams = { - ...executorParams, -}; +export const apiParams: PushToServiceApiParams = executorParams; diff --git a/x-pack/plugins/cases/public/components/all_cases/columns.tsx b/x-pack/plugins/cases/public/components/all_cases/columns.tsx index c0e7bc918154cc..3476be64c09ffb 100644 --- a/x-pack/plugins/cases/public/components/all_cases/columns.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/columns.tsx @@ -446,7 +446,6 @@ export const ExternalServiceColumn: React.FC = ({ theCase, connectors }) > {theCase.externalService?.externalTitle} - {hasDataToPush ? renderStringField(i18n.REQUIRES_UPDATE, `case-table-column-external-requiresUpdate`) : renderStringField(i18n.UP_TO_DATE, `case-table-column-external-upToDate`)} diff --git a/x-pack/plugins/cases/server/client/cases/push.ts b/x-pack/plugins/cases/server/client/cases/push.ts index db3acefe30dfb7..cfdec1da5e7752 100644 --- a/x-pack/plugins/cases/server/client/cases/push.ts +++ b/x-pack/plugins/cases/server/client/cases/push.ts @@ -135,6 +135,7 @@ export const push = async ( const alertsInfo = getAlertInfoFromComments(theCase?.comments); const alerts = await getAlerts(alertsInfo, clientArgs); + const getMappingsResponse = await casesClientInternal.configuration.getMappings({ connector: theCase.connector, }); diff --git a/x-pack/plugins/cases/server/connectors/cases_webook/mapping.ts b/x-pack/plugins/cases/server/connectors/cases_webook/mapping.ts index 4cdd0ad548a7bf..50bd66fdcec8c4 100644 --- a/x-pack/plugins/cases/server/connectors/cases_webook/mapping.ts +++ b/x-pack/plugins/cases/server/connectors/cases_webook/mapping.ts @@ -7,8 +7,5 @@ import { GetMapping } from './types'; -export const getMapping: GetMapping = () => { - return [ - // To do?? - ]; -}; +// Mappings are done directly in the connector configuration +export const getMapping: GetMapping = () => []; From 17507e99b2fe1b2ae5534146704e31d569d2b7f0 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 18 Jul 2022 08:19:57 -0600 Subject: [PATCH 54/97] shorter step titles --- .../builtin_action_types/cases_webhook/translations.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts index e4ff63e2e278a5..4c011189770741 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts @@ -419,21 +419,21 @@ export const STEP_1 = i18n.translate( export const STEP_2 = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.step2', { - defaultMessage: 'Create incident in external system', + defaultMessage: 'Create incident', } ); export const STEP_3 = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.step3', { - defaultMessage: 'Get incident information from external system', + defaultMessage: 'Get incident information', } ); export const STEP_4 = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.step4', { - defaultMessage: 'Add comments and updates in external system', + defaultMessage: 'Comments and updates', } ); From ffc0e8016cfceadaf2d258c5bd83c49e94d2ce10 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 18 Jul 2022 08:30:34 -0600 Subject: [PATCH 55/97] move makeCaseStringy --- .../server/builtin_action_types/cases_webhook/service.ts | 8 +------- .../server/builtin_action_types/cases_webhook/utils.ts | 7 +++++++ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts index 4708e988e3c569..11ff0e6e6327ad 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts @@ -14,6 +14,7 @@ import { createServiceError, getObjectValueByKey, getPushedDate, + makeCaseStringy, removeSlash, throwIfResponseIsNotValidSpecial, } from './utils'; @@ -111,13 +112,6 @@ export const createExternalService = ( } }; - const makeCaseStringy = (properties: Record) => ({ - case: Object.entries(properties).reduce( - (acc, [key, value]) => ({ ...acc, [key]: JSON.stringify(value) }), - {} - ), - }); - const createIncident = async ({ incident, }: CreateIncidentParams): Promise => { diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts index 69b3ad44a87ab5..b6281d4dd14391 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts @@ -116,3 +116,10 @@ export const throwIfResponseIsNotValidSpecial = ({ }; export const removeSlash = (url: string) => (url.endsWith('/') ? url.slice(0, -1) : url); + +export const makeCaseStringy = (properties: Record) => ({ + case: Object.entries(properties).reduce( + (acc, [key, value]) => ({ ...acc, [key]: JSON.stringify(value) }), + {} + ), +}); From a6aba999c04cdc1700f1aa7bfc3c0553a5b67bc0 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 18 Jul 2022 08:59:01 -0600 Subject: [PATCH 56/97] more fix whoops --- .../builtin_action_types/cases_webhook/webhook.tsx | 1 - .../public/application/components/json_field_wrapper.tsx | 8 +++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx index a933bba7308016..069facbf18f847 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx @@ -17,7 +17,6 @@ export function getActionType(): ActionTypeModel< > { return { id: '.cases-webhook', - // TODO: Steph/cases webhook get an icon iconClass: 'logoWebhook', selectMessage: i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.selectMessageText', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/json_field_wrapper.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/json_field_wrapper.tsx index 390110e9743e50..3764dea13de620 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/json_field_wrapper.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/json_field_wrapper.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import { i18n } from '@kbn/i18n'; import { FieldHook, getFieldValidityAndErrorMessage, @@ -39,7 +40,12 @@ export const JsonFieldWrapper = ({ field, ...rest }: Props) => { errors={errorMessage ? [errorMessage] : []} helpText={

{helpText}

} inputTargetValue={value} - label={label ?? 'JSON Editor'} + label={ + label ?? + i18n.translate('xpack.triggersActionsUI.jsonFieldWrapper.defaultLabel', { + defaultMessage: 'JSON Editor', + }) + } onDocumentsChange={onJsonUpdate} {...rest} /> From 30614848b23e72deb438181a3b5fb974116d7964 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 18 Jul 2022 12:47:19 -0600 Subject: [PATCH 57/97] tests --- .../cases_webhook/utils.test.ts | 142 ++++++++++++++++++ .../cases_webhook/utils.ts | 19 ++- .../server/lib/mustache_renderer.test.ts | 140 ++++++++++++++++- 3 files changed, 296 insertions(+), 5 deletions(-) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.test.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.test.ts new file mode 100644 index 00000000000000..971e76e1d084e7 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.test.ts @@ -0,0 +1,142 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getObjectValueByKey, throwIfResponseIsNotValidSpecial } from './utils'; + +const bigOlObject = { + fields: { + id: [ + { + good: { + cool: 'cool', + }, + }, + { + more: [ + { + more: { + complicated: 'complicated', + }, + }, + ], + }, + ], + }, + field: { + simple: 'simple', + }, +}; +describe('cases_webhook/utils', () => { + describe('getObjectValueByKey()', () => { + it('Handles a simple key', () => { + expect(getObjectValueByKey(bigOlObject, 'field.simple')).toEqual('simple'); + }); + it('Handles a complicated key', () => { + expect(getObjectValueByKey(bigOlObject, 'fields.id[0].good.cool')).toEqual('cool'); + }); + it('Handles a more complicated key', () => { + expect(getObjectValueByKey(bigOlObject, 'fields.id[1].more[0].more.complicated')).toEqual( + 'complicated' + ); + }); + it('Handles a bad key', () => { + expect(() => getObjectValueByKey(bigOlObject, 'bad.key')).toThrow( + 'Value not found in object for key bad.key' + ); + }); + }); + describe('throwIfResponseIsNotValidSpecial()', () => { + const res = { + data: bigOlObject, + headers: {}, + status: 200, + statusText: 'hooray', + config: { + method: 'post', + url: 'https://poster.com', + }, + }; + it('Throws error when missing content-type', () => { + expect(() => + throwIfResponseIsNotValidSpecial({ + res, + requiredAttributesToBeInTheResponse: ['field.simple'], + }) + ).toThrow( + 'Missing content type header in post https://poster.com. Supported content types: application/json' + ); + }); + it('Throws error when content-type is not valid', () => { + expect(() => + throwIfResponseIsNotValidSpecial({ + res: { + ...res, + headers: { + ['content-type']: 'not/cool', + }, + }, + requiredAttributesToBeInTheResponse: ['field.simple'], + }) + ).toThrow( + 'Unsupported content type: not/cool in post https://poster.com. Supported content types: application/json' + ); + }); + it('Throws error when for bad data', () => { + expect(() => + throwIfResponseIsNotValidSpecial({ + res: { + ...res, + headers: { + ['content-type']: 'application/json', + }, + data: '', + }, + requiredAttributesToBeInTheResponse: ['field.simple'], + }) + ).toThrow('Response is not a valid JSON'); + }); + it('Throws for bad key', () => { + expect(() => + throwIfResponseIsNotValidSpecial({ + res: { + ...res, + headers: { + ['content-type']: 'application/json', + }, + }, + requiredAttributesToBeInTheResponse: ['bad.key'], + }) + ).toThrow('Response is missing the expected field: bad.key'); + }); + it('Throws for multiple bad keys', () => { + expect(() => + throwIfResponseIsNotValidSpecial({ + res: { + ...res, + headers: { + ['content-type']: 'application/json', + }, + }, + requiredAttributesToBeInTheResponse: ['bad.key', 'bad.again'], + }) + ).toThrow('Response is missing the expected fields: bad.key, bad.again'); + }); + it('Does not throw for valid key', () => { + expect(() => + throwIfResponseIsNotValidSpecial({ + res: { + ...res, + headers: { + ['content-type']: 'application/json', + }, + }, + requiredAttributesToBeInTheResponse: ['fields.id[0].good.cool'], + }) + ).not.toThrow(); + }); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts index b6281d4dd14391..5205fea943fb74 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts @@ -55,7 +55,11 @@ export const getObjectValueByKey = ( obj: Record | unknown>, key: string ): string => { - return findTheValue(obj, splitKeys(key)) as string; + try { + return findTheValue(obj, splitKeys(key)) as string; + } catch (e) { + throw new Error(`Value not found in object for key ${key}`); + } }; export const throwIfResponseIsNotValidSpecial = ({ @@ -66,13 +70,18 @@ export const throwIfResponseIsNotValidSpecial = ({ requiredAttributesToBeInTheResponse?: string[]; }) => { const requiredContentType = 'application/json'; - const contentType = res.headers['content-type'] ?? 'undefined'; + const contentType = res.headers['content-type']; const data = res.data; /** * Check that the content-type of the response is application/json. * Then includes is added because the header can be application/json;charset=UTF-8. */ + if (contentType == null) { + throw new Error( + `Missing content type header in ${res.config.method} ${res.config.url}. Supported content types: ${requiredContentType}` + ); + } if (!contentType.includes(requiredContentType)) { throw new Error( `Unsupported content type: ${contentType} in ${res.config.method} ${res.config.url}. Supported content types: ${requiredContentType}` @@ -86,7 +95,7 @@ export const throwIfResponseIsNotValidSpecial = ({ * isObjectLike will fail. * Axios converts automatically JSON to JS objects. */ - if (!isEmpty(data) && !isObjectLike(data)) { + if (isEmpty(data) || !isObjectLike(data)) { throw new Error('Response is not a valid JSON'); } @@ -105,7 +114,9 @@ export const throwIfResponseIsNotValidSpecial = ({ */ requiredAttributesToBeInTheResponse.forEach((attr) => { // Check only for undefined as null is a valid value - if (getObjectValueByKey(data, attr) === undefined) { + try { + getObjectValueByKey(data, attr); + } catch (e) { errorAttributes.push(attr); } }); diff --git a/x-pack/plugins/actions/server/lib/mustache_renderer.test.ts b/x-pack/plugins/actions/server/lib/mustache_renderer.test.ts index 2dff9bac3eddf0..154937c6f8065f 100644 --- a/x-pack/plugins/actions/server/lib/mustache_renderer.test.ts +++ b/x-pack/plugins/actions/server/lib/mustache_renderer.test.ts @@ -5,7 +5,12 @@ * 2.0. */ -import { renderMustacheString, renderMustacheObject, Escape } from './mustache_renderer'; +import { + renderMustacheString, + renderMustacheStringNoEscape, + renderMustacheObject, + Escape, +} from './mustache_renderer'; const variables = { a: 1, @@ -120,6 +125,139 @@ describe('mustache_renderer', () => { ); }); }); + describe('renderMustacheStringNoEscape()', () => { + const id = 'cool_id'; + const title = 'cool_title'; + const summary = 'A cool good summary'; + const description = 'A cool good description'; + const tags = ['cool', 'neat', 'nice']; + const str = 'https://siem-kibana.atlassian.net/browse/{{{external.system.title}}}'; + + const objStr = + '{\n' + + '\t"fields": {\n' + + '\t "summary": {{{case.title}}},\n' + + '\t "description": {{{case.description}}},\n' + + '\t "labels": {{{case.tags}}},\n' + + '\t "project":{"key":"ROC"},\n' + + '\t "issuetype":{"id":"10024"}\n' + + '\t}\n' + + '}'; + const objStrDouble = + '{\n' + + '\t"fields": {\n' + + '\t "summary": {{case.title}},\n' + + '\t "description": {{case.description}},\n' + + '\t "labels": {{case.tags}},\n' + + '\t "project":{"key":"ROC"},\n' + + '\t "issuetype":{"id":"10024"}\n' + + '\t}\n' + + '}'; + const caseVariables = { + case: { + title: summary, + description, + tags, + }, + }; + const caseVariablesStr = { + case: { + title: JSON.stringify(summary), + description: JSON.stringify(description), + tags: JSON.stringify(tags), + }, + }; + it('Inserts variables into string without quotes', () => { + const urlVariables = { + external: { + system: { + id, + title, + }, + }, + }; + expect(renderMustacheStringNoEscape(str, urlVariables)).toBe( + `https://siem-kibana.atlassian.net/browse/cool_title` + ); + }); + it('Inserts variables into url with quotes whens stringified', () => { + const urlVariablesStr = { + external: { + system: { + id: JSON.stringify(id), + title: JSON.stringify(title), + }, + }, + }; + expect(renderMustacheStringNoEscape(str, urlVariablesStr)).toBe( + `https://siem-kibana.atlassian.net/browse/"cool_title"` + ); + }); + it('Inserts variables into JSON non-escaped when triple brackets and JSON.stringified variables', () => { + expect(renderMustacheStringNoEscape(objStr, caseVariablesStr)).toBe( + `{ +\t"fields": { +\t "summary": "A cool good summary", +\t "description": "A cool good description", +\t "labels": ["cool","neat","nice"], +\t "project":{"key":"ROC"}, +\t "issuetype":{"id":"10024"} +\t} +}` + ); + }); + it('Inserts variables into JSON without quotes when triple brackets and NON stringified variables', () => { + expect(renderMustacheStringNoEscape(objStr, caseVariables)).toBe( + `{ +\t"fields": { +\t "summary": A cool good summary, +\t "description": A cool good description, +\t "labels": cool,neat,nice, +\t "project":{"key":"ROC"}, +\t "issuetype":{"id":"10024"} +\t} +}` + ); + }); + it('Inserts variables into JSON escaped when double brackets and JSON.stringified variables', () => { + expect(renderMustacheStringNoEscape(objStrDouble, caseVariablesStr)).toBe( + `{ +\t"fields": { +\t "summary": "A cool good summary", +\t "description": "A cool good description", +\t "labels": ["cool","neat","nice"], +\t "project":{"key":"ROC"}, +\t "issuetype":{"id":"10024"} +\t} +}` + ); + }); + it('Inserts variables into JSON without quotes when double brackets and NON stringified variables', () => { + expect(renderMustacheStringNoEscape(objStrDouble, caseVariables)).toBe( + `{ +\t"fields": { +\t "summary": A cool good summary, +\t "description": A cool good description, +\t "labels": cool,neat,nice, +\t "project":{"key":"ROC"}, +\t "issuetype":{"id":"10024"} +\t} +}` + ); + }); + + it('handles errors triple bracket', () => { + expect(renderMustacheStringNoEscape('{{{a}}', variables)).toMatchInlineSnapshot( + `"error rendering mustache template \\"{{{a}}\\": Unclosed tag at 6"` + ); + }); + + it('handles errors double bracket', () => { + expect(renderMustacheStringNoEscape('{{a}', variables)).toMatchInlineSnapshot( + `"error rendering mustache template \\"{{a}\\": Unclosed tag at 4"` + ); + }); + }); const object = { literal: 0, From fd478d799a9adfbf09aec68034a96e404a28eba9 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 19 Jul 2022 09:08:42 -0600 Subject: [PATCH 58/97] pr changes --- .../cases_webhook/api.test.ts | 35 ------------------- .../cases_webhook/schema.ts | 10 +----- .../cases_webhook/utils.ts | 4 ++- .../builtin_action_types/cases_webhook.ts | 27 -------------- 4 files changed, 4 insertions(+), 72 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/api.test.ts index 824726bbdd5d70..8709cbc73e695c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/api.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/api.test.ts @@ -75,20 +75,6 @@ describe('api', () => { expect(externalService.updateIncident).not.toHaveBeenCalled(); }); - test('it calls createIncident correctly without mapping', async () => { - const params = { ...apiParams, incident: { ...apiParams.incident, externalId: null } }; - await api.pushToService({ externalService, params, logger: mockedLogger }); - - expect(externalService.createIncident).toHaveBeenCalledWith({ - incident: { - description: 'Incident description', - summary: 'Incident title', - labels: ['kibana', 'elastic'], - }, - }); - expect(externalService.updateIncident).not.toHaveBeenCalled(); - }); - test('it calls createComment correctly', async () => { const params = { ...apiParams, incident: { ...apiParams.incident, externalId: null } }; await api.pushToService({ externalService, params, logger: mockedLogger }); @@ -109,27 +95,6 @@ describe('api', () => { }, }); }); - - test('it calls createComment correctly without mapping', async () => { - const params = { ...apiParams, incident: { ...apiParams.incident, externalId: null } }; - await api.pushToService({ externalService, params, logger: mockedLogger }); - expect(externalService.createComment).toHaveBeenCalledTimes(2); - expect(externalService.createComment).toHaveBeenNthCalledWith(1, { - incidentId: 'incident-1', - comment: { - commentId: 'case-comment-1', - comment: 'A comment', - }, - }); - - expect(externalService.createComment).toHaveBeenNthCalledWith(2, { - incidentId: 'incident-1', - comment: { - commentId: 'case-comment-2', - comment: 'Another comment', - }, - }); - }); }); describe('update incident', () => { diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts index ad4f05651b9d6b..55d4451a502561 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts @@ -74,15 +74,7 @@ export const ExecutorSubActionPushParamsSchema = schema.object({ summary: schema.string(), description: schema.nullable(schema.string()), externalId: schema.nullable(schema.string()), - labels: schema.nullable( - schema.arrayOf( - schema.string({ - validate: (label) => - // Matches any space, tab or newline character. - label.match(/\s/g) ? `The label ${label} cannot contain spaces` : undefined, - }) - ) - ), + labels: schema.nullable(schema.arrayOf(schema.string())), }), comments: schema.nullable( schema.arrayOf( diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts index 5205fea943fb74..06bae0b91a0919 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts @@ -115,7 +115,9 @@ export const throwIfResponseIsNotValidSpecial = ({ requiredAttributesToBeInTheResponse.forEach((attr) => { // Check only for undefined as null is a valid value try { - getObjectValueByKey(data, attr); + if (getObjectValueByKey(data, attr) === undefined) { + errorAttributes.push(attr); + } } catch (e) { errorAttributes.push(attr); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/cases_webhook.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/cases_webhook.ts index 00a1b3a5ef0538..40bbfcd7090f32 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/cases_webhook.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/cases_webhook.ts @@ -369,33 +369,6 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { }); }); }); - - it('should handle failing with a simulated success when labels containing a space', async () => { - await supertest - .post(`/api/actions/connector/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - ...mockCasesWebhook.params, - subActionParams: { - incident: { - ...mockCasesWebhook.params.subActionParams.incident, - labels: ['label with spaces'], - }, - comments: [], - }, - }, - }) - .then((resp: any) => { - expect(resp.body).to.eql({ - connector_id: simulatedActionId, - status: 'error', - retry: false, - message: - 'error validating action params: [subActionParams.incident.labels]: types that failed validation:\n- [subActionParams.incident.labels.0.0]: The label label with spaces cannot contain spaces\n- [subActionParams.incident.labels.1]: expected value to equal [null]', - }); - }); - }); }); describe('Execution', () => { From a71ddf79f568fc4c6304bc4666284b385f1ba325 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 19 Jul 2022 10:37:20 -0600 Subject: [PATCH 59/97] pr changes 3 --- .../cases_webhook/api.test.ts | 12 +-- .../cases_webhook/mock.ts | 6 +- .../cases_webhook/schema.ts | 4 +- .../cases_webhook/service.test.ts | 30 ++++---- .../cases_webhook/service.ts | 42 +++++----- .../cases_webhook/types.ts | 9 +-- .../cases_webhook/utils.test.ts | 36 ++++++--- .../cases_webhook/utils.ts | 45 +++-------- .../cases_webhook/validators.ts | 76 +++++-------------- .../cases/common/api/connectors/index.ts | 5 +- .../public/components/connectors/card.tsx | 24 +----- .../connectors/cases_webhook/case_fields.tsx | 34 +++++---- .../connectors/cases_webhook/index.ts | 4 +- .../{ => cases_webhook}/translations.ts | 0 .../public/components/connectors/index.ts | 3 +- .../server/connectors/cases_webook/format.ts | 7 +- .../server/connectors/cases_webook/types.ts | 11 +-- .../cases_webhook/webhook.tsx | 6 +- .../cases_webhook/webhook_params.test.tsx | 6 +- .../cases_webhook/webhook_params.tsx | 32 ++++---- .../components/json_field_wrapper.scss | 3 - .../components/json_field_wrapper.styles.ts} | 15 ++-- .../components/json_field_wrapper.tsx | 30 ++++---- .../mustache_text_field_wrapper.tsx | 1 - 24 files changed, 185 insertions(+), 256 deletions(-) rename x-pack/plugins/cases/public/components/connectors/{ => cases_webhook}/translations.ts (100%) delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/json_field_wrapper.scss rename x-pack/plugins/{cases/common/api/connectors/cases_webhook.ts => triggers_actions_ui/public/application/components/json_field_wrapper.styles.ts} (51%) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/api.test.ts index 8709cbc73e695c..4764d9acdb2357 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/api.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/api.test.ts @@ -67,9 +67,9 @@ describe('api', () => { expect(externalService.createIncident).toHaveBeenCalledWith({ incident: { - labels: ['kibana', 'elastic'], + tags: ['kibana', 'elastic'], description: 'Incident description', - summary: 'Incident title', + title: 'Incident title', }, }); expect(externalService.updateIncident).not.toHaveBeenCalled(); @@ -146,9 +146,9 @@ describe('api', () => { expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { - labels: ['kibana', 'elastic'], + tags: ['kibana', 'elastic'], description: 'Incident description', - summary: 'Incident title', + title: 'Incident title', }, }); expect(externalService.createIncident).not.toHaveBeenCalled(); @@ -162,8 +162,8 @@ describe('api', () => { incidentId: 'incident-3', incident: { description: 'Incident description', - summary: 'Incident title', - labels: ['kibana', 'elastic'], + title: 'Incident title', + tags: ['kibana', 'elastic'], }, }); expect(externalService.createIncident).not.toHaveBeenCalled(); diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/mock.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/mock.ts index 4d7f08ad2ec436..20a5bcdfc1162f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/mock.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/mock.ts @@ -13,7 +13,7 @@ const createMock = (): jest.Mocked => { Promise.resolve({ id: 'incident-1', key: 'CK-1', - summary: 'title from jira', + title: 'title from jira', description: 'description from jira', created: '2020-04-27T10:59:46.202Z', updated: '2020-04-27T10:59:46.202Z', @@ -64,9 +64,9 @@ export const externalServiceMock = { const executorParams: ExecutorSubActionPushParams = { incident: { externalId: 'incident-3', - summary: 'Incident title', + title: 'Incident title', description: 'Incident description', - labels: ['kibana', 'elastic'], + tags: ['kibana', 'elastic'], }, comments: [ { diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts index 55d4451a502561..01c5b558023cd0 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts @@ -71,10 +71,10 @@ export const ExternalIncidentServiceSecretConfigurationSchema = schema.object( export const ExecutorSubActionPushParamsSchema = schema.object({ incident: schema.object({ - summary: schema.string(), + title: schema.string(), description: schema.nullable(schema.string()), externalId: schema.nullable(schema.string()), - labels: schema.nullable(schema.arrayOf(schema.string())), + tags: schema.nullable(schema.arrayOf(schema.string())), }), comments: schema.nullable( schema.arrayOf( diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts index 5148becbec2253..0ba198135e681c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts @@ -33,7 +33,7 @@ const config: CasesWebhookPublicConfigurationType = { createCommentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}/comment', createIncidentJson: - '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"labels":{{{case.tags}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', + '{"fields":{"title":{{{case.title}}},"description":{{{case.description}}},"tags":{{{case.tags}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', createIncidentMethod: CasesWebhookMethods.POST, createIncidentResponseKey: 'id', createIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue', @@ -45,7 +45,7 @@ const config: CasesWebhookPublicConfigurationType = { incidentViewUrl: 'https://siem-kibana.atlassian.net/browse/{{{external.system.title}}}', getIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}', updateIncidentJson: - '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"labels":{{{case.tags}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', + '{"fields":{"title":{{{case.title}}},"description":{{{case.description}}},"tags":{{{case.tags}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', updateIncidentMethod: CasesWebhookMethods.PUT, updateIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}', }; @@ -129,7 +129,7 @@ describe('Cases webhook service', () => { id: '1', key: 'CK-1', fields: { - summary: 'title', + title: 'title', description: 'description', created: '2021-10-20T19:41:02.754+0300', updated: '2021-10-20T19:41:02.754+0300', @@ -137,14 +137,14 @@ describe('Cases webhook service', () => { }, }; - test('it returns the incident correctly', async () => { + test.only('it returns the incident correctly', async () => { requestMock.mockImplementation(() => createAxiosResponse(axiosRes)); const res = await service.getIncident('1'); expect(res).toEqual({ id: '1', title: 'CK-1', - created: '2021-10-20T19:41:02.754+0300', - updated: '2021-10-20T19:41:02.754+0300', + createdAt: '2021-10-20T19:41:02.754+0300', + updatedAt: '2021-10-20T19:41:02.754+0300', }); }); @@ -195,9 +195,9 @@ describe('Cases webhook service', () => { describe('createIncident', () => { const incident = { incident: { - summary: 'title', + title: 'title', description: 'desc', - labels: ['hello', 'world'], + tags: ['hello', 'world'], issueType: '10006', priority: 'High', parent: 'RJ-107', @@ -207,7 +207,7 @@ describe('Cases webhook service', () => { test('it creates the incident correctly', async () => { requestMock.mockImplementationOnce(() => createAxiosResponse({ - data: { id: '1', key: 'CK-1', fields: { summary: 'title', description: 'description' } }, + data: { id: '1', key: 'CK-1', fields: { title: 'title', description: 'description' } }, }) ); @@ -224,7 +224,7 @@ describe('Cases webhook service', () => { const res = await service.createIncident(incident); expect(requestMock.mock.calls[0][0].data).toEqual( - `{"fields":{"summary":"title","description":"desc","labels":["hello","world"],"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}` + `{"fields":{"title":"title","description":"desc","tags":["hello","world"],"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}` ); expect(res).toEqual({ @@ -264,7 +264,7 @@ describe('Cases webhook service', () => { logger, method: CasesWebhookMethods.POST, configurationUtilities, - data: `{"fields":{"summary":"title","description":"desc","labels":["hello","world"],"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}`, + data: `{"fields":{"title":"title","description":"desc","tags":["hello","world"],"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}`, }); }); @@ -303,9 +303,9 @@ describe('Cases webhook service', () => { const incident = { incidentId: '1', incident: { - summary: 'title', + title: 'title', description: 'desc', - labels: ['hello', 'world'], + tags: ['hello', 'world'], }, }; @@ -351,9 +351,9 @@ describe('Cases webhook service', () => { url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1', data: JSON.stringify({ fields: { - summary: 'title', + title: 'title', description: 'desc', - labels: ['hello', 'world'], + tags: ['hello', 'world'], project: { key: 'ROC' }, issuetype: { id: '10024' }, }, diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts index 11ff0e6e6327ad..c7072669688ad4 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts @@ -14,7 +14,7 @@ import { createServiceError, getObjectValueByKey, getPushedDate, - makeCaseStringy, + stringifyObjValues, removeSlash, throwIfResponseIsNotValidSpecial, } from './utils'; @@ -76,7 +76,10 @@ export const createExternalService = ( ...(hasAuth && isString(secrets.user) && isString(secrets.password) ? { auth: { username: secrets.user, password: secrets.password } } : {}), - ...(headers != null ? { headers } : {}), + headers: { + ['content-type']: 'application/json', + ...(headers != null ? headers : {}), + }, }); const getIncident = async (id: string): Promise => { @@ -103,19 +106,19 @@ export const createExternalService = ( ], }); - const title = getObjectValueByKey(res.data, getIncidentResponseExternalTitleKey); - const created = getObjectValueByKey(res.data, getIncidentResponseCreatedDateKey); - const updated = getObjectValueByKey(res.data, getIncidentResponseUpdatedDateKey); - return { id, title, created, updated }; + const title = getObjectValueByKey(res.data, getIncidentResponseExternalTitleKey); + const createdAt = getObjectValueByKey(res.data, getIncidentResponseCreatedDateKey); + const updatedAt = getObjectValueByKey(res.data, getIncidentResponseUpdatedDateKey); + return { id, title, createdAt, updatedAt }; } catch (error) { - throw createServiceError(error, 'Unable to get incident'); + throw createServiceError(error, `Unable to get incident with id ${id}`); } }; const createIncident = async ({ incident, }: CreateIncidentParams): Promise => { - const { labels, summary, description } = incident; + const { tags, title, description } = incident; try { const res: AxiosResponse = await request({ axios: axiosInstance, @@ -124,10 +127,10 @@ export const createExternalService = ( method: createIncidentMethod, data: renderMustacheStringNoEscape( createIncidentJson, - makeCaseStringy({ - title: summary, + stringifyObjValues({ + title, description: description ?? '', - tags: labels ?? [], + tags: tags ?? [], }) ), configurationUtilities, @@ -139,7 +142,7 @@ export const createExternalService = ( res, requiredAttributesToBeInTheResponse: [createIncidentResponseKey], }); - const externalId = getObjectValueByKey(data, createIncidentResponseKey); + const externalId = getObjectValueByKey(data, createIncidentResponseKey); const insertedIncident = await getIncident(externalId); logger.debug(`response from webhook action "${actionId}": [HTTP ${status}] ${statusText}`); @@ -155,7 +158,7 @@ export const createExternalService = ( }, }, }), - pushedDate: getPushedDate(insertedIncident.created), + pushedDate: getPushedDate(insertedIncident.createdAt), }; } catch (error) { throw createServiceError(error, 'Unable to create incident'); @@ -167,7 +170,7 @@ export const createExternalService = ( incident, }: UpdateIncidentParams): Promise => { try { - const { labels, summary, description } = incident; + const { tags, title, description } = incident; const res = await request({ axios: axiosInstance, method: updateIncidentMethod, @@ -181,10 +184,10 @@ export const createExternalService = ( logger, data: renderMustacheStringNoEscape( updateIncidentJson, - makeCaseStringy({ - ...(summary ? { title: summary } : {}), + stringifyObjValues({ + ...(title ? { title } : {}), ...(description ? { description } : {}), - ...(labels ? { tags: labels } : {}), + ...(tags ? { tags } : {}), }) ), configurationUtilities, @@ -207,7 +210,7 @@ export const createExternalService = ( }, }, }), - pushedDate: getPushedDate(updatedIncident.updated), + pushedDate: getPushedDate(updatedIncident.updatedAt), }; } catch (error) { throw createServiceError(error, `Unable to update incident with id ${incidentId}`); @@ -232,7 +235,7 @@ export const createExternalService = ( logger, data: renderMustacheStringNoEscape( createCommentJson, - makeCaseStringy({ comment: comment.comment }) + stringifyObjValues({ comment: comment.comment }) ), configurationUtilities, }); @@ -240,7 +243,6 @@ export const createExternalService = ( throwIfResponseIsNotValidSpecial({ res, }); - return res.data; } catch (error) { throw createServiceError(error, `Unable to create comment at incident with id ${incidentId}`); } diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts index 89eb915ee83638..61dd1b645abdb2 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts @@ -104,8 +104,8 @@ export interface ExternalServiceCommentResponse { export interface GetIncidentResponse { id: string; title: string; - created: string; - updated: string; + createdAt: string; + updatedAt: string; } export interface ExternalServiceApi { @@ -113,8 +113,3 @@ export interface ExternalServiceApi { } export type CasesWebhookExecutorResultData = ExternalServiceIncidentResponse; - -export interface ResponseError { - errorMessages: string[] | null | undefined; - errors: { [k: string]: string } | null | undefined; -} diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.test.ts index 971e76e1d084e7..eb8539bd677939 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { getObjectValueByKey, throwIfResponseIsNotValidSpecial } from './utils'; +import { getObjectValueByKey, stringifyObjValues, throwIfResponseIsNotValidSpecial } from './utils'; const bigOlObject = { fields: { @@ -33,20 +33,20 @@ const bigOlObject = { describe('cases_webhook/utils', () => { describe('getObjectValueByKey()', () => { it('Handles a simple key', () => { - expect(getObjectValueByKey(bigOlObject, 'field.simple')).toEqual('simple'); + expect(getObjectValueByKey(bigOlObject, 'field.simple')).toEqual('simple'); }); it('Handles a complicated key', () => { - expect(getObjectValueByKey(bigOlObject, 'fields.id[0].good.cool')).toEqual('cool'); + expect(getObjectValueByKey(bigOlObject, 'fields.id[0].good.cool')).toEqual( + 'cool' + ); }); it('Handles a more complicated key', () => { - expect(getObjectValueByKey(bigOlObject, 'fields.id[1].more[0].more.complicated')).toEqual( - 'complicated' - ); + expect( + getObjectValueByKey(bigOlObject, 'fields.id[1].more[0].more.complicated') + ).toEqual('complicated'); }); it('Handles a bad key', () => { - expect(() => getObjectValueByKey(bigOlObject, 'bad.key')).toThrow( - 'Value not found in object for key bad.key' - ); + expect(getObjectValueByKey(bigOlObject, 'bad.key')).toEqual(undefined); }); }); describe('throwIfResponseIsNotValidSpecial()', () => { @@ -139,4 +139,22 @@ describe('cases_webhook/utils', () => { ).not.toThrow(); }); }); + describe('stringifyObjValues()', () => { + const caseObj = { + title: 'title', + description: 'description', + labels: ['cool', 'rad', 'awesome'], + comment: 'comment', + }; + it('Handles a case object', () => { + expect(stringifyObjValues(caseObj)).toEqual({ + case: { + comment: '"comment"', + description: '"description"', + labels: '["cool","rad","awesome"]', + title: '"title"', + }, + }); + }); + }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts index 06bae0b91a0919..51b03804d3f76a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts @@ -6,7 +6,7 @@ */ import { AxiosResponse, AxiosError } from 'axios'; -import { isEmpty, isObjectLike } from 'lodash'; +import { isEmpty, isObjectLike, get } from 'lodash'; import { addTimeZoneToDate, getErrorMessage } from '../lib/axios_utils'; import * as i18n from './translations'; @@ -21,7 +21,7 @@ export const createServiceError = (error: AxiosError, message: string) => ); export const getPushedDate = (timestamp?: string) => { - if (timestamp != null) { + if (timestamp != null && new Date(timestamp).getTime() > 0) { try { return new Date(timestamp).toISOString(); } catch (e) { @@ -31,35 +31,12 @@ export const getPushedDate = (timestamp?: string) => { return new Date().toISOString(); }; -const splitKeys = (key: string) => { - const split1 = key.split('.'); - const split2 = split1.reduce((acc: string[], k: string) => { - const newSplit = k.split('['); - return [...acc, ...newSplit.filter((j) => j !== '')]; - }, []); - return split2.reduce((acc: string[], k: string) => { - const newSplit = k.split(']'); - return [...acc, ...newSplit.filter((j) => j !== '')]; - }, []); -}; -const findTheValue = (obj: Record | unknown>, keys: string[]) => { - let currentLevel: unknown = obj; - keys.forEach((k: string) => { - // @ts-ignore - currentLevel = currentLevel[k]; - }); - return currentLevel; -}; - -export const getObjectValueByKey = ( - obj: Record | unknown>, +export const getObjectValueByKey = ( + obj: Record | T>, key: string -): string => { - try { - return findTheValue(obj, splitKeys(key)) as string; - } catch (e) { - throw new Error(`Value not found in object for key ${key}`); - } +): T => { + // @ts-ignore + return get(obj, key); }; export const throwIfResponseIsNotValidSpecial = ({ @@ -114,11 +91,7 @@ export const throwIfResponseIsNotValidSpecial = ({ */ requiredAttributesToBeInTheResponse.forEach((attr) => { // Check only for undefined as null is a valid value - try { - if (getObjectValueByKey(data, attr) === undefined) { - errorAttributes.push(attr); - } - } catch (e) { + if (typeof getObjectValueByKey(data, attr) === 'undefined') { errorAttributes.push(attr); } }); @@ -130,7 +103,7 @@ export const throwIfResponseIsNotValidSpecial = ({ export const removeSlash = (url: string) => (url.endsWith('/') ? url.slice(0, -1) : url); -export const makeCaseStringy = (properties: Record) => ({ +export const stringifyObjValues = (properties: Record) => ({ case: Object.entries(properties).reduce( (acc, [key, value]) => ({ ...acc, [key]: JSON.stringify(value) }), {} diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts index 449925238a95f3..8e756a0e1c61c5 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts @@ -25,64 +25,28 @@ const validateConfig = ( updateIncidentUrl, } = configObject; - try { - new URL(createIncidentUrl); - } catch (err) { - return i18n.INVALID_URL(err, 'createIncidentUrl'); - } - - try { - configurationUtilities.ensureUriAllowed(createIncidentUrl); - } catch (allowListError) { - return i18n.CONFIG_ERR(allowListError.message); - } - if (createCommentUrl) { - try { - new URL(createCommentUrl); - } catch (err) { - return i18n.INVALID_URL(err, 'createCommentUrl'); - } - - try { - configurationUtilities.ensureUriAllowed(createCommentUrl); - } catch (allowListError) { - return i18n.CONFIG_ERR(allowListError.message); + const urls = [ + createCommentUrl, + createIncidentUrl, + incidentViewUrl, + getIncidentUrl, + updateIncidentUrl, + ]; + + for (const url of urls) { + if (url) { + try { + new URL(url); + } catch (err) { + return i18n.INVALID_URL(err, url); + } + try { + configurationUtilities.ensureUriAllowed(url); + } catch (allowListError) { + return i18n.CONFIG_ERR(allowListError.message); + } } } - - try { - new URL(incidentViewUrl); - } catch (err) { - return i18n.INVALID_URL(err, 'incidentViewUrl'); - } - - try { - configurationUtilities.ensureUriAllowed(incidentViewUrl); - } catch (allowListError) { - return i18n.CONFIG_ERR(allowListError.message); - } - try { - new URL(getIncidentUrl); - } catch (err) { - return i18n.INVALID_URL(err, 'getIncidentUrl'); - } - - try { - configurationUtilities.ensureUriAllowed(getIncidentUrl); - } catch (allowListError) { - return i18n.CONFIG_ERR(allowListError.message); - } - try { - new URL(updateIncidentUrl); - } catch (err) { - return i18n.INVALID_URL(err, 'updateIncidentUrl'); - } - - try { - configurationUtilities.ensureUriAllowed(updateIncidentUrl); - } catch (allowListError) { - return i18n.CONFIG_ERR(allowListError.message); - } }; export const validateSecrets = (secrets: CasesWebhookSecretConfigurationType) => { diff --git a/x-pack/plugins/cases/common/api/connectors/index.ts b/x-pack/plugins/cases/common/api/connectors/index.ts index 4d87c6ade07b01..e1e110913de749 100644 --- a/x-pack/plugins/cases/common/api/connectors/index.ts +++ b/x-pack/plugins/cases/common/api/connectors/index.ts @@ -16,14 +16,12 @@ import type { ActionType } from '@kbn/actions-plugin/common'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import type { ActionResult } from '@kbn/actions-plugin/server/types'; -import { CasesWebhookFieldsRT } from './cases_webhook'; import { JiraFieldsRT } from './jira'; import { ResilientFieldsRT } from './resilient'; import { ServiceNowITSMFieldsRT } from './servicenow_itsm'; import { ServiceNowSIRFieldsRT } from './servicenow_sir'; import { SwimlaneFieldsRT } from './swimlane'; -export * from './cases_webhook'; export * from './jira'; export * from './servicenow_itsm'; export * from './servicenow_sir'; @@ -35,7 +33,6 @@ export type ActionConnector = ActionResult; export type ActionTypeConnector = ActionType; export const ConnectorFieldsRt = rt.union([ - CasesWebhookFieldsRT, JiraFieldsRT, ResilientFieldsRT, ServiceNowITSMFieldsRT, @@ -55,7 +52,7 @@ export enum ConnectorTypes { const ConnectorCasesWebhookTypeFieldsRt = rt.type({ type: rt.literal(ConnectorTypes.casesWebhook), - fields: rt.union([CasesWebhookFieldsRT, rt.null]), + fields: rt.null, }); const ConnectorJiraTypeFieldsRt = rt.type({ diff --git a/x-pack/plugins/cases/public/components/connectors/card.tsx b/x-pack/plugins/cases/public/components/connectors/card.tsx index 845e64f9e137da..d6ef92572e5069 100644 --- a/x-pack/plugins/cases/public/components/connectors/card.tsx +++ b/x-pack/plugins/cases/public/components/connectors/card.tsx @@ -6,27 +6,18 @@ */ import React, { memo, useMemo } from 'react'; -import { - EuiCallOut, - EuiCard, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiLoadingSpinner, -} from '@elastic/eui'; +import { EuiCard, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLoadingSpinner } from '@elastic/eui'; import styled from 'styled-components'; import { ConnectorTypes } from '../../../common/api'; import { useKibana } from '../../common/lib/kibana'; import { getConnectorIcon } from '../utils'; -import * as i18n from './translations'; interface ConnectorCardProps { connectorType: ConnectorTypes; title: string; listItems: Array<{ title: string; description: React.ReactNode }>; isLoading: boolean; - showCommentsWarning?: boolean; } const StyledText = styled.span` @@ -40,7 +31,6 @@ const ConnectorCardDisplay: React.FC = ({ title, listItems, isLoading, - showCommentsWarning = false, }) => { const { triggersActionsUi } = useKibana().services; @@ -79,17 +69,7 @@ const ConnectorCardDisplay: React.FC = ({ paddingSize="none" title={title} titleSize="xs" - > - {showCommentsWarning && ( - -

{i18n.CREATE_COMMENT_WARNING_DESC(title)}

-
- )} - + />
{icon}
diff --git a/x-pack/plugins/cases/public/components/connectors/cases_webhook/case_fields.tsx b/x-pack/plugins/cases/public/components/connectors/cases_webhook/case_fields.tsx index d8f20184ee793e..f0410839517f2a 100644 --- a/x-pack/plugins/cases/public/components/connectors/cases_webhook/case_fields.tsx +++ b/x-pack/plugins/cases/public/components/connectors/cases_webhook/case_fields.tsx @@ -7,24 +7,32 @@ import React from 'react'; -import { ConnectorTypes, CasesWebhookFieldsType } from '../../../../common/api'; +import { EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { ConnectorTypes } from '../../../../common/api'; import { ConnectorFieldsProps } from '../types'; import { ConnectorCard } from '../card'; +import * as i18n from './translations'; -const CasesWebhookComponent: React.FunctionComponent< - ConnectorFieldsProps -> = ({ connector, isEdit = true }) => ( +const CasesWebhookComponent: React.FunctionComponent> = ({ + connector, + isEdit = true, +}) => ( <> {!isEdit && ( - + <> + + + {(!connector.config?.createCommentUrl || !connector.config?.createCommentJson) && ( + +

{i18n.CREATE_COMMENT_WARNING_DESC(connector.name)}

+
+ )} + )} ); diff --git a/x-pack/plugins/cases/public/components/connectors/cases_webhook/index.ts b/x-pack/plugins/cases/public/components/connectors/cases_webhook/index.ts index dac2cb5bd1f54f..e884ef36841714 100644 --- a/x-pack/plugins/cases/public/components/connectors/cases_webhook/index.ts +++ b/x-pack/plugins/cases/public/components/connectors/cases_webhook/index.ts @@ -8,9 +8,9 @@ import { lazy } from 'react'; import { CaseConnector } from '../types'; -import { ConnectorTypes, CasesWebhookFieldsType } from '../../../../common/api'; +import { ConnectorTypes } from '../../../../common/api'; -export const getCaseConnector = (): CaseConnector => { +export const getCaseConnector = (): CaseConnector => { return { id: ConnectorTypes.casesWebhook, fieldsComponent: lazy(() => import('./case_fields')), diff --git a/x-pack/plugins/cases/public/components/connectors/translations.ts b/x-pack/plugins/cases/public/components/connectors/cases_webhook/translations.ts similarity index 100% rename from x-pack/plugins/cases/public/components/connectors/translations.ts rename to x-pack/plugins/cases/public/components/connectors/cases_webhook/translations.ts diff --git a/x-pack/plugins/cases/public/components/connectors/index.ts b/x-pack/plugins/cases/public/components/connectors/index.ts index 6f405bc45fc3b8..f70773c4864d23 100644 --- a/x-pack/plugins/cases/public/components/connectors/index.ts +++ b/x-pack/plugins/cases/public/components/connectors/index.ts @@ -13,7 +13,6 @@ import { getCaseConnector as getResilientCaseConnector } from './resilient'; import { getCaseConnector as getCasesWebhookCaseConnector } from './cases_webhook'; import { getServiceNowITSMCaseConnector, getServiceNowSIRCaseConnector } from './servicenow'; import { - CasesWebhookFieldsType, JiraFieldsType, ServiceNowITSMFieldsType, ServiceNowSIRFieldsType, @@ -43,7 +42,7 @@ class CaseConnectors { ); this.caseConnectorsRegistry.register(getServiceNowSIRCaseConnector()); this.caseConnectorsRegistry.register(getSwimlaneCaseConnector()); - this.caseConnectorsRegistry.register(getCasesWebhookCaseConnector()); + this.caseConnectorsRegistry.register(getCasesWebhookCaseConnector()); } registry(): CaseConnectorsRegistry { diff --git a/x-pack/plugins/cases/server/connectors/cases_webook/format.ts b/x-pack/plugins/cases/server/connectors/cases_webook/format.ts index 612de2e7e678fd..2356df109dd008 100644 --- a/x-pack/plugins/cases/server/connectors/cases_webook/format.ts +++ b/x-pack/plugins/cases/server/connectors/cases_webook/format.ts @@ -7,11 +7,10 @@ import { Format } from './types'; -export const format: Format = (theCase, alerts) => { +export const format: Format = (theCase) => { return { - summary: theCase.title, + title: theCase.title, description: theCase.description, - // CasesWebook do not allows empty spaces on labels. We replace white spaces with hyphens - labels: theCase.tags.map((tag) => tag.replace(/\s+/g, '-')), + tags: theCase.tags, }; }; diff --git a/x-pack/plugins/cases/server/connectors/cases_webook/types.ts b/x-pack/plugins/cases/server/connectors/cases_webook/types.ts index 0a0e42c7f29703..61d74070370dc0 100644 --- a/x-pack/plugins/cases/server/connectors/cases_webook/types.ts +++ b/x-pack/plugins/cases/server/connectors/cases_webook/types.ts @@ -5,13 +5,8 @@ * 2.0. */ -import { CasesWebhookFieldsType } from '../../../common/api'; import { ICasesConnector } from '../types'; -interface ExternalServiceFormatterParams extends CasesWebhookFieldsType { - labels: string[]; -} - -export type CasesWebhookCaseConnector = ICasesConnector; -export type Format = ICasesConnector['format']; -export type GetMapping = ICasesConnector['getMapping']; +export type CasesWebhookCaseConnector = ICasesConnector; +export type Format = ICasesConnector['format']; +export type GetMapping = ICasesConnector['getMapping']; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx index 069facbf18f847..4673a86f5fcfeb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx @@ -35,15 +35,15 @@ export function getActionType(): ActionTypeModel< ): Promise> => { const translations = await import('./translations'); const errors = { - 'subActionParams.incident.summary': new Array(), + 'subActionParams.incident.title': new Array(), }; const validationResult = { errors }; if ( actionParams.subActionParams && actionParams.subActionParams.incident && - !actionParams.subActionParams.incident.summary?.length + !actionParams.subActionParams.incident.title?.length ) { - errors['subActionParams.incident.summary'].push(translations.SUMMARY_REQUIRED); + errors['subActionParams.incident.title'].push(translations.SUMMARY_REQUIRED); } return validationResult; }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_params.test.tsx index 630e910fc832cf..91adb9616c4ab9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_params.test.tsx @@ -27,9 +27,9 @@ const actionParams = { subAction: 'pushToService', subActionParams: { incident: { - summary: 'sn title', + title: 'sn title', description: 'some description', - labels: ['kibana'], + tags: ['kibana'], externalId: null, }, comments: [], @@ -61,7 +61,7 @@ describe('WebhookParamsFields renders', () => { ]} /> ); - expect(wrapper.find('[data-test-subj="summaryInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="titleInput"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="descriptionTextArea"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="tagsComboBox"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="commentsTextArea"]').length > 0).toBeTruthy(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_params.tsx index d3c53d9d57be7e..2c2a876ec12733 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_params.tsx @@ -36,8 +36,8 @@ const WebhookParamsFields: React.FunctionComponent (incident.labels ? incident.labels.map((label: string) => ({ label })) : []), - [incident.labels] + () => (incident.tags ? incident.tags.map((label: string) => ({ label })) : []), + [incident.tags] ); const editSubActionProperty = useCallback( (key: string, value: any) => { @@ -85,16 +85,16 @@ const WebhookParamsFields: React.FunctionComponent 0 && - incident.summary !== undefined + errors['subActionParams.incident.title'] !== undefined && + errors['subActionParams.incident.title'].length > 0 && + incident.title !== undefined } label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhook.summaryFieldLabel', + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhook.titleFieldLabel', { defaultMessage: 'Summary (required)', } @@ -104,9 +104,9 @@ const WebhookParamsFields: React.FunctionComponent { const newOptions = [...labelOptions, { label: searchValue }]; editSubActionProperty( - 'labels', + 'tags', newOptions.map((newOption) => newOption.label) ); }} onChange={(selectedOptions: Array<{ label: string }>) => { editSubActionProperty( - 'labels', + 'tags', selectedOptions.map((selectedOption) => selectedOption.label) ); }} onBlur={() => { - if (!incident.labels) { - editSubActionProperty('labels', []); + if (!incident.tags) { + editSubActionProperty('tags', []); } }} isClearable={true} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/json_field_wrapper.scss b/x-pack/plugins/triggers_actions_ui/public/application/components/json_field_wrapper.scss deleted file mode 100644 index 91b6c802ec105d..00000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/json_field_wrapper.scss +++ /dev/null @@ -1,3 +0,0 @@ -.euiFormRow__fieldWrapper .kibanaCodeEditor { - height: auto; -} diff --git a/x-pack/plugins/cases/common/api/connectors/cases_webhook.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/json_field_wrapper.styles.ts similarity index 51% rename from x-pack/plugins/cases/common/api/connectors/cases_webhook.ts rename to x-pack/plugins/triggers_actions_ui/public/application/components/json_field_wrapper.styles.ts index 07db50f282e608..4ae891adcd3ed7 100644 --- a/x-pack/plugins/cases/common/api/connectors/cases_webhook.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/json_field_wrapper.styles.ts @@ -5,11 +5,12 @@ * 2.0. */ -import * as rt from 'io-ts'; +import { css } from '@emotion/react'; -export const CasesWebhookFieldsRT = rt.record( - rt.string, - rt.union([rt.string, rt.array(rt.string), rt.undefined, rt.null]) -); - -export type CasesWebhookFieldsType = rt.TypeOf; +export const styles = { + editor: css` + .euiFormRow__fieldWrapper .kibanaCodeEditor { + height: auto; + } + `, +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/json_field_wrapper.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/json_field_wrapper.tsx index 3764dea13de620..223aee0648efac 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/json_field_wrapper.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/json_field_wrapper.tsx @@ -11,8 +11,8 @@ import { getFieldValidityAndErrorMessage, } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import React, { useCallback } from 'react'; -import './json_field_wrapper.scss'; import { ActionVariable } from '@kbn/alerting-plugin/common'; +import { styles } from './json_field_wrapper.styles'; import { JsonEditorWithMessageVariables } from './json_editor_with_message_variables'; interface Props { @@ -36,18 +36,20 @@ export const JsonFieldWrapper = ({ field, ...rest }: Props) => { ); return ( - {helpText}

} - inputTargetValue={value} - label={ - label ?? - i18n.translate('xpack.triggersActionsUI.jsonFieldWrapper.defaultLabel', { - defaultMessage: 'JSON Editor', - }) - } - onDocumentsChange={onJsonUpdate} - {...rest} - /> + + {helpText}

} + inputTargetValue={value} + label={ + label ?? + i18n.translate('xpack.triggersActionsUI.jsonFieldWrapper.defaultLabel', { + defaultMessage: 'JSON Editor', + }) + } + onDocumentsChange={onJsonUpdate} + {...rest} + /> +
); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/mustache_text_field_wrapper.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/mustache_text_field_wrapper.tsx index e1bdb26fca7229..b568e06179230c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/mustache_text_field_wrapper.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/mustache_text_field_wrapper.tsx @@ -10,7 +10,6 @@ import { getFieldValidityAndErrorMessage, } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import React, { useCallback } from 'react'; -import './json_field_wrapper.scss'; import { ActionVariable } from '@kbn/alerting-plugin/common'; import { TextFieldWithMessageVariables } from './text_field_with_message_variables'; From 0902c4947e04de0cc73b80484f85d6b7bc811445 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 19 Jul 2022 10:42:52 -0600 Subject: [PATCH 60/97] fix type --- .../tests/common/configure/post_configure.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/configure/post_configure.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/configure/post_configure.ts index 4c7f9f344096e8..47744830ec8b85 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/configure/post_configure.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/configure/post_configure.ts @@ -284,11 +284,11 @@ export default ({ getService }: FtrProviderContext): void => { await createConfiguration( supertest, { - // @ts-expect-error connector: { id: 'test-id', type: ConnectorTypes.none, name: 'Connector', + // @ts-expect-error fields: {}, }, closure_type: 'close-by-user', From 7d322a0e0a083700f5e6fde4a95a3f38bbed3661 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 19 Jul 2022 14:30:08 -0600 Subject: [PATCH 61/97] pr changes 4 --- .../cases_webhook/service.ts | 10 +-- .../cases_webhook/utils.test.ts | 20 +++--- .../cases_webhook/utils.ts | 12 +--- .../cases_webhook/validator.ts | 42 ++++-------- .../cases_webhook/webhook.test.tsx | 8 +-- .../cases_webhook/webhook_params.tsx | 58 +++++++++++++---- .../json_editor_with_message_variables.tsx | 2 +- .../mustache_text_field_wrapper.tsx | 13 ++-- .../text_area_with_message_variables.tsx | 65 +++++-------------- 9 files changed, 102 insertions(+), 128 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts index c7072669688ad4..0644991a5a93fd 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts @@ -16,7 +16,7 @@ import { getPushedDate, stringifyObjValues, removeSlash, - throwIfResponseIsNotValidSpecial, + throwDescriptiveErrorIfResponseIsNotValid, } from './utils'; import { CreateIncidentParams, @@ -97,7 +97,7 @@ export const createExternalService = ( configurationUtilities, }); - throwIfResponseIsNotValidSpecial({ + throwDescriptiveErrorIfResponseIsNotValid({ res, requiredAttributesToBeInTheResponse: [ getIncidentResponseCreatedDateKey, @@ -138,7 +138,7 @@ export const createExternalService = ( const { status, statusText, data } = res; - throwIfResponseIsNotValidSpecial({ + throwDescriptiveErrorIfResponseIsNotValid({ res, requiredAttributesToBeInTheResponse: [createIncidentResponseKey], }); @@ -193,7 +193,7 @@ export const createExternalService = ( configurationUtilities, }); - throwIfResponseIsNotValidSpecial({ + throwDescriptiveErrorIfResponseIsNotValid({ res, }); @@ -240,7 +240,7 @@ export const createExternalService = ( configurationUtilities, }); - throwIfResponseIsNotValidSpecial({ + throwDescriptiveErrorIfResponseIsNotValid({ res, }); } catch (error) { diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.test.ts index eb8539bd677939..72a4bc64663907 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.test.ts @@ -5,7 +5,11 @@ * 2.0. */ -import { getObjectValueByKey, stringifyObjValues, throwIfResponseIsNotValidSpecial } from './utils'; +import { + getObjectValueByKey, + stringifyObjValues, + throwDescriptiveErrorIfResponseIsNotValid, +} from './utils'; const bigOlObject = { fields: { @@ -49,7 +53,7 @@ describe('cases_webhook/utils', () => { expect(getObjectValueByKey(bigOlObject, 'bad.key')).toEqual(undefined); }); }); - describe('throwIfResponseIsNotValidSpecial()', () => { + describe('throwDescriptiveErrorIfResponseIsNotValid()', () => { const res = { data: bigOlObject, headers: {}, @@ -62,7 +66,7 @@ describe('cases_webhook/utils', () => { }; it('Throws error when missing content-type', () => { expect(() => - throwIfResponseIsNotValidSpecial({ + throwDescriptiveErrorIfResponseIsNotValid({ res, requiredAttributesToBeInTheResponse: ['field.simple'], }) @@ -72,7 +76,7 @@ describe('cases_webhook/utils', () => { }); it('Throws error when content-type is not valid', () => { expect(() => - throwIfResponseIsNotValidSpecial({ + throwDescriptiveErrorIfResponseIsNotValid({ res: { ...res, headers: { @@ -87,7 +91,7 @@ describe('cases_webhook/utils', () => { }); it('Throws error when for bad data', () => { expect(() => - throwIfResponseIsNotValidSpecial({ + throwDescriptiveErrorIfResponseIsNotValid({ res: { ...res, headers: { @@ -101,7 +105,7 @@ describe('cases_webhook/utils', () => { }); it('Throws for bad key', () => { expect(() => - throwIfResponseIsNotValidSpecial({ + throwDescriptiveErrorIfResponseIsNotValid({ res: { ...res, headers: { @@ -114,7 +118,7 @@ describe('cases_webhook/utils', () => { }); it('Throws for multiple bad keys', () => { expect(() => - throwIfResponseIsNotValidSpecial({ + throwDescriptiveErrorIfResponseIsNotValid({ res: { ...res, headers: { @@ -127,7 +131,7 @@ describe('cases_webhook/utils', () => { }); it('Does not throw for valid key', () => { expect(() => - throwIfResponseIsNotValidSpecial({ + throwDescriptiveErrorIfResponseIsNotValid({ res: { ...res, headers: { diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts index 51b03804d3f76a..644a7a7d778e2d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts @@ -35,11 +35,10 @@ export const getObjectValueByKey = ( obj: Record | T>, key: string ): T => { - // @ts-ignore - return get(obj, key); + return get(obj, key) as T; }; -export const throwIfResponseIsNotValidSpecial = ({ +export const throwDescriptiveErrorIfResponseIsNotValid = ({ res, requiredAttributesToBeInTheResponse = [], }: { @@ -65,13 +64,6 @@ export const throwIfResponseIsNotValidSpecial = ({ ); } - /** - * Check if the response is a JS object (data != null && typeof data === 'object') - * in case the content type is application/json but for some reason the response is not. - * Empty responses (204 No content) are ignored because the typeof data will be string and - * isObjectLike will fail. - * Axios converts automatically JSON to JS objects. - */ if (isEmpty(data) || !isObjectLike(data)) { throw new Error('Response is not a valid JSON'); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/validator.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/validator.ts index a06aa61a0f1936..7c1c336b403b33 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/validator.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/validator.ts @@ -17,7 +17,7 @@ import { templateActionVariable } from '../../../lib'; const errorCode: ERROR_CODE = 'ERR_FIELD_MISSING'; -const missingVariable = (path: string, variables: string[]) => ({ +const missingVariableErrorMessage = (path: string, variables: string[]) => ({ code: errorCode, path, message: i18n.MISSING_VARIABLES(variables), @@ -33,26 +33,13 @@ export const containsTitleAndDesc = const description = templateActionVariable( casesVars.find((actionVariable) => actionVariable.name === 'case.description')! ); - let error; - if (typeof value === 'string') { - // will always be true, but this keeps my doesContain var contained! - if (!error) { - const { doesContain } = containsChars(title)(value); - if (!doesContain) { - error = missingVariable(path, [title]); - } - } - if (!error) { - const { doesContain } = containsChars(description)(value); - error = !doesContain ? missingVariable(path, [description]) : undefined; - } else { - const { doesContain } = containsChars(description)(value); - error = !doesContain - ? missingVariable(path, [title, description]) - : missingVariable(path, [title]); - } + const varsWithErrors = [title, description].filter( + (variable) => !containsChars(variable)(value as string).doesContain + ); + + if (varsWithErrors.length > 0) { + return missingVariableErrorMessage(path, varsWithErrors); } - return error; }; export const containsExternalId = @@ -63,14 +50,9 @@ export const containsExternalId = const id = templateActionVariable( urlVars.find((actionVariable) => actionVariable.name === 'external.system.id')! ); - let error; - if (typeof value === 'string') { - const { doesContain } = containsChars(id)(value); - if (!doesContain) { - error = missingVariable(path, [id]); - } - } - return error; + return containsChars(id)(value as string).doesContain + ? undefined + : missingVariableErrorMessage(path, [id]); }; export const containsExternalIdOrTitle = @@ -84,7 +66,7 @@ export const containsExternalIdOrTitle = const title = templateActionVariable( urlVarsExt.find((actionVariable) => actionVariable.name === 'external.system.title')! ); - const error = missingVariable(path, [id, title]); + const error = missingVariableErrorMessage(path, [id, title]); if (typeof value === 'string') { const { doesContain: doesContainId } = containsChars(id)(value); const { doesContain: doesContainTitle } = containsChars(title)(value); @@ -134,7 +116,7 @@ export const containsCommentsOrEmpty = if (typeof value === 'string') { const { doesContain } = containsChars(comment)(value); if (!doesContain) { - error = missingVariable(path, [comment]); + error = missingVariableErrorMessage(path, [comment]); } } return error; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.test.tsx index 86cfe366f9751a..4e72d958c503e1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.test.tsx @@ -32,22 +32,22 @@ describe('actionTypeRegistry.get() works', () => { describe('webhook action params validation', () => { test('action params validation succeeds when action params is valid', async () => { const actionParams = { - subActionParams: { incident: { summary: 'some title {{test}}' }, comments: [] }, + subActionParams: { incident: { title: 'some title {{test}}' }, comments: [] }, }; expect(await actionTypeModel.validateParams(actionParams)).toEqual({ - errors: { 'subActionParams.incident.summary': [] }, + errors: { 'subActionParams.incident.title': [] }, }); }); test('params validation fails when body is not valid', async () => { const actionParams = { - subActionParams: { incident: { summary: '' }, comments: [] }, + subActionParams: { incident: { title: '' }, comments: [] }, }; expect(await actionTypeModel.validateParams(actionParams)).toEqual({ errors: { - 'subActionParams.incident.summary': ['Summary is required.'], + 'subActionParams.incident.title': ['Summary is required.'], }, }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_params.tsx index 2c2a876ec12733..2d1f8b03bd08fd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_params.tsx @@ -7,12 +7,27 @@ import React, { useCallback, useEffect, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiComboBox, EuiFormRow } from '@elastic/eui'; +import { EuiCallOut, EuiComboBox, EuiFormRow, EuiSpacer } from '@elastic/eui'; import { ActionParamsProps } from '../../../../types'; import { CasesWebhookActionConnector, CasesWebhookActionParams } from './types'; import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables'; import { TextFieldWithMessageVariables } from '../../text_field_with_message_variables'; +const CREATE_COMMENT_WARNING_TITLE = i18n.translate( + 'xpack.triggersActionsUI.components.textAreaWithMessageVariable.createCommentWarningTitle', + { + defaultMessage: 'Unable to share case comments', + } +); + +const CREATE_COMMENT_WARNING_DESC = i18n.translate( + 'xpack.triggersActionsUI.components.textAreaWithMessageVariable.createCommentWarningDesc', + { + defaultMessage: + 'Configure the Create Comment URL and Create Comment Objects fields for the connector to share comments externally.', + } +); + const WebhookParamsFields: React.FunctionComponent> = ({ actionConnector, actionParams, @@ -158,20 +173,35 @@ const WebhookParamsFields: React.FunctionComponent - 0 ? comments[0].comment : undefined} - label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhook.commentsTextAreaFieldLabel', - { - defaultMessage: 'Additional comments', - } + <> + 0 ? comments[0].comment : undefined} + label={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhook.commentsTextAreaFieldLabel', + { + defaultMessage: 'Additional comments', + } + )} + /> + {(!createCommentUrl || !createCommentJson) && ( + <> + + +

{CREATE_COMMENT_WARNING_DESC}

+
+ )} - /> + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.tsx index 5bdc25fd48752f..135d4783da8228 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.tsx @@ -185,13 +185,13 @@ export const JsonEditorWithMessageVariables: React.FunctionComponent = ({ height="200px" data-test-subj={`${paramsProperty}JsonEditor`} aria-label={areaLabel} + {...euiCodeEditorProps} editorDidMount={onEditorMount} onChange={(xjson: string) => { setXJson(xjson); // Keep the documents in sync with the editor content onDocumentsChange(convertToJson(xjson)); }} - {...euiCodeEditorProps} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/mustache_text_field_wrapper.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/mustache_text_field_wrapper.tsx index b568e06179230c..c5f5f3d5259968 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/mustache_text_field_wrapper.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/mustache_text_field_wrapper.tsx @@ -24,23 +24,17 @@ interface Props { export const MustacheTextFieldWrapper = ({ field, euiFieldProps, idAria, ...rest }: Props) => { const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); - const { value, onChange } = field; + const { value, setValue } = field; const editAction = useCallback( (property: string, newValue: string) => { - onChange({ - // @ts-ignore we don't have to send the whole type - target: { - value: newValue, - }, - }); + setValue(newValue); }, - [onChange] + [setValue] ); return ( ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx index 528e19981e3ebd..3bb65037d5c93f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx @@ -6,26 +6,12 @@ */ import React, { useState } from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiCallOut, EuiTextArea, EuiFormRow, EuiSpacer } from '@elastic/eui'; +import { EuiTextArea, EuiFormRow } from '@elastic/eui'; import './add_message_variables.scss'; import { ActionVariable } from '@kbn/alerting-plugin/common'; import { AddMessageVariables } from './add_message_variables'; import { templateActionVariable } from '../lib'; -const CREATE_COMMENT_WARNING_TITLE = i18n.translate( - 'xpack.triggersActionsUI.components.textAreaWithMessageVariable.createCommentWarningTitle', - { - defaultMessage: 'Unable to share case comments', - } -); -const CREATE_COMMENT_WARNING_DESC = i18n.translate( - 'xpack.triggersActionsUI.components.textAreaWithMessageVariable.createCommentWarningDesc', - { - defaultMessage: - 'Configure the Create Comment URL and Create Comment Objects fields for the connector to share comments externally.', - } -); interface Props { messageVariables?: ActionVariable[]; paramsProperty: string; @@ -79,38 +65,23 @@ export const TextAreaWithMessageVariables: React.FunctionComponent = ({ /> } > - <> - {isDisabled && paramsProperty === 'comments' && ( - <> - -

{CREATE_COMMENT_WARNING_DESC}

-
- - - )} - 0 && inputTargetValue !== undefined} - name={paramsProperty} - value={inputTargetValue || ''} - data-test-subj={`${paramsProperty}TextArea`} - onChange={(e: React.ChangeEvent) => onChangeWithMessageVariable(e)} - onFocus={(e: React.FocusEvent) => { - setCurrentTextElement(e.target); - }} - onBlur={() => { - if (!inputTargetValue) { - editAction(paramsProperty, '', index); - } - }} - /> - + 0 && inputTargetValue !== undefined} + name={paramsProperty} + value={inputTargetValue || ''} + data-test-subj={`${paramsProperty}TextArea`} + onChange={(e: React.ChangeEvent) => onChangeWithMessageVariable(e)} + onFocus={(e: React.FocusEvent) => { + setCurrentTextElement(e.target); + }} + onBlur={() => { + if (!inputTargetValue) { + editAction(paramsProperty, '', index); + } + }} + /> ); }; From 18db51d5c217a41849ea041625feb7ff2f0fbac7 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 20 Jul 2022 08:47:00 -0600 Subject: [PATCH 62/97] remove only --- .../server/builtin_action_types/cases_webhook/service.test.ts | 2 +- .../server/builtin_action_types/cases_webhook/validators.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts index 0ba198135e681c..7b9bdd5c89b726 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts @@ -137,7 +137,7 @@ describe('Cases webhook service', () => { }, }; - test.only('it returns the incident correctly', async () => { + test('it returns the incident correctly', async () => { requestMock.mockImplementation(() => createAxiosResponse(axiosRes)); const res = await service.getIncident('1'); expect(res).toEqual({ diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts index 8e756a0e1c61c5..bb5768ffc680c0 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts @@ -41,6 +41,7 @@ const validateConfig = ( return i18n.INVALID_URL(err, url); } try { + console.log('ensure allowed', url, configurationUtilities.ensureUriAllowed(url)); configurationUtilities.ensureUriAllowed(url); } catch (allowListError) { return i18n.CONFIG_ERR(allowListError.message); From 4ec23a431f200690a454128a81757148e5e306b0 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 20 Jul 2022 08:57:45 -0600 Subject: [PATCH 63/97] fix func test --- .../builtin_action_types/cases_webhook/validators.ts | 3 +-- .../actions/builtin_action_types/cases_webhook.ts | 10 +++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts index bb5768ffc680c0..f49f96d9bf463c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts @@ -26,8 +26,8 @@ const validateConfig = ( } = configObject; const urls = [ - createCommentUrl, createIncidentUrl, + createCommentUrl, incidentViewUrl, getIncidentUrl, updateIncidentUrl, @@ -41,7 +41,6 @@ const validateConfig = ( return i18n.INVALID_URL(err, url); } try { - console.log('ensure allowed', url, configurationUtilities.ensureUriAllowed(url)); configurationUtilities.ensureUriAllowed(url); } catch (allowListError) { return i18n.CONFIG_ERR(allowListError.message); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/cases_webhook.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/cases_webhook.ts index 40bbfcd7090f32..46510e8c1b4d57 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/cases_webhook.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/cases_webhook.ts @@ -67,7 +67,7 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { subAction: 'pushToService', subActionParams: { incident: { - summary: 'a title', + title: 'a title', description: 'a description', externalId: null, }, @@ -284,7 +284,7 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [subActionParams.incident.summary]: expected value of type [string] but got [undefined]', + 'error validating action params: [subActionParams.incident.title]: expected value of type [string] but got [undefined]', }); }); }); @@ -310,7 +310,7 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: [subActionParams.incident.summary]: expected value of type [string] but got [undefined]', + 'error validating action params: [subActionParams.incident.title]: expected value of type [string] but got [undefined]', }); }); }); @@ -326,7 +326,7 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { incident: { ...mockCasesWebhook.params.subActionParams.incident, description: 'success', - summary: 'success', + title: 'success', }, comments: [{ comment: 'comment' }], }, @@ -353,7 +353,7 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { subActionParams: { incident: { ...mockCasesWebhook.params.subActionParams.incident, - summary: 'success', + title: 'success', }, comments: [{ commentId: 'success' }], }, From b0d344faccf07b37b5e9dbcd460ef3d1a0d1f48c Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 20 Jul 2022 10:22:33 -0600 Subject: [PATCH 64/97] fix --- .../server/builtin_action_types/cases_webhook/utils.test.ts | 4 ++-- .../server/builtin_action_types/cases_webhook/utils.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.test.ts index 72a4bc64663907..a388c21eaf9abb 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.test.ts @@ -89,7 +89,7 @@ describe('cases_webhook/utils', () => { 'Unsupported content type: not/cool in post https://poster.com. Supported content types: application/json' ); }); - it('Throws error when for bad data', () => { + it('Throws error for bad data', () => { expect(() => throwDescriptiveErrorIfResponseIsNotValid({ res: { @@ -97,7 +97,7 @@ describe('cases_webhook/utils', () => { headers: { ['content-type']: 'application/json', }, - data: '', + data: 'bad', }, requiredAttributesToBeInTheResponse: ['field.simple'], }) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts index 644a7a7d778e2d..b26071a923ea2f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts @@ -64,7 +64,7 @@ export const throwDescriptiveErrorIfResponseIsNotValid = ({ ); } - if (isEmpty(data) || !isObjectLike(data)) { + if (!isEmpty(data) && !isObjectLike(data)) { throw new Error('Response is not a valid JSON'); } From bee21e788ed623e5e6a78ec6277a16ff8e64b368 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 20 Jul 2022 10:39:45 -0600 Subject: [PATCH 65/97] incident > case --- .../cases_webhook/index.ts | 2 +- .../cases_webhook/service.test.ts | 28 +++++----- .../cases_webhook/service.ts | 8 +-- .../cases_webhook/translations.ts | 52 +++++++++---------- .../cases_webhook/webhook.test.tsx | 2 +- 5 files changed, 43 insertions(+), 49 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts index a6350ffb447819..4b8f7615ad325f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts @@ -107,7 +107,7 @@ export async function executor( logger, }); - logger.debug(`response push to service for incident id: ${data.id}`); + logger.debug(`response push to service for case id: ${data.id}`); } return { status: 'ok', data, actionId }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts index 7b9bdd5c89b726..b97972f4c6f375 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts @@ -167,7 +167,7 @@ describe('Cases webhook service', () => { throw error; }); await expect(service.getIncident('1')).rejects.toThrow( - '[Action][Webhook - Case Management]: Unable to get incident. Error: An error has occurred. Reason: Required field' + '[Action][Webhook - Case Management]: Unable to get case with id 1. Error: An error has occurred. Reason: Required field' ); }); @@ -177,7 +177,7 @@ describe('Cases webhook service', () => { ); await expect(service.getIncident('1')).rejects.toThrow( - '[Action][Webhook - Case Management]: Unable to get incident. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json' + '[Action][Webhook - Case Management]: Unable to get case with id 1. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json' ); }); @@ -187,7 +187,7 @@ describe('Cases webhook service', () => { ); await expect(service.getIncident('1')).rejects.toThrow( - '[Action][Webhook - Case Management]: Unable to get incident. Error: Response is missing the expected fields: fields.created, key, fields.updated' + '[Action][Webhook - Case Management]: Unable to get case with id 1. Error: Response is missing the expected fields: fields.created, key, fields.updated' ); }); }); @@ -276,7 +276,7 @@ describe('Cases webhook service', () => { }); await expect(service.createIncident(incident)).rejects.toThrow( - '[Action][Webhook - Case Management]: Unable to create incident. Error: An error has occurred. Reason: Required field' + '[Action][Webhook - Case Management]: Unable to create case. Error: An error has occurred. Reason: Required field' ); }); @@ -286,7 +286,7 @@ describe('Cases webhook service', () => { ); await expect(service.createIncident(incident)).rejects.toThrow( - '[Action][Webhook - Case Management]: Unable to create incident. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json.' + '[Action][Webhook - Case Management]: Unable to create case. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json.' ); }); @@ -294,7 +294,7 @@ describe('Cases webhook service', () => { requestMock.mockImplementation(() => createAxiosResponse({ data: { notRequired: 'test' } })); await expect(service.createIncident(incident)).rejects.toThrow( - '[Action][Webhook - Case Management]: Unable to create incident. Error: Response is missing the expected field: id.' + '[Action][Webhook - Case Management]: Unable to create case. Error: Response is missing the expected field: id.' ); }); }); @@ -369,7 +369,7 @@ describe('Cases webhook service', () => { }); await expect(service.updateIncident(incident)).rejects.toThrow( - '[Action][Webhook - Case Management]: Unable to update incident with id 1. Error: An error has occurred. Reason: Required field' + '[Action][Webhook - Case Management]: Unable to update case with id 1. Error: An error has occurred. Reason: Required field' ); }); @@ -379,7 +379,7 @@ describe('Cases webhook service', () => { ); await expect(service.updateIncident(incident)).rejects.toThrow( - '[Action][Webhook - Case Management]: Unable to update incident with id 1. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json.' + '[Action][Webhook - Case Management]: Unable to update case with id 1. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json.' ); }); }); @@ -403,13 +403,9 @@ describe('Cases webhook service', () => { }) ); - const res = await service.createComment(commentReq); + await service.createComment(commentReq); - expect(res).toEqual({ - id: '1', - key: 'CK-1', - created: '2020-04-27T10:59:46.202Z', - }); + expect(requestMock.mock.calls[0][0].data).toEqual('{"body":"comment"}'); }); test('it should call request with correct arguments', async () => { @@ -443,7 +439,7 @@ describe('Cases webhook service', () => { }); await expect(service.createComment(commentReq)).rejects.toThrow( - '[Action][Webhook - Case Management]: Unable to create comment at incident with id 1. Error: An error has occurred. Reason: Required field' + '[Action][Webhook - Case Management]: Unable to create comment at case with id 1. Error: An error has occurred. Reason: Required field' ); }); @@ -453,7 +449,7 @@ describe('Cases webhook service', () => { ); await expect(service.createComment(commentReq)).rejects.toThrow( - '[Action][Webhook - Case Management]: Unable to create comment at incident with id 1. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json.' + '[Action][Webhook - Case Management]: Unable to create comment at case with id 1. Error: Unsupported content type: text/html in GET https://example.com. Supported content types: application/json.' ); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts index 0644991a5a93fd..ab8c5111e08b9f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts @@ -111,7 +111,7 @@ export const createExternalService = ( const updatedAt = getObjectValueByKey(res.data, getIncidentResponseUpdatedDateKey); return { id, title, createdAt, updatedAt }; } catch (error) { - throw createServiceError(error, `Unable to get incident with id ${id}`); + throw createServiceError(error, `Unable to get case with id ${id}`); } }; @@ -161,7 +161,7 @@ export const createExternalService = ( pushedDate: getPushedDate(insertedIncident.createdAt), }; } catch (error) { - throw createServiceError(error, 'Unable to create incident'); + throw createServiceError(error, 'Unable to create case'); } }; @@ -213,7 +213,7 @@ export const createExternalService = ( pushedDate: getPushedDate(updatedIncident.updatedAt), }; } catch (error) { - throw createServiceError(error, `Unable to update incident with id ${incidentId}`); + throw createServiceError(error, `Unable to update case with id ${incidentId}`); } }; @@ -244,7 +244,7 @@ export const createExternalService = ( res, }); } catch (error) { - throw createServiceError(error, `Unable to create comment at incident with id ${incidentId}`); + throw createServiceError(error, `Unable to create comment at case with id ${incidentId}`); } }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts index 4c011189770741..2393823c81be36 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts @@ -10,47 +10,47 @@ import { i18n } from '@kbn/i18n'; export const CREATE_URL_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.error.requiredCreateUrlText', { - defaultMessage: 'Create incident URL is required.', + defaultMessage: 'Create case URL is required.', } ); export const CREATE_INCIDENT_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.error.requiredCreateIncidentText', { - defaultMessage: 'Create incident object is required and must be valid JSON.', + defaultMessage: 'Create case object is required and must be valid JSON.', } ); export const CREATE_METHOD_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.sections.addAction.casesWebhookAction.error.requiredCreateMethodText', { - defaultMessage: 'Create incident method is required.', + defaultMessage: 'Create case method is required.', } ); export const CREATE_RESPONSE_KEY_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.sections.addAction.casesWebhookAction.error.requiredCreateIncidentResponseKeyText', { - defaultMessage: 'Create incident response incident key is required.', + defaultMessage: 'Create case response case id key is required.', } ); export const UPDATE_URL_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.error.requiredUpdateUrlText', { - defaultMessage: 'Update incident URL is required.', + defaultMessage: 'Update case URL is required.', } ); export const UPDATE_INCIDENT_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.error.requiredUpdateIncidentText', { - defaultMessage: 'Update incident object is required and must be valid JSON.', + defaultMessage: 'Update case object is required and must be valid JSON.', } ); export const UPDATE_METHOD_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.sections.addAction.casesWebhookAction.error.requiredUpdateMethodText', { - defaultMessage: 'Update incident method is required.', + defaultMessage: 'Update case method is required.', } ); @@ -77,31 +77,31 @@ export const CREATE_COMMENT_METHOD_REQUIRED = i18n.translate( export const GET_INCIDENT_URL_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.error.requiredGetIncidentUrlText', { - defaultMessage: 'Get incident URL is required.', + defaultMessage: 'Get case URL is required.', } ); export const GET_RESPONSE_EXTERNAL_TITLE_KEY_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.sections.addAction.casesWebhookAction.error.requiredGetIncidentResponseExternalTitleKeyText', { - defaultMessage: 'Get incident response external incident title key is re quired.', + defaultMessage: 'Get case response external case title key is re quired.', } ); export const GET_RESPONSE_EXTERNAL_CREATED_KEY_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.sections.addAction.casesWebhookAction.error.requiredGetIncidentResponseCreatedKeyText', { - defaultMessage: 'Get incident response created date key is required.', + defaultMessage: 'Get case response created date key is required.', } ); export const GET_RESPONSE_EXTERNAL_UPDATED_KEY_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.sections.addAction.casesWebhookAction.error.requiredGetIncidentResponseUpdatedKeyText', { - defaultMessage: 'Get incident response updated date key is required.', + defaultMessage: 'Get case response updated date key is required.', } ); export const GET_INCIDENT_VIEW_URL_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.sections.addAction.casesWebhookAction.error.requiredGetIncidentViewUrlKeyText', { - defaultMessage: 'View incident URL is required.', + defaultMessage: 'View case URL is required.', } ); @@ -125,7 +125,7 @@ export const USERNAME_REQUIRED = i18n.translate( export const SUMMARY_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredWebhookSummaryText', { - defaultMessage: 'Summary is required.', + defaultMessage: 'Title is required.', } ); @@ -183,7 +183,7 @@ export const CREATE_INCIDENT_JSON_HELP = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.createIncidentJsonHelpText', { defaultMessage: - 'JSON object to create incident. Use the variable selector to add Cases data to the payload.', + 'JSON object to create case. Use the variable selector to add Cases data to the payload.', } ); @@ -210,7 +210,7 @@ export const CREATE_INCIDENT_RESPONSE_KEY = i18n.translate( export const CREATE_INCIDENT_RESPONSE_KEY_HELP = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.createIncidentResponseKeyHelpText', { - defaultMessage: 'JSON key in create incident response that contains the external incident id', + defaultMessage: 'JSON key in create case response that contains the external case id', } ); @@ -231,7 +231,7 @@ export const GET_INCIDENT_URL_HELP = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.getIncidentUrlHelp', { defaultMessage: - 'API URL to GET incident details JSON from external system. Use the variable selector to add external system id to the url.', + 'API URL to GET case details JSON from external system. Use the variable selector to add external system id to the url.', } ); @@ -244,7 +244,7 @@ export const GET_INCIDENT_TITLE_KEY = i18n.translate( export const GET_INCIDENT_TITLE_KEY_HELP = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.getIncidentResponseExternalTitleKeyHelp', { - defaultMessage: 'JSON key in get incident response that contains the external incident title', + defaultMessage: 'JSON key in get case response that contains the external case title', } ); @@ -257,8 +257,7 @@ export const GET_INCIDENT_CREATED_KEY = i18n.translate( export const GET_INCIDENT_CREATED_KEY_HELP = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.getIncidentResponseCreatedDateKeyHelp', { - defaultMessage: - 'JSON key in get incident response that contains the date the incident was created.', + defaultMessage: 'JSON key in get case response that contains the date the case was created.', } ); @@ -271,8 +270,7 @@ export const GET_INCIDENT_UPDATED_KEY = i18n.translate( export const GET_INCIDENT_UPDATED_KEY_HELP = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.getIncidentResponseUpdatedDateKeyHelp', { - defaultMessage: - 'JSON key in get incident response that contains the date the incident was updated.', + defaultMessage: 'JSON key in get case response that contains the date the case was updated.', } ); @@ -286,7 +284,7 @@ export const EXTERNAL_INCIDENT_VIEW_URL_HELP = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.incidentViewUrlHelp', { defaultMessage: - 'URL to view incident in external system. Use the variable selector to add external system id or external system title to the url.', + 'URL to view case in external system. Use the variable selector to add external system id or external system title to the url.', } ); @@ -307,7 +305,7 @@ export const UPDATE_INCIDENT_URL_HELP = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.updateIncidentUrlHelp', { defaultMessage: - 'API URL to update incident. Use the variable selector to add external system id to the url.', + 'API URL to update case. Use the variable selector to add external system id to the url.', } ); @@ -321,7 +319,7 @@ export const UPDATE_INCIDENT_JSON_HELP = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.updateIncidentJsonHelpl', { defaultMessage: - 'JSON object to update incident. Use the variable selector to add Cases data to the payload.', + 'JSON object to update case. Use the variable selector to add Cases data to the payload.', } ); @@ -342,7 +340,7 @@ export const CREATE_COMMENT_URL_HELP = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.createCommentUrlHelp', { defaultMessage: - 'API URL to add comment to incident. Use the variable selector to add external system id to the url.', + 'API URL to add comment to case. Use the variable selector to add external system id to the url.', } ); @@ -419,14 +417,14 @@ export const STEP_1 = i18n.translate( export const STEP_2 = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.step2', { - defaultMessage: 'Create incident', + defaultMessage: 'Create case', } ); export const STEP_3 = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.step3', { - defaultMessage: 'Get incident information', + defaultMessage: 'Get case information', } ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.test.tsx index 4e72d958c503e1..45169e0fcb0321 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.test.tsx @@ -47,7 +47,7 @@ describe('webhook action params validation', () => { expect(await actionTypeModel.validateParams(actionParams)).toEqual({ errors: { - 'subActionParams.incident.title': ['Summary is required.'], + 'subActionParams.incident.title': ['Title is required.'], }, }); }); From 81595e8a9b50cf5ae057a5ec48b67125cb14bb9a Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 21 Jul 2022 11:44:42 -0600 Subject: [PATCH 66/97] update allows external.service.id in json, not required in url --- .../cases_webhook/service.ts | 26 ++++++++++++------- .../cases_webhook/steps/update.tsx | 14 +++------- .../cases_webhook/validator.ts | 17 ------------ .../cases_webhook/webhook_connectors.test.tsx | 6 ++--- 4 files changed, 22 insertions(+), 41 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts index ab8c5111e08b9f..99d4e9f776c253 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts @@ -182,14 +182,18 @@ export const createExternalService = ( }, }), logger, - data: renderMustacheStringNoEscape( - updateIncidentJson, - stringifyObjValues({ + data: renderMustacheStringNoEscape(updateIncidentJson, { + ...stringifyObjValues({ ...(title ? { title } : {}), ...(description ? { description } : {}), ...(tags ? { tags } : {}), - }) - ), + }), + external: { + system: { + id: incidentId, + }, + }, + }), configurationUtilities, }); @@ -233,10 +237,14 @@ export const createExternalService = ( }, }), logger, - data: renderMustacheStringNoEscape( - createCommentJson, - stringifyObjValues({ comment: comment.comment }) - ), + data: renderMustacheStringNoEscape(createCommentJson, { + ...stringifyObjValues({ comment: comment.comment }), + external: { + system: { + id: incidentId, + }, + }, + }), configurationUtilities, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/update.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/update.tsx index 4fb1a219878691..442f07322eb444 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/update.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/update.tsx @@ -10,13 +10,7 @@ import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { FIELD_TYPES, UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { Field } from '@kbn/es-ui-shared-plugin/static/forms/components'; -import { - containsCommentsOrEmpty, - containsExternalId, - containsIdOrEmpty, - containsTitleAndDesc, - isUrlButCanBeEmpty, -} from '../validator'; +import { containsCommentsOrEmpty, containsTitleAndDesc, isUrlButCanBeEmpty } from '../validator'; import { MustacheTextFieldWrapper } from '../../../mustache_text_field_wrapper'; import { casesVars, commentVars, urlVars } from '../action_variables'; import { JsonFieldWrapper } from '../../../json_field_wrapper'; @@ -64,7 +58,6 @@ export const UpdateStep: FunctionComponent = ({ display, readOnly }) => ( { validator: urlField(i18n.UPDATE_URL_REQUIRED), }, - { validator: containsExternalId() }, ], helpText: i18n.UPDATE_INCIDENT_URL_HELP, }} @@ -106,7 +99,7 @@ export const UpdateStep: FunctionComponent = ({ display, readOnly }) => ( 'data-test-subj': 'webhookUpdateIncidentJson', ['aria-label']: i18n.CODE_EDITOR, }, - messageVariables: casesVars, + messageVariables: [...casesVars, ...urlVars], paramsProperty: 'updateIncidentJson', buttonTitle: i18n.ADD_CASES_VARIABLE, showButtonTitle: true, @@ -147,7 +140,6 @@ export const UpdateStep: FunctionComponent = ({ display, readOnly }) => ( { validator: isUrlButCanBeEmpty(i18n.CREATE_COMMENT_URL_REQUIRED), }, - { validator: containsIdOrEmpty(i18n.CREATE_COMMENT_URL_REQUIRED) }, ], helpText: i18n.CREATE_COMMENT_URL_HELP, }} @@ -186,7 +178,7 @@ export const UpdateStep: FunctionComponent = ({ display, readOnly }) => ( 'data-test-subj': 'webhookCreateCommentJson', ['aria-label']: i18n.CODE_EDITOR, }, - messageVariables: commentVars, + messageVariables: [...commentVars, ...urlVars], paramsProperty: 'createCommentJson', buttonTitle: i18n.ADD_CASES_VARIABLE, showButtonTitle: true, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/validator.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/validator.ts index 7c1c336b403b33..7f34f76807e556 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/validator.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/validator.ts @@ -77,23 +77,6 @@ export const containsExternalIdOrTitle = return error; }; -export const containsIdOrEmpty = - (message: string) => - (...args: Parameters): ReturnType> => { - const [{ value }] = args; - if (typeof value !== 'string') { - return { - code: 'ERR_FIELD_FORMAT', - formatType: 'STRING', - message, - }; - } - if (value.length === 0) { - return undefined; - } - return containsExternalId()(...args); - }; - export const containsCommentsOrEmpty = (message: string) => (...args: Parameters): ReturnType> => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx index db1f78ea1dd2ea..470848be77e2f6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx @@ -334,8 +334,8 @@ describe('CasesWebhookActionConnectorFields renders', () => { ['getIncidentResponseExternalTitleKeyText', ''], ['getIncidentResponseCreatedDateKeyText', ''], ['getIncidentResponseUpdatedDateKeyText', ''], - ['updateIncidentUrlInput', 'https://missingexternalid.com'], - ['createCommentUrlInput', 'https://missingexternalid.com'], + ['updateIncidentUrlInput', 'badurl.com'], + ['createCommentUrlInput', 'badurl.com'], ]; const mustacheTests: Array<[string, string, string[]]> = [ @@ -344,14 +344,12 @@ describe('CasesWebhookActionConnectorFields renders', () => { ['updateIncidentJson', invalidJsonTitle, ['{{{case.title}}}']], ['updateIncidentJson', invalidJsonBoth, ['{{{case.title}}}', '{{{case.description}}}']], ['createCommentJson', invalidJsonBoth, ['{{{case.comment}}}']], - ['updateIncidentUrl', 'https://missingexternalid.com', ['{{{external.system.id}}}']], [ 'incidentViewUrl', 'https://missingexternalid.com', ['{{{external.system.id}}}', '{{{external.system.title}}}'], ], ['getIncidentUrl', 'https://missingexternalid.com', ['{{{external.system.id}}}']], - ['createCommentUrl', 'https://missingexternalid.com', ['{{{external.system.id}}}']], ]; it('connector validation succeeds when connector config is valid', async () => { From ae487b178572edb01f0ce7d40526a89f40a10156 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Fri, 22 Jul 2022 08:21:31 -0600 Subject: [PATCH 67/97] response id to string --- .../server/builtin_action_types/cases_webhook/service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts index 99d4e9f776c253..757e59ed9609ae 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts @@ -142,7 +142,7 @@ export const createExternalService = ( res, requiredAttributesToBeInTheResponse: [createIncidentResponseKey], }); - const externalId = getObjectValueByKey(data, createIncidentResponseKey); + const externalId = `${getObjectValueByKey(data, createIncidentResponseKey)}`; const insertedIncident = await getIncident(externalId); logger.debug(`response from webhook action "${actionId}": [HTTP ${status}] ${statusText}`); From 050ea48dbc1b0889952183dfe9dd1e2a9ebe2d51 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Fri, 22 Jul 2022 08:36:54 -0600 Subject: [PATCH 68/97] fix user --- .../server/builtin_action_types/cases_webhook/schema.ts | 4 ++-- .../server/builtin_action_types/cases_webhook/validators.ts | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts index 01c5b558023cd0..c9a20ef820e138 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts @@ -61,8 +61,8 @@ export const ExternalIncidentServiceConfigurationSchema = schema.object( ); export const ExternalIncidentServiceSecretConfiguration = { - user: schema.string(), - password: schema.string(), + user: schema.nullable(schema.string()), + password: schema.nullable(schema.string()), }; export const ExternalIncidentServiceSecretConfigurationSchema = schema.object( diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts index f49f96d9bf463c..3c85ac43df2b5b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts @@ -50,6 +50,11 @@ const validateConfig = ( }; export const validateSecrets = (secrets: CasesWebhookSecretConfigurationType) => { + console.log('hey', { + first: !secrets.password && !secrets.user, + second: secrets.password && secrets.user, + secrets, + }); // user and password must be set together (or not at all) if (!secrets.password && !secrets.user) return; if (secrets.password && secrets.user) return; From c537288a8da69b51fa50b25fddb7401057a08f43 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Fri, 22 Jul 2022 09:17:51 -0600 Subject: [PATCH 69/97] getObjectValueByKeyAsString + url manipulation fix --- .../cases_webhook/service.ts | 111 +++++++++++------- .../cases_webhook/translations.ts | 8 ++ .../cases_webhook/utils.ts | 11 +- .../cases_webhook/validators.ts | 37 +++++- 4 files changed, 113 insertions(+), 54 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts index 757e59ed9609ae..24e10fd4222290 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts @@ -9,10 +9,11 @@ import axios, { AxiosResponse } from 'axios'; import { Logger } from '@kbn/core/server'; import { isString } from 'lodash'; +import { assertURL, ensureUriAllowed, normalizeURL } from './validators'; import { renderMustacheStringNoEscape } from '../../lib/mustache_renderer'; import { createServiceError, - getObjectValueByKey, + getObjectValueByKeyAsString, getPushedDate, stringifyObjValues, removeSlash, @@ -83,16 +84,20 @@ export const createExternalService = ( }); const getIncident = async (id: string): Promise => { + const getUrl = renderMustacheStringNoEscape(getIncidentUrl, { + external: { + system: { + id, + }, + }, + }); + assertURL(`${getUrl}`); + ensureUriAllowed(`${getUrl}`, configurationUtilities); + const normalizedUrl = normalizeURL(`${getUrl}`); try { const res = await request({ axios: axiosInstance, - url: renderMustacheStringNoEscape(getIncidentUrl, { - external: { - system: { - id, - }, - }, - }), + url: normalizedUrl, logger, configurationUtilities, }); @@ -106,9 +111,9 @@ export const createExternalService = ( ], }); - const title = getObjectValueByKey(res.data, getIncidentResponseExternalTitleKey); - const createdAt = getObjectValueByKey(res.data, getIncidentResponseCreatedDateKey); - const updatedAt = getObjectValueByKey(res.data, getIncidentResponseUpdatedDateKey); + const title = getObjectValueByKeyAsString(res.data, getIncidentResponseExternalTitleKey)!; + const createdAt = getObjectValueByKeyAsString(res.data, getIncidentResponseCreatedDateKey)!; + const updatedAt = getObjectValueByKeyAsString(res.data, getIncidentResponseUpdatedDateKey)!; return { id, title, createdAt, updatedAt }; } catch (error) { throw createServiceError(error, `Unable to get case with id ${id}`); @@ -119,10 +124,13 @@ export const createExternalService = ( incident, }: CreateIncidentParams): Promise => { const { tags, title, description } = incident; + assertURL(`${createIncidentUrl}`); + ensureUriAllowed(`${createIncidentUrl}`, configurationUtilities); + const normalizedUrl = normalizeURL(`${createIncidentUrl}`); try { const res: AxiosResponse = await request({ axios: axiosInstance, - url: `${createIncidentUrl}`, + url: normalizedUrl, logger, method: createIncidentMethod, data: renderMustacheStringNoEscape( @@ -142,22 +150,26 @@ export const createExternalService = ( res, requiredAttributesToBeInTheResponse: [createIncidentResponseKey], }); - const externalId = `${getObjectValueByKey(data, createIncidentResponseKey)}`; + const externalId = getObjectValueByKeyAsString(data, createIncidentResponseKey)!; const insertedIncident = await getIncident(externalId); logger.debug(`response from webhook action "${actionId}": [HTTP ${status}] ${statusText}`); + const viewUrl = renderMustacheStringNoEscape(incidentViewUrl, { + external: { + system: { + id: externalId, + title: insertedIncident.title, + }, + }, + }); + assertURL(`${viewUrl}`); + ensureUriAllowed(`${viewUrl}`, configurationUtilities); + const normalizedViewUrl = normalizeURL(`${viewUrl}`); return { id: externalId, title: insertedIncident.title, - url: renderMustacheStringNoEscape(incidentViewUrl, { - external: { - system: { - id: externalId, - title: insertedIncident.title, - }, - }, - }), + url: normalizedViewUrl, pushedDate: getPushedDate(insertedIncident.createdAt), }; } catch (error) { @@ -169,18 +181,22 @@ export const createExternalService = ( incidentId, incident, }: UpdateIncidentParams): Promise => { + const updateUrl = renderMustacheStringNoEscape(updateIncidentUrl, { + external: { + system: { + id: incidentId, + }, + }, + }); + assertURL(`${updateUrl}`); + ensureUriAllowed(`${updateUrl}`, configurationUtilities); + const normalizedUrl = normalizeURL(`${updateUrl}`); try { const { tags, title, description } = incident; const res = await request({ axios: axiosInstance, method: updateIncidentMethod, - url: renderMustacheStringNoEscape(updateIncidentUrl, { - external: { - system: { - id: incidentId, - }, - }, - }), + url: normalizedUrl, logger, data: renderMustacheStringNoEscape(updateIncidentJson, { ...stringifyObjValues({ @@ -202,18 +218,21 @@ export const createExternalService = ( }); const updatedIncident = await getIncident(incidentId as string); - + const viewUrl = renderMustacheStringNoEscape(incidentViewUrl, { + external: { + system: { + id: incidentId, + title: updatedIncident.title, + }, + }, + }); + assertURL(`${viewUrl}`); + ensureUriAllowed(`${viewUrl}`, configurationUtilities); + const normalizedViewUrl = normalizeURL(`${viewUrl}`); return { id: incidentId, title: updatedIncident.title, - url: renderMustacheStringNoEscape(incidentViewUrl, { - external: { - system: { - id: incidentId, - title: updatedIncident.title, - }, - }, - }), + url: normalizedViewUrl, pushedDate: getPushedDate(updatedIncident.updatedAt), }; } catch (error) { @@ -226,16 +245,20 @@ export const createExternalService = ( if (!createCommentUrl || !createCommentJson) { return {}; } + const commentUrl = renderMustacheStringNoEscape(createCommentUrl, { + external: { + system: { + id: incidentId, + }, + }, + }); + assertURL(`${commentUrl}`); + ensureUriAllowed(`${commentUrl}`, configurationUtilities); + const normalizedUrl = normalizeURL(`${commentUrl}`); const res = await request({ axios: axiosInstance, method: createCommentMethod, - url: renderMustacheStringNoEscape(createCommentUrl, { - external: { - system: { - id: incidentId, - }, - }, - }), + url: normalizedUrl, logger, data: renderMustacheStringNoEscape(createCommentJson, { ...stringifyObjValues({ comment: comment.comment }), diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/translations.ts index 4e2a76575432a0..06fd81491c0043 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/translations.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/translations.ts @@ -34,3 +34,11 @@ export const INVALID_USER_PW = i18n.translate( defaultMessage: 'both user and password must be specified', } ); + +export const ALLOWED_HOSTS_ERROR = (message: string) => + i18n.translate('xpack.actions.builtin.casesWebhook.configuration.apiAllowedHostsError', { + defaultMessage: 'error configuring connector action: {message}', + values: { + message, + }, + }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts index b26071a923ea2f..cedd9002774aed 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts @@ -31,11 +31,12 @@ export const getPushedDate = (timestamp?: string) => { return new Date().toISOString(); }; -export const getObjectValueByKey = ( - obj: Record | T>, +export const getObjectValueByKeyAsString = ( + obj: Record | unknown>, key: string -): T => { - return get(obj, key) as T; +): string | undefined => { + const value = get(obj, key); + return value === undefined ? value : `${value}`; }; export const throwDescriptiveErrorIfResponseIsNotValid = ({ @@ -83,7 +84,7 @@ export const throwDescriptiveErrorIfResponseIsNotValid = ({ */ requiredAttributesToBeInTheResponse.forEach((attr) => { // Check only for undefined as null is a valid value - if (typeof getObjectValueByKey(data, attr) === 'undefined') { + if (typeof getObjectValueByKeyAsString(data, attr) === 'undefined') { errorAttributes.push(attr); } }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts index 3c85ac43df2b5b..bc01bf01e7b40a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts @@ -50,11 +50,6 @@ const validateConfig = ( }; export const validateSecrets = (secrets: CasesWebhookSecretConfigurationType) => { - console.log('hey', { - first: !secrets.password && !secrets.user, - second: secrets.password && secrets.user, - secrets, - }); // user and password must be set together (or not at all) if (!secrets.password && !secrets.user) return; if (secrets.password && secrets.user) return; @@ -65,3 +60,35 @@ export const validate: ExternalServiceValidation = { config: validateConfig, secrets: validateSecrets, }; + +const validProtocols: string[] = ['http:', 'https:']; +export const assertURL = (url: string) => { + try { + const parsedUrl = new URL(url); + + if (!parsedUrl.hostname) { + throw new Error('URL must contain hostname'); + } + + if (!validProtocols.includes(parsedUrl.protocol)) { + throw new Error('Invalid protocol'); + } + } catch (error) { + throw new Error(`URL Error: ${error.message}`); + } +}; +export const ensureUriAllowed = ( + url: string, + configurationUtilities: ActionsConfigurationUtilities +) => { + try { + configurationUtilities.ensureUriAllowed(url); + } catch (allowedListError) { + throw new Error(i18n.ALLOWED_HOSTS_ERROR(allowedListError.message)); + } +}; +export const normalizeURL = (url: string) => { + const urlWithoutTrailingSlash = url.endsWith('/') ? url.slice(0, -1) : url; + const replaceDoubleSlashesRegex = new RegExp('([^:]/)/+', 'g'); + return urlWithoutTrailingSlash.replace(replaceDoubleSlashesRegex, '$1'); +}; From 008bdcb11a1b3b486e41def51970dda41db8d269 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Fri, 22 Jul 2022 09:36:39 -0600 Subject: [PATCH 70/97] add technical preview badge --- .../builtin_action_types/cases_webhook/webhook.tsx | 9 +++++++++ .../action_connector_form/action_type_menu.tsx | 2 ++ .../create_connector_flyout/header.tsx | 13 ++++++++++++- .../create_connector_flyout/index.tsx | 1 + x-pack/plugins/triggers_actions_ui/public/types.ts | 3 ++- 5 files changed, 26 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx index 4673a86f5fcfeb..79f99424f9f820 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx @@ -24,6 +24,15 @@ export function getActionType(): ActionTypeModel< defaultMessage: 'Send a request to a Case Management web service.', } ), + betaBadgeProps: { + label: i18n.translate('xpack.triggersActionsUi.technicalPreviewBadgeLabel', { + defaultMessage: 'Technical preview', + }), + tooltipContent: i18n.translate('xpack.triggersActionsUi.technicalPreviewBadgeDescription', { + defaultMessage: + 'This functionality is in technical preview and may be changed or removed completely in a future release. Elastic will take a best effort approach to fix any issues, but features in technical preview are not subject to the support SLA of official GA features.', + }), + }, actionTypeTitle: i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.actionTypeTitle', { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx index be9bdbac679494..5be1d5fe27ead8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx @@ -94,6 +94,7 @@ export const ActionTypeMenu = ({ selectMessage: actionTypeModel ? actionTypeModel.selectMessage : '', actionType, name: actionType.name, + betaBadgeProps: actionTypeModel.betaBadgeProps, }; }); @@ -103,6 +104,7 @@ export const ActionTypeMenu = ({ const checkEnabledResult = checkActionTypeEnabled(item.actionType); const card = ( } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/header.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/header.tsx index 7da1919b55654f..e0234be1094e29 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/header.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/header.tsx @@ -14,14 +14,17 @@ import { EuiText, EuiFlyoutHeader, IconType, + EuiBetaBadge, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiBetaBadgeProps } from '@elastic/eui/src/components/badge/beta_badge'; const FlyoutHeaderComponent: React.FC<{ icon?: IconType | null; actionTypeName?: string | null; actionTypeMessage?: string | null; -}> = ({ icon, actionTypeName, actionTypeMessage }) => { + betaBadgeProps?: Partial; +}> = ({ icon, actionTypeName, actionTypeMessage, betaBadgeProps }) => { return ( @@ -59,6 +62,14 @@ const FlyoutHeaderComponent: React.FC<{
)} + {betaBadgeProps && ( + + + + )}
); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/index.tsx index 63eab1bf932296..dc55119289a0eb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/index.tsx @@ -156,6 +156,7 @@ const CreateConnectorFlyoutComponent: React.FC = ({ icon={actionTypeModel?.iconClass} actionTypeName={actionType?.name} actionTypeMessage={actionTypeModel?.selectMessage} + betaBadgeProps={actionTypeModel?.betaBadgeProps} /> : null} diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 2f81f513aee72e..b532c4adee4c90 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -13,7 +13,7 @@ import type { ChartsPluginSetup } from '@kbn/charts-plugin/public'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; -import type { IconType, EuiFlyoutSize } from '@elastic/eui'; +import type { IconType, EuiFlyoutSize, EuiBetaBadgeProps } from '@elastic/eui'; import { EuiDataGridColumn, EuiDataGridControlColumn, EuiDataGridSorting } from '@elastic/eui'; import { ActionType, @@ -193,6 +193,7 @@ export interface ActionTypeModel | null; actionParamsFields: React.LazyExoticComponent>>; customConnectorSelectItem?: CustomConnectorSelectionItem; + betaBadgeProps?: Partial; } export interface GenericValidationResult { From 90d69d13db5155c4862f2090be5d0c45b91eb58c Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Fri, 22 Jul 2022 09:38:46 -0600 Subject: [PATCH 71/97] fix test --- .../cases_webhook/utils.test.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.test.ts index a388c21eaf9abb..29c732c1b7d400 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.test.ts @@ -6,7 +6,7 @@ */ import { - getObjectValueByKey, + getObjectValueByKeyAsString, stringifyObjValues, throwDescriptiveErrorIfResponseIsNotValid, } from './utils'; @@ -35,22 +35,20 @@ const bigOlObject = { }, }; describe('cases_webhook/utils', () => { - describe('getObjectValueByKey()', () => { + describe('getObjectValueByKeyAsString()', () => { it('Handles a simple key', () => { - expect(getObjectValueByKey(bigOlObject, 'field.simple')).toEqual('simple'); + expect(getObjectValueByKeyAsString(bigOlObject, 'field.simple')).toEqual('simple'); }); it('Handles a complicated key', () => { - expect(getObjectValueByKey(bigOlObject, 'fields.id[0].good.cool')).toEqual( - 'cool' - ); + expect(getObjectValueByKeyAsString(bigOlObject, 'fields.id[0].good.cool')).toEqual('cool'); }); it('Handles a more complicated key', () => { expect( - getObjectValueByKey(bigOlObject, 'fields.id[1].more[0].more.complicated') + getObjectValueByKeyAsString(bigOlObject, 'fields.id[1].more[0].more.complicated') ).toEqual('complicated'); }); it('Handles a bad key', () => { - expect(getObjectValueByKey(bigOlObject, 'bad.key')).toEqual(undefined); + expect(getObjectValueByKeyAsString(bigOlObject, 'bad.key')).toEqual(undefined); }); }); describe('throwDescriptiveErrorIfResponseIsNotValid()', () => { From cd86d5df924a536665e26becd845545ea4aa6bfc Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Fri, 22 Jul 2022 09:57:37 -0600 Subject: [PATCH 72/97] no async submit validation --- .../cases_webhook/webhook_connectors.tsx | 41 +++++++++++-------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx index cbb35fda495a0b..9673f2ab75e010 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx @@ -55,7 +55,7 @@ type PossibleStepNumbers = 1 | 2 | 3 | 4; const CasesWebhookActionConnectorFields: React.FunctionComponent = ({ readOnly, }) => { - const { isValid, validateFields } = useFormContext(); + const { isValid, getFields, validateFields } = useFormContext(); const [currentStep, setCurrentStep] = useState(1); const [status, setStatus] = useState>({ step1: 'incomplete', @@ -65,26 +65,30 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent { const steps: PossibleStepNumbers[] = [1, 2, 3, 4]; - const statuses = await Promise.all( - steps.map(async (index) => { - if (typeof isValid !== 'undefined' && !isValid) { - const { areFieldsValid } = await validateFields(fields[`step${index}`]); - return { - [`step${index}`]: areFieldsValid ? 'complete' : ('danger' as EuiStepStatus), - }; - } + const currentFields = getFields(); + const statuses = steps.map((index) => { + if (typeof isValid !== 'undefined' && !isValid) { + const fieldsToValidate = fields[`step${index}`]; + // submit validation fields have already been through validator + // so we can look at the isValid property from `getFields()` + const areFieldsValid = fieldsToValidate.every((field) => + currentFields[field] !== undefined ? currentFields[field].isValid : true + ); return { - [`step${index}`]: - currentStep === index - ? 'current' - : currentStep > index - ? 'complete' - : ('incomplete' as EuiStepStatus), + [`step${index}`]: areFieldsValid ? 'complete' : ('danger' as EuiStepStatus), }; - }) - ); + } + return { + [`step${index}`]: + currentStep === index + ? 'current' + : currentStep > index + ? 'complete' + : ('incomplete' as EuiStepStatus), + }; + }); setStatus(statuses.reduce((acc: Record, i) => ({ ...acc, ...i }), {})); - }, [currentStep, isValid, validateFields]); + }, [currentStep, getFields, isValid]); useEffect(() => { updateStatus(); @@ -107,6 +111,7 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent Date: Fri, 22 Jul 2022 10:01:24 -0600 Subject: [PATCH 73/97] type fixes --- .../components/builtin_action_types/cases_webhook/webhook.tsx | 4 ++-- .../action_connector_form/create_connector_flyout/header.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx index 79f99424f9f820..beacfc6a77c7e0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx @@ -25,10 +25,10 @@ export function getActionType(): ActionTypeModel< } ), betaBadgeProps: { - label: i18n.translate('xpack.triggersActionsUi.technicalPreviewBadgeLabel', { + label: i18n.translate('xpack.triggersActionsUI.technicalPreviewBadgeLabel', { defaultMessage: 'Technical preview', }), - tooltipContent: i18n.translate('xpack.triggersActionsUi.technicalPreviewBadgeDescription', { + tooltipContent: i18n.translate('xpack.triggersActionsUI.technicalPreviewBadgeDescription', { defaultMessage: 'This functionality is in technical preview and may be changed or removed completely in a future release. Elastic will take a best effort approach to fix any issues, but features in technical preview are not subject to the support SLA of official GA features.', }), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/header.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/header.tsx index e0234be1094e29..1ee8f0099838eb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/header.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/header.tsx @@ -15,9 +15,9 @@ import { EuiFlyoutHeader, IconType, EuiBetaBadge, + EuiBetaBadgeProps, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { EuiBetaBadgeProps } from '@elastic/eui/src/components/badge/beta_badge'; const FlyoutHeaderComponent: React.FC<{ icon?: IconType | null; From 2dc98c351e435615c7fd4430da7e51aa0d7f5ec5 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Fri, 22 Jul 2022 10:03:58 -0600 Subject: [PATCH 74/97] fix i18n --- x-pack/plugins/translations/translations/fr-FR.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 29bbbc0b98acf3..01f325f0da7335 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -9948,8 +9948,6 @@ "xpack.cases.configureCases.incidentManagementSystemDesc": "Connectez vos cas à un système de gestion des incidents externes. Vous pouvez ensuite transmettre les données de cas en tant qu'incident dans un système tiers.", "xpack.cases.configureCases.incidentManagementSystemLabel": "Système de gestion des incidents", "xpack.cases.configureCases.incidentManagementSystemTitle": "Système de gestion des incidents externes", - "xpack.cases.configureCases.requiredMappings": "Au moins un champ de cas doit être mappé aux champs { connectorName } requis suivants : { fields }", - "xpack.cases.configureCases.saveAndCloseButton": "Enregistrer et fermer", "xpack.cases.configureCases.saveButton": "Enregistrer", "xpack.cases.configureCases.updateSelectedConnector": "Mettre à jour { connectorName }", "xpack.cases.configureCases.warningMessage": "Le connecteur utilisé pour envoyer des mises à jour au service externe a été supprimé ou vous ne disposez pas de la {appropriateLicense} pour l'utiliser. Pour mettre à jour des cas dans des systèmes externes, sélectionnez un autre connecteur ou créez-en un nouveau.", From f581b1da449b28ff406dd00a36b38b76d970df13 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Fri, 22 Jul 2022 10:33:30 -0600 Subject: [PATCH 75/97] translations and headers --- .../cases_webhook/service.ts | 2 +- .../cases_webhook/steps/create.tsx | 9 ++- .../cases_webhook/steps/get.tsx | 8 ++- .../cases_webhook/steps/update.tsx | 16 ++++- .../cases_webhook/translations.ts | 70 +++++++++++++++---- 5 files changed, 89 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts index 24e10fd4222290..ffe344b2b38e39 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts @@ -243,7 +243,7 @@ export const createExternalService = ( const createComment = async ({ incidentId, comment }: CreateCommentParams): Promise => { try { if (!createCommentUrl || !createCommentJson) { - return {}; + return; } const commentUrl = renderMustacheStringNoEscape(createCommentUrl, { external: { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/create.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/create.tsx index f74b44be34b404..c754a89ed89fec 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/create.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/create.tsx @@ -6,7 +6,7 @@ */ import React, { FunctionComponent } from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui'; import { FIELD_TYPES, UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { Field } from '@kbn/es-ui-shared-plugin/static/forms/components'; import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; @@ -24,6 +24,13 @@ interface Props { export const CreateStep: FunctionComponent = ({ display, readOnly }) => ( + +

{i18n.STEP_2}

+ +

{i18n.STEP_2_DESCRIPTION}

+
+
+ = ({ display, readOnly }) => ( + +

{i18n.STEP_3}

+ +

{i18n.STEP_3_DESCRIPTION}

+
+
= ({ display, readOnly }) => ( + +

{i18n.STEP_4A}

+ +

{i18n.STEP_4A_DESCRIPTION}

+
+
+ = ({ display, readOnly }) => ( /> + +

{i18n.STEP_4B}

+ +

{i18n.STEP_4B_DESCRIPTION}

+
+
+ Date: Fri, 22 Jul 2022 10:50:12 -0600 Subject: [PATCH 76/97] fix user/pw validation --- .../builtin_action_types/cases_webhook/index.ts | 1 + .../builtin_action_types/cases_webhook/types.ts | 4 ++++ .../builtin_action_types/cases_webhook/validators.ts | 11 +++++++++++ .../actions/builtin_action_types/cases_webhook.ts | 2 +- 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts index 4b8f7615ad325f..42dd422bfd3731 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts @@ -56,6 +56,7 @@ export function getActionType({ validate: curry(validate.secrets), }), params: ExecutorParamsSchema, + connector: validate.connector, }, executor: curry(executor)({ logger, configurationUtilities }), }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts index 61dd1b645abdb2..21796c1d743aa3 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts @@ -44,6 +44,10 @@ export interface ExternalServiceValidation { configObject: CasesWebhookPublicConfigurationType ) => void; secrets: (secrets: CasesWebhookSecretConfigurationType) => void; + connector: ( + configObject: CasesWebhookPublicConfigurationType, + secrets: CasesWebhookSecretConfigurationType + ) => string | null; } export interface ExternalServiceIncidentResponse { diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts index bc01bf01e7b40a..51afa4eae9dfbe 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts @@ -49,6 +49,16 @@ const validateConfig = ( } }; +export const validateConnector = ( + configObject: CasesWebhookPublicConfigurationType, + secrets: CasesWebhookSecretConfigurationType +): string | null => { + // user and password must be set together (or not at all) + if (!configObject.hasAuth) return null; + if (secrets.password && secrets.user) return null; + return i18n.INVALID_USER_PW; +}; + export const validateSecrets = (secrets: CasesWebhookSecretConfigurationType) => { // user and password must be set together (or not at all) if (!secrets.password && !secrets.user) return; @@ -59,6 +69,7 @@ export const validateSecrets = (secrets: CasesWebhookSecretConfigurationType) => export const validate: ExternalServiceValidation = { config: validateConfig, secrets: validateSecrets, + connector: validateConnector, }; const validProtocols: string[] = ['http:', 'https:']; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/cases_webhook.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/cases_webhook.ts index 46510e8c1b4d57..de73d90342b5b6 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/cases_webhook.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/cases_webhook.ts @@ -206,7 +206,7 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - 'error validating action type secrets: [user]: expected value of type [string] but got [undefined]', + 'error validating action type connector: both user and password must be specified', }); }); }); From 953e23575709fc219d9937b519b7dae3d0ce53b7 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Fri, 22 Jul 2022 12:55:42 -0600 Subject: [PATCH 77/97] fix translations and tests --- .../cases_webhook/service.test.ts | 4 ++-- .../cases_webhook/steps/update.tsx | 1 + .../cases_webhook/translations.ts | 20 +++++++++---------- .../cases_webhook/webhook_connectors.tsx | 10 +++++++++- .../create_connector_flyout/header.tsx | 1 + 5 files changed, 22 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts index b97972f4c6f375..2675530b660c97 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts @@ -465,7 +465,7 @@ describe('Cases webhook service', () => { ); const res = await service.createComment(commentReq); expect(requestMock).not.toHaveBeenCalled(); - expect(res).toEqual({}); + expect(res).toBeUndefined(); }); test('it fails silently if createCommentJson is missing', async () => { @@ -480,7 +480,7 @@ describe('Cases webhook service', () => { ); const res = await service.createComment(commentReq); expect(requestMock).not.toHaveBeenCalled(); - expect(res).toEqual({}); + expect(res).toBeUndefined(); }); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/update.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/update.tsx index a6d2ccc79e2029..c8bfa4ad350b13 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/update.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/update.tsx @@ -114,6 +114,7 @@ export const UpdateStep: FunctionComponent = ({ display, readOnly }) => ( /> +

{i18n.STEP_4B}

diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts index 43a91d1f308728..1e21e64228b176 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/translations.ts @@ -304,8 +304,7 @@ export const UPDATE_INCIDENT_URL = i18n.translate( export const UPDATE_INCIDENT_URL_HELP = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.updateIncidentUrlHelp', { - defaultMessage: - 'API URL to update case. Use the variable selector to add external system id to the url.', + defaultMessage: 'API URL to update case.', } ); @@ -339,8 +338,7 @@ export const CREATE_COMMENT_URL = i18n.translate( export const CREATE_COMMENT_URL_HELP = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.createCommentUrlHelp', { - defaultMessage: - 'API URL to add comment to case. Use the variable selector to add external system id to the url.', + defaultMessage: 'API URL to add comment to case.', } ); @@ -365,13 +363,6 @@ export const HAS_AUTH = i18n.translate( } ); -export const REENTER_VALUES = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.reenterValuesLabel', - { - defaultMessage: 'Username and password are encrypted. Please reenter values for these fields.', - } -); - export const USERNAME = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.userTextFieldLabel', { @@ -536,3 +527,10 @@ export const EXTERNAL_TITLE_DESC = i18n.translate( defaultMessage: 'External system title', } ); + +export const DOC_LINK = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.docLink', + { + defaultMessage: 'Configuring Webhook - Case Management connector.', + } +); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx index 9673f2ab75e010..d828f8226b8dfb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.tsx @@ -12,10 +12,12 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem, + EuiLink, EuiSpacer, EuiStepsHorizontal, EuiStepStatus, } from '@elastic/eui'; +import { useKibana } from '../../../../common/lib/kibana'; import { ActionConnectorFieldsProps } from '../../../../types'; import * as i18n from './translations'; import { AuthStep, CreateStep, GetStep, UpdateStep } from './steps'; @@ -55,6 +57,7 @@ type PossibleStepNumbers = 1 | 2 | 3 | 4; const CasesWebhookActionConnectorFields: React.FunctionComponent = ({ readOnly, }) => { + const { docLinks } = useKibana().services; const { isValid, getFields, validateFields } = useFormContext(); const [currentStep, setCurrentStep] = useState(1); const [status, setStatus] = useState>({ @@ -161,12 +164,17 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent + + {i18n.DOC_LINK} + - {currentStep < 4 && ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/header.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/header.tsx index 1ee8f0099838eb..138e9f056bfda9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/header.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/header.tsx @@ -75,4 +75,5 @@ const FlyoutHeaderComponent: React.FC<{ ); }; +// @ts-ignore ts does not like the EuiBetaBadgeProps ¯\_(ツ)_/¯ export const FlyoutHeader = memo(FlyoutHeaderComponent); From 8aecdc3e29ed75154f44d6ece0df8b98980f1c9f Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Fri, 22 Jul 2022 13:15:30 -0600 Subject: [PATCH 78/97] fix type --- .../create_connector_flyout/header.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/header.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/header.tsx index 138e9f056bfda9..7c230716244e05 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/header.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/header.tsx @@ -75,5 +75,9 @@ const FlyoutHeaderComponent: React.FC<{ ); }; -// @ts-ignore ts does not like the EuiBetaBadgeProps ¯\_(ツ)_/¯ -export const FlyoutHeader = memo(FlyoutHeaderComponent); +export const FlyoutHeader: React.NamedExoticComponent<{ + icon?: IconType | null; + actionTypeName?: string | null; + actionTypeMessage?: string | null; + betaBadgeProps?: Partial; +}> = memo(FlyoutHeaderComponent); From 3dc8e3ee74e51d41ee4e14f503221c0f96d23ea6 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Fri, 22 Jul 2022 13:17:46 -0600 Subject: [PATCH 79/97] Props to own interface --- .../create_connector_flyout/header.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/header.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/header.tsx index 7c230716244e05..6b803fcf2ca193 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/header.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/header.tsx @@ -19,12 +19,19 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -const FlyoutHeaderComponent: React.FC<{ +interface Props { icon?: IconType | null; actionTypeName?: string | null; actionTypeMessage?: string | null; betaBadgeProps?: Partial; -}> = ({ icon, actionTypeName, actionTypeMessage, betaBadgeProps }) => { +} + +const FlyoutHeaderComponent: React.FC = ({ + icon, + actionTypeName, + actionTypeMessage, + betaBadgeProps, +}) => { return ( @@ -75,9 +82,4 @@ const FlyoutHeaderComponent: React.FC<{ ); }; -export const FlyoutHeader: React.NamedExoticComponent<{ - icon?: IconType | null; - actionTypeName?: string | null; - actionTypeMessage?: string | null; - betaBadgeProps?: Partial; -}> = memo(FlyoutHeaderComponent); +export const FlyoutHeader: React.NamedExoticComponent = memo(FlyoutHeaderComponent); From 4b8b1e3b4217861cdc92d8c4b03bac28cb0b4bbe Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Sat, 23 Jul 2022 08:27:09 -0600 Subject: [PATCH 80/97] better error messaging --- .../cases_webhook/service.ts | 157 +++++---- .../cases_webhook/types.ts | 2 +- .../cases_webhook/validators.ts | 26 +- .../builtin_action_types/cases_webhook.ts | 332 +++++++++++++++++- 4 files changed, 450 insertions(+), 67 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts index ffe344b2b38e39..bd8bc3e212259f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts @@ -9,7 +9,7 @@ import axios, { AxiosResponse } from 'axios'; import { Logger } from '@kbn/core/server'; import { isString } from 'lodash'; -import { assertURL, ensureUriAllowed, normalizeURL } from './validators'; +import { validateAndNormalizeUrl, validateJson } from './validators'; import { renderMustacheStringNoEscape } from '../../lib/mustache_renderer'; import { createServiceError, @@ -84,17 +84,20 @@ export const createExternalService = ( }); const getIncident = async (id: string): Promise => { - const getUrl = renderMustacheStringNoEscape(getIncidentUrl, { - external: { - system: { - id, - }, - }, - }); - assertURL(`${getUrl}`); - ensureUriAllowed(`${getUrl}`, configurationUtilities); - const normalizedUrl = normalizeURL(`${getUrl}`); try { + const getUrl = renderMustacheStringNoEscape(getIncidentUrl, { + external: { + system: { + id, + }, + }, + }); + + const normalizedUrl = validateAndNormalizeUrl( + `${getUrl}`, + configurationUtilities, + 'Get case URL' + ); const res = await request({ axios: axiosInstance, url: normalizedUrl, @@ -123,24 +126,29 @@ export const createExternalService = ( const createIncident = async ({ incident, }: CreateIncidentParams): Promise => { - const { tags, title, description } = incident; - assertURL(`${createIncidentUrl}`); - ensureUriAllowed(`${createIncidentUrl}`, configurationUtilities); - const normalizedUrl = normalizeURL(`${createIncidentUrl}`); try { + const { tags, title, description } = incident; + const normalizedUrl = validateAndNormalizeUrl( + `${createIncidentUrl}`, + configurationUtilities, + 'Create case URL' + ); + const json = renderMustacheStringNoEscape( + createIncidentJson, + stringifyObjValues({ + title, + description: description ?? '', + tags: tags ?? [], + }) + ); + + validateJson(json, 'Create case JSON body'); const res: AxiosResponse = await request({ axios: axiosInstance, url: normalizedUrl, logger, method: createIncidentMethod, - data: renderMustacheStringNoEscape( - createIncidentJson, - stringifyObjValues({ - title, - description: description ?? '', - tags: tags ?? [], - }) - ), + data: json, configurationUtilities, }); @@ -163,9 +171,11 @@ export const createExternalService = ( }, }, }); - assertURL(`${viewUrl}`); - ensureUriAllowed(`${viewUrl}`, configurationUtilities); - const normalizedViewUrl = normalizeURL(`${viewUrl}`); + const normalizedViewUrl = validateAndNormalizeUrl( + `${viewUrl}`, + configurationUtilities, + 'View case URL' + ); return { id: externalId, title: insertedIncident.title, @@ -181,43 +191,55 @@ export const createExternalService = ( incidentId, incident, }: UpdateIncidentParams): Promise => { - const updateUrl = renderMustacheStringNoEscape(updateIncidentUrl, { - external: { - system: { - id: incidentId, - }, - }, - }); - assertURL(`${updateUrl}`); - ensureUriAllowed(`${updateUrl}`, configurationUtilities); - const normalizedUrl = normalizeURL(`${updateUrl}`); try { + const updateUrl = renderMustacheStringNoEscape(updateIncidentUrl, { + external: { + system: { + id: incidentId, + }, + }, + }); + const normalizedUrl = validateAndNormalizeUrl( + `${updateUrl}`, + configurationUtilities, + 'Update case URL' + ); + console.log({ + updateUrl, + normalizedUrl, + }); const { tags, title, description } = incident; + const json = renderMustacheStringNoEscape(updateIncidentJson, { + ...stringifyObjValues({ + title, + description: description ?? '', + tags: tags ?? [], + }), + external: { + system: { + id: incidentId, + }, + }, + }); + + console.log('JSON', json); + validateJson(json, 'Update case JSON body'); const res = await request({ axios: axiosInstance, method: updateIncidentMethod, url: normalizedUrl, logger, - data: renderMustacheStringNoEscape(updateIncidentJson, { - ...stringifyObjValues({ - ...(title ? { title } : {}), - ...(description ? { description } : {}), - ...(tags ? { tags } : {}), - }), - external: { - system: { - id: incidentId, - }, - }, - }), + data: json, configurationUtilities, }); + console.log('res', res); throwDescriptiveErrorIfResponseIsNotValid({ res, }); - + console.log('1'); const updatedIncident = await getIncident(incidentId as string); + console.log('2'); const viewUrl = renderMustacheStringNoEscape(incidentViewUrl, { external: { system: { @@ -226,9 +248,13 @@ export const createExternalService = ( }, }, }); - assertURL(`${viewUrl}`); - ensureUriAllowed(`${viewUrl}`, configurationUtilities); - const normalizedViewUrl = normalizeURL(`${viewUrl}`); + console.log('3'); + const normalizedViewUrl = validateAndNormalizeUrl( + `${viewUrl}`, + configurationUtilities, + 'View case URL' + ); + console.log('4'); return { id: incidentId, title: updatedIncident.title, @@ -236,6 +262,7 @@ export const createExternalService = ( pushedDate: getPushedDate(updatedIncident.updatedAt), }; } catch (error) { + console.log('error', error); throw createServiceError(error, `Unable to update case with id ${incidentId}`); } }; @@ -252,22 +279,26 @@ export const createExternalService = ( }, }, }); - assertURL(`${commentUrl}`); - ensureUriAllowed(`${commentUrl}`, configurationUtilities); - const normalizedUrl = normalizeURL(`${commentUrl}`); + const normalizedUrl = validateAndNormalizeUrl( + `${commentUrl}`, + configurationUtilities, + 'Create comment URL' + ); + const json = renderMustacheStringNoEscape(createCommentJson, { + ...stringifyObjValues({ comment: comment.comment }), + external: { + system: { + id: incidentId, + }, + }, + }); + validateJson(json, 'Create comment JSON body'); const res = await request({ axios: axiosInstance, method: createCommentMethod, url: normalizedUrl, logger, - data: renderMustacheStringNoEscape(createCommentJson, { - ...stringifyObjValues({ comment: comment.comment }), - external: { - system: { - id: incidentId, - }, - }, - }), + data: json, configurationUtilities, }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts index 21796c1d743aa3..1ea2b515e3ecd3 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/types.ts @@ -74,7 +74,7 @@ export interface CreateIncidentParams { } export interface UpdateIncidentParams { incidentId: string; - incident: Partial; + incident: Incident; } export interface SimpleComment { comment: string; diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts index 51afa4eae9dfbe..46990060b07321 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts @@ -85,7 +85,7 @@ export const assertURL = (url: string) => { throw new Error('Invalid protocol'); } } catch (error) { - throw new Error(`URL Error: ${error.message}`); + throw new Error(`${error.message}`); } }; export const ensureUriAllowed = ( @@ -95,7 +95,7 @@ export const ensureUriAllowed = ( try { configurationUtilities.ensureUriAllowed(url); } catch (allowedListError) { - throw new Error(i18n.ALLOWED_HOSTS_ERROR(allowedListError.message)); + throw Error(i18n.ALLOWED_HOSTS_ERROR(allowedListError.message)); } }; export const normalizeURL = (url: string) => { @@ -103,3 +103,25 @@ export const normalizeURL = (url: string) => { const replaceDoubleSlashesRegex = new RegExp('([^:]/)/+', 'g'); return urlWithoutTrailingSlash.replace(replaceDoubleSlashesRegex, '$1'); }; + +export const validateAndNormalizeUrl = ( + url: string, + configurationUtilities: ActionsConfigurationUtilities, + urlDesc: string +) => { + try { + assertURL(url); + ensureUriAllowed(url, configurationUtilities); + return normalizeURL(url); + } catch (e) { + throw Error(`${urlDesc} ${e}`); + } +}; + +export const validateJson = (jsonString: string, jsonDesc: string) => { + try { + JSON.parse(jsonString); + } catch (e) { + throw new Error(`JSON Error: ${jsonDesc} must be valid JSON`); + } +}; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/cases_webhook.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/cases_webhook.ts index de73d90342b5b6..e49b15ed0e5a77 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/cases_webhook.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/cases_webhook.ts @@ -35,7 +35,7 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { getIncidentResponseExternalTitleKey: 'key', getIncidentResponseUpdatedDateKey: 'fields.updated', hasAuth: true, - headers: { ['content-type']: 'application/json' }, + headers: { ['content-type']: 'application/json', ['kbn-xsrf']: 'abcd' }, incidentViewUrl: 'https://siem-kibana.atlassian.net/browse/{{{external.system.title}}}', getIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}', updateIncidentJson: @@ -407,5 +407,335 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { } }); }); + + describe('CasesWebhook - Executor bad data', () => { + describe('bad case JSON', () => { + let simulatedActionId: string; + let proxyServer: httpProxy | undefined; + let proxyHaveBeenCalled = false; + const jsonExtraCommas = + '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"labels":{{{case.tags}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}},,,,,'; + before(async () => { + const { body } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A casesWebhook simulator', + connector_type_id: '.cases-webhook', + config: { + ...simulatorConfig, + createIncidentJson: jsonExtraCommas, + updateIncidentJson: jsonExtraCommas, + }, + secrets, + }); + simulatedActionId = body.id; + + proxyServer = await getHttpProxyServer( + kibanaServer.resolveUrl('/'), + configService.get('kbnTestServer.serverArgs'), + () => { + proxyHaveBeenCalled = true; + } + ); + }); + + it('should respond with bad JSON error when create case JSON is bad', async () => { + await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockCasesWebhook.params, + subActionParams: { + incident: { + title: 'success', + description: 'success', + }, + comments: [], + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + connector_id: simulatedActionId, + status: 'error', + retry: false, + message: 'an error occurred while running the action', + service_message: + '[Action][Webhook - Case Management]: Unable to create case. Error: JSON Error: Create case JSON body must be valid JSON. ', + }); + }); + expect(proxyHaveBeenCalled).to.equal(false); + }); + + it('should respond with bad JSON error when update case JSON is bad', async () => { + await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockCasesWebhook.params, + subActionParams: { + incident: { + title: 'success', + description: 'success', + externalId: '12345', + }, + comments: [], + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + connector_id: simulatedActionId, + status: 'error', + retry: false, + message: 'an error occurred while running the action', + service_message: + '[Action][Webhook - Case Management]: Unable to update case with id 12345. Error: JSON Error: Update case JSON body must be valid JSON. ', + }); + }); + expect(proxyHaveBeenCalled).to.equal(false); + }); + after(() => { + if (proxyServer) { + proxyServer.close(); + } + }); + }); + describe('bad comment JSON', () => { + let simulatedActionId: string; + let proxyServer: httpProxy | undefined; + let proxyHaveBeenCalled = false; + before(async () => { + const { body } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A casesWebhook simulator', + connector_type_id: '.cases-webhook', + config: { + ...simulatorConfig, + createCommentJson: '{"body":{{{case.comment}}}},,,,,,,', + }, + secrets, + }); + simulatedActionId = body.id; + + proxyServer = await getHttpProxyServer( + kibanaServer.resolveUrl('/'), + configService.get('kbnTestServer.serverArgs'), + () => { + proxyHaveBeenCalled = true; + } + ); + }); + + it('should respond with bad JSON error when create case comment JSON is bad', async () => { + await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockCasesWebhook.params, + subActionParams: { + incident: { + title: 'success', + description: 'success', + }, + comments: [ + { + comment: 'first comment', + commentId: '456', + }, + ], + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + connector_id: simulatedActionId, + status: 'error', + retry: false, + message: 'an error occurred while running the action', + service_message: + '[Action][Webhook - Case Management]: Unable to create comment at case with id 123. Error: JSON Error: Create comment JSON body must be valid JSON. ', + }); + }); + expect(proxyHaveBeenCalled).to.equal(true); // called for the create case successful call + }); + + after(() => { + if (proxyServer) { + proxyServer.close(); + } + }); + }); + }); + + describe('CasesWebhook - Executor bad URLs', () => { + describe('bad case URL', () => { + let simulatedActionId: string; + let proxyServer: httpProxy | undefined; + let proxyHaveBeenCalled = false; + before(async () => { + const { body } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A casesWebhook simulator', + connector_type_id: '.cases-webhook', + config: { + ...simulatorConfig, + createIncidentUrl: `https${casesWebhookSimulatorURL}`, + updateIncidentUrl: `${casesWebhookSimulatorURL}/rest/api/2/issue/{{{external.system.id}}}e\\\\whoathisisbad4{}\{\{`, + }, + secrets, + }); + simulatedActionId = body.id; + + proxyServer = await getHttpProxyServer( + kibanaServer.resolveUrl('/'), + configService.get('kbnTestServer.serverArgs'), + () => { + proxyHaveBeenCalled = true; + } + ); + }); + + it('should respond with bad URL error when create case URL is bad', async () => { + await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockCasesWebhook.params, + subActionParams: { + incident: { + title: 'success', + description: 'success', + }, + comments: [], + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + connector_id: simulatedActionId, + status: 'error', + retry: false, + message: 'an error occurred while running the action', + service_message: + '[Action][Webhook - Case Management]: Unable to create case. Error: Create case URL Error: Invalid protocol. ', + }); + }); + expect(proxyHaveBeenCalled).to.equal(false); + }); + + it('should respond with bad URL error when update case URL is bad', async () => { + await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockCasesWebhook.params, + subActionParams: { + incident: { + title: 'success', + description: 'success', + externalId: '12345', + }, + comments: [], + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + connector_id: simulatedActionId, + status: 'error', + retry: false, + message: 'an error occurred while running the action', + service_message: + '[Action][Webhook - Case Management]: Unable to update case with id 12345. Error: Update case URL Error: Invalid URL. ', + }); + }); + expect(proxyHaveBeenCalled).to.equal(false); + }); + after(() => { + if (proxyServer) { + proxyServer.close(); + } + }); + }); + describe('bad comment URL', () => { + let simulatedActionId: string; + let proxyServer: httpProxy | undefined; + let proxyHaveBeenCalled = false; + before(async () => { + const { body } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A casesWebhook simulator', + connector_type_id: '.cases-webhook', + config: { + ...simulatorConfig, + createCommentJson: '{"body":{{{case.comment}}}},,,,,,,', + }, + secrets, + }); + simulatedActionId = body.id; + + proxyServer = await getHttpProxyServer( + kibanaServer.resolveUrl('/'), + configService.get('kbnTestServer.serverArgs'), + () => { + proxyHaveBeenCalled = true; + } + ); + }); + + it('should respond with bad JSON error when create case comment JSON is bad', async () => { + await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockCasesWebhook.params, + subActionParams: { + incident: { + title: 'success', + description: 'success', + }, + comments: [ + { + comment: 'first comment', + commentId: '456', + }, + ], + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + connector_id: simulatedActionId, + status: 'error', + retry: false, + message: 'an error occurred while running the action', + service_message: + '[Action][Webhook - Case Management]: Unable to create comment at case with id 123. Error: JSON Error: Create comment JSON body must be valid JSON. ', + }); + }); + expect(proxyHaveBeenCalled).to.equal(true); // called for the create case successful call + }); + + after(() => { + if (proxyServer) { + proxyServer.close(); + } + }); + }); + }); }); } From df21227f076a14eafcad43c07a2e53a4aef166fe Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Sat, 23 Jul 2022 08:32:11 -0600 Subject: [PATCH 81/97] fix tests --- .../cases_webhook/webhook_connectors.test.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx index 470848be77e2f6..3ce481f3b3466c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx @@ -14,6 +14,18 @@ import { MockCodeEditor } from '../../../code_editor.mock'; import * as i18n from './translations'; const kibanaReactPath = '../../../../../../../../src/plugins/kibana_react/public'; +jest.mock('../../../../common/lib/kibana', () => { + const originalModule = jest.requireActual('../../../../common/lib/kibana'); + return { + ...originalModule, + useKibana: () => ({ + services: { + docLinks: { ELASTIC_WEBSITE_URL: 'url' }, + }, + }), + }; +}); + jest.mock(kibanaReactPath, () => { const original = jest.requireActual(kibanaReactPath); return { From f70e80e641cd0ac74865077da9a3664380549ea7 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 25 Jul 2022 07:25:16 -0600 Subject: [PATCH 82/97] betaBadgeProps > isExperimental --- .../cases_webhook/webhook.tsx | 10 +--------- .../action_connector_form/action_type_menu.tsx | 5 +++-- .../action_connector_form/beta_badge_props.tsx | 18 ++++++++++++++++++ .../create_connector_flyout/header.tsx | 8 ++++---- .../create_connector_flyout/index.tsx | 2 +- .../triggers_actions_ui/public/types.ts | 4 ++-- 6 files changed, 29 insertions(+), 18 deletions(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/beta_badge_props.tsx diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx index beacfc6a77c7e0..5ac8d915e26d9f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook.tsx @@ -24,15 +24,7 @@ export function getActionType(): ActionTypeModel< defaultMessage: 'Send a request to a Case Management web service.', } ), - betaBadgeProps: { - label: i18n.translate('xpack.triggersActionsUI.technicalPreviewBadgeLabel', { - defaultMessage: 'Technical preview', - }), - tooltipContent: i18n.translate('xpack.triggersActionsUI.technicalPreviewBadgeDescription', { - defaultMessage: - 'This functionality is in technical preview and may be changed or removed completely in a future release. Elastic will take a best effort approach to fix any issues, but features in technical preview are not subject to the support SLA of official GA features.', - }), - }, + isExperimental: true, actionTypeTitle: i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.actionTypeTitle', { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx index 5be1d5fe27ead8..f8f3e97ccf1834 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx @@ -17,6 +17,7 @@ import { actionTypeCompare } from '../../lib/action_type_compare'; import { checkActionTypeEnabled } from '../../lib/check_action_type_enabled'; import { useKibana } from '../../../common/lib/kibana'; import { SectionLoading } from '../../components/section_loading'; +import { betaBadgeProps } from './beta_badge_props'; interface Props { onActionTypeChange: (actionType: ActionType) => void; @@ -94,7 +95,7 @@ export const ActionTypeMenu = ({ selectMessage: actionTypeModel ? actionTypeModel.selectMessage : '', actionType, name: actionType.name, - betaBadgeProps: actionTypeModel.betaBadgeProps, + isExperimental: actionTypeModel.isExperimental, }; }); @@ -104,7 +105,7 @@ export const ActionTypeMenu = ({ const checkEnabledResult = checkActionTypeEnabled(item.actionType); const card = ( } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/beta_badge_props.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/beta_badge_props.tsx new file mode 100644 index 00000000000000..d88be2a9759ff2 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/beta_badge_props.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const betaBadgeProps = { + label: i18n.translate('xpack.triggersActionsUI.technicalPreviewBadgeLabel', { + defaultMessage: 'Technical preview', + }), + tooltipContent: i18n.translate('xpack.triggersActionsUI.technicalPreviewBadgeDescription', { + defaultMessage: + 'This functionality is in technical preview and may be changed or removed completely in a future release. Elastic will take a best effort approach to fix any issues, but features in technical preview are not subject to the support SLA of official GA features.', + }), +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/header.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/header.tsx index 6b803fcf2ca193..8f94c181726d2e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/header.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/header.tsx @@ -15,22 +15,22 @@ import { EuiFlyoutHeader, IconType, EuiBetaBadge, - EuiBetaBadgeProps, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import { betaBadgeProps } from '../beta_badge_props'; interface Props { icon?: IconType | null; actionTypeName?: string | null; actionTypeMessage?: string | null; - betaBadgeProps?: Partial; + isExperimental?: boolean; } const FlyoutHeaderComponent: React.FC = ({ icon, actionTypeName, actionTypeMessage, - betaBadgeProps, + isExperimental, }) => { return ( @@ -69,7 +69,7 @@ const FlyoutHeaderComponent: React.FC = ({ )} - {betaBadgeProps && ( + {isExperimental && ( = ({ icon={actionTypeModel?.iconClass} actionTypeName={actionType?.name} actionTypeMessage={actionTypeModel?.selectMessage} - betaBadgeProps={actionTypeModel?.betaBadgeProps} + isExperimental={actionTypeModel?.isExperimental} /> : null} diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 9c9b5a21ec37d2..b83a0cb9789dee 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -13,7 +13,7 @@ import type { ChartsPluginSetup } from '@kbn/charts-plugin/public'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; -import type { IconType, EuiFlyoutSize, EuiBetaBadgeProps } from '@elastic/eui'; +import type { IconType, EuiFlyoutSize } from '@elastic/eui'; import { EuiDataGridColumn, EuiDataGridControlColumn, EuiDataGridSorting } from '@elastic/eui'; import { ActionType, @@ -194,7 +194,7 @@ export interface ActionTypeModel | null; actionParamsFields: React.LazyExoticComponent>>; customConnectorSelectItem?: CustomConnectorSelectionItem; - betaBadgeProps?: Partial; + isExperimental?: boolean; } export interface GenericValidationResult { From d675a890db071b8f74d0a08f6bb568b497469076 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 25 Jul 2022 07:26:06 -0600 Subject: [PATCH 83/97] rm console.logs --- .../builtin_action_types/cases_webhook/service.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts index bd8bc3e212259f..dd9644d708e467 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts @@ -204,10 +204,7 @@ export const createExternalService = ( configurationUtilities, 'Update case URL' ); - console.log({ - updateUrl, - normalizedUrl, - }); + const { tags, title, description } = incident; const json = renderMustacheStringNoEscape(updateIncidentJson, { ...stringifyObjValues({ @@ -222,7 +219,6 @@ export const createExternalService = ( }, }); - console.log('JSON', json); validateJson(json, 'Update case JSON body'); const res = await request({ axios: axiosInstance, @@ -233,13 +229,10 @@ export const createExternalService = ( configurationUtilities, }); - console.log('res', res); throwDescriptiveErrorIfResponseIsNotValid({ res, }); - console.log('1'); const updatedIncident = await getIncident(incidentId as string); - console.log('2'); const viewUrl = renderMustacheStringNoEscape(incidentViewUrl, { external: { system: { @@ -248,13 +241,11 @@ export const createExternalService = ( }, }, }); - console.log('3'); const normalizedViewUrl = validateAndNormalizeUrl( `${viewUrl}`, configurationUtilities, 'View case URL' ); - console.log('4'); return { id: incidentId, title: updatedIncident.title, @@ -262,7 +253,6 @@ export const createExternalService = ( pushedDate: getPushedDate(updatedIncident.updatedAt), }; } catch (error) { - console.log('error', error); throw createServiceError(error, `Unable to update case with id ${incidentId}`); } }; From 2d22adcf48f8752914500b14178adbb10e85adb5 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 25 Jul 2022 07:49:22 -0600 Subject: [PATCH 84/97] fix badge --- .../create_connector_flyout/header.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/header.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/header.tsx index 758ddcabdfe17e..e0cd2a77cc265d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/header.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/header.tsx @@ -81,14 +81,6 @@ const FlyoutHeaderComponent: React.FC = ({ {getConnectorFeatureName(featureId)} ))} - {isExperimental && ( - - - - )} )} @@ -104,6 +96,14 @@ const FlyoutHeaderComponent: React.FC = ({ )}
+ {actionTypeName && isExperimental && ( + + + + )}
); From 8693147c08979f7fd4a7392095a2a1af97dad4fa Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 25 Jul 2022 08:54:37 -0600 Subject: [PATCH 85/97] better errs --- .../builtin_action_types/cases_webhook/utils.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts index cedd9002774aed..2e04416a9d79f2 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts @@ -10,15 +10,23 @@ import { isEmpty, isObjectLike, get } from 'lodash'; import { addTimeZoneToDate, getErrorMessage } from '../lib/axios_utils'; import * as i18n from './translations'; -export const createServiceError = (error: AxiosError, message: string) => - new Error( +export const createServiceError = (error: AxiosError, message: string) => { + const serverResponse = + error.response && error.response.data ? JSON.stringify(error.response.data) : null; + + return new Error( getErrorMessage( i18n.NAME, `${message}. Error: ${error.message}. ${ - error.response?.statusText != null ? `Reason: ${error.response?.statusText}` : '' + serverResponse != null + ? serverResponse + : error.response?.statusText != null + ? `Reason: ${error.response?.statusText}` + : '' }` ) ); +}; export const getPushedDate = (timestamp?: string) => { if (timestamp != null && new Date(timestamp).getTime() > 0) { From 3d4fcc41ea8df3a0162b9e3695b938bd5f21b5ae Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 25 Jul 2022 09:11:08 -0600 Subject: [PATCH 86/97] better url err --- .../builtin_action_types/cases_webhook/validators.ts | 8 ++++---- .../actions/builtin_action_types/cases_webhook.ts | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts index 46990060b07321..98ab28444933e4 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts @@ -78,11 +78,11 @@ export const assertURL = (url: string) => { const parsedUrl = new URL(url); if (!parsedUrl.hostname) { - throw new Error('URL must contain hostname'); + throw new Error(`URL must contain hostname`); } if (!validProtocols.includes(parsedUrl.protocol)) { - throw new Error('Invalid protocol'); + throw new Error(`Invalid protocol`); } } catch (error) { throw new Error(`${error.message}`); @@ -95,7 +95,7 @@ export const ensureUriAllowed = ( try { configurationUtilities.ensureUriAllowed(url); } catch (allowedListError) { - throw Error(i18n.ALLOWED_HOSTS_ERROR(allowedListError.message)); + throw Error(`${i18n.ALLOWED_HOSTS_ERROR(allowedListError.message)}`); } }; export const normalizeURL = (url: string) => { @@ -114,7 +114,7 @@ export const validateAndNormalizeUrl = ( ensureUriAllowed(url, configurationUtilities); return normalizeURL(url); } catch (e) { - throw Error(`${urlDesc} ${e}`); + throw Error(`Invalid ${urlDesc}: ${url}. ${e}`); } }; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/cases_webhook.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/cases_webhook.ts index e49b15ed0e5a77..faf0474a9a8f67 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/cases_webhook.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/cases_webhook.ts @@ -627,7 +627,7 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { retry: false, message: 'an error occurred while running the action', service_message: - '[Action][Webhook - Case Management]: Unable to create case. Error: Create case URL Error: Invalid protocol. ', + '[Action][Webhook - Case Management]: Unable to create case. Error: Invalid Create case URL: httpshttp://elastic:changeme@localhost:5620/api/_actions-FTS-external-service-simulators/jira. Error: Invalid protocol. ', }); }); expect(proxyHaveBeenCalled).to.equal(false); @@ -657,7 +657,7 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { retry: false, message: 'an error occurred while running the action', service_message: - '[Action][Webhook - Case Management]: Unable to update case with id 12345. Error: Update case URL Error: Invalid URL. ', + '[Action][Webhook - Case Management]: Unable to update case with id 12345. Error: Invalid Update case URL: error rendering mustache template "http://elastic:changeme@localhost:5620/api/_actions-FTS-external-service-simulators/jira/rest/api/2/issue/{{{external.system.id}}}e\\\\whoathisisbad4{}{{": Unclosed tag at 151. Error: Invalid URL. ', }); }); expect(proxyHaveBeenCalled).to.equal(false); @@ -681,7 +681,7 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { connector_type_id: '.cases-webhook', config: { ...simulatorConfig, - createCommentJson: '{"body":{{{case.comment}}}},,,,,,,', + createCommentUrl: `${casesWebhookSimulatorURL}/rest/api/2/issue/{{{external.system.id}}}e\\\\whoathisisbad4{}\{\{`, }, secrets, }); @@ -696,7 +696,7 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { ); }); - it('should respond with bad JSON error when create case comment JSON is bad', async () => { + it('should respond with bad URL error when create case comment URL is bad', async () => { await supertest .post(`/api/actions/connector/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') @@ -724,7 +724,7 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { retry: false, message: 'an error occurred while running the action', service_message: - '[Action][Webhook - Case Management]: Unable to create comment at case with id 123. Error: JSON Error: Create comment JSON body must be valid JSON. ', + '[Action][Webhook - Case Management]: Unable to create comment at case with id 123. Error: Invalid Create comment URL: error rendering mustache template "http://elastic:changeme@localhost:5620/api/_actions-FTS-external-service-simulators/jira/rest/api/2/issue/{{{external.system.id}}}e\\\\whoathisisbad4{}{{": Unclosed tag at 151. Error: Invalid URL. ', }); }); expect(proxyHaveBeenCalled).to.equal(true); // called for the create case successful call From 7a22fe574e7332739d2d0d824c5894bbbc311371 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 25 Jul 2022 09:21:00 -0600 Subject: [PATCH 87/97] err msg --- .../builtin_action_types/cases_webhook/utils.ts | 8 ++------ .../actions/builtin_action_types/cases_webhook.ts | 12 ++++++------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts index 2e04416a9d79f2..5833db7b6358e4 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/utils.ts @@ -17,12 +17,8 @@ export const createServiceError = (error: AxiosError, message: string) => { return new Error( getErrorMessage( i18n.NAME, - `${message}. Error: ${error.message}. ${ - serverResponse != null - ? serverResponse - : error.response?.statusText != null - ? `Reason: ${error.response?.statusText}` - : '' + `${message}. Error: ${error.message}. ${serverResponse != null ? serverResponse : ''} ${ + error.response?.statusText != null ? `Reason: ${error.response?.statusText}` : '' }` ) ); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/cases_webhook.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/cases_webhook.ts index faf0474a9a8f67..8e6d2fb33dce5b 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/cases_webhook.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/cases_webhook.ts @@ -463,7 +463,7 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { retry: false, message: 'an error occurred while running the action', service_message: - '[Action][Webhook - Case Management]: Unable to create case. Error: JSON Error: Create case JSON body must be valid JSON. ', + '[Action][Webhook - Case Management]: Unable to create case. Error: JSON Error: Create case JSON body must be valid JSON. ', }); }); expect(proxyHaveBeenCalled).to.equal(false); @@ -493,7 +493,7 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { retry: false, message: 'an error occurred while running the action', service_message: - '[Action][Webhook - Case Management]: Unable to update case with id 12345. Error: JSON Error: Update case JSON body must be valid JSON. ', + '[Action][Webhook - Case Management]: Unable to update case with id 12345. Error: JSON Error: Update case JSON body must be valid JSON. ', }); }); expect(proxyHaveBeenCalled).to.equal(false); @@ -560,7 +560,7 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { retry: false, message: 'an error occurred while running the action', service_message: - '[Action][Webhook - Case Management]: Unable to create comment at case with id 123. Error: JSON Error: Create comment JSON body must be valid JSON. ', + '[Action][Webhook - Case Management]: Unable to create comment at case with id 123. Error: JSON Error: Create comment JSON body must be valid JSON. ', }); }); expect(proxyHaveBeenCalled).to.equal(true); // called for the create case successful call @@ -627,7 +627,7 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { retry: false, message: 'an error occurred while running the action', service_message: - '[Action][Webhook - Case Management]: Unable to create case. Error: Invalid Create case URL: httpshttp://elastic:changeme@localhost:5620/api/_actions-FTS-external-service-simulators/jira. Error: Invalid protocol. ', + '[Action][Webhook - Case Management]: Unable to create case. Error: Invalid Create case URL: httpshttp://elastic:changeme@localhost:5620/api/_actions-FTS-external-service-simulators/jira. Error: Invalid protocol. ', }); }); expect(proxyHaveBeenCalled).to.equal(false); @@ -657,7 +657,7 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { retry: false, message: 'an error occurred while running the action', service_message: - '[Action][Webhook - Case Management]: Unable to update case with id 12345. Error: Invalid Update case URL: error rendering mustache template "http://elastic:changeme@localhost:5620/api/_actions-FTS-external-service-simulators/jira/rest/api/2/issue/{{{external.system.id}}}e\\\\whoathisisbad4{}{{": Unclosed tag at 151. Error: Invalid URL. ', + '[Action][Webhook - Case Management]: Unable to update case with id 12345. Error: Invalid Update case URL: error rendering mustache template "http://elastic:changeme@localhost:5620/api/_actions-FTS-external-service-simulators/jira/rest/api/2/issue/{{{external.system.id}}}e\\\\whoathisisbad4{}{{": Unclosed tag at 151. Error: Invalid URL. ', }); }); expect(proxyHaveBeenCalled).to.equal(false); @@ -724,7 +724,7 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { retry: false, message: 'an error occurred while running the action', service_message: - '[Action][Webhook - Case Management]: Unable to create comment at case with id 123. Error: Invalid Create comment URL: error rendering mustache template "http://elastic:changeme@localhost:5620/api/_actions-FTS-external-service-simulators/jira/rest/api/2/issue/{{{external.system.id}}}e\\\\whoathisisbad4{}{{": Unclosed tag at 151. Error: Invalid URL. ', + '[Action][Webhook - Case Management]: Unable to create comment at case with id 123. Error: Invalid Create comment URL: error rendering mustache template "http://elastic:changeme@localhost:5620/api/_actions-FTS-external-service-simulators/jira/rest/api/2/issue/{{{external.system.id}}}e\\\\whoathisisbad4{}{{": Unclosed tag at 151. Error: Invalid URL. ', }); }); expect(proxyHaveBeenCalled).to.equal(true); // called for the create case successful call From 9de3498577fde9463acb57de1719eeb0c8fb4918 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 25 Jul 2022 10:15:27 -0600 Subject: [PATCH 88/97] fix tests and nullable --- .../server/builtin_action_types/cases_webhook/schema.ts | 6 +++--- .../builtin_action_types/cases_webhook/service.test.ts | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts index c9a20ef820e138..fafa0a64101b63 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/schema.ts @@ -38,8 +38,8 @@ export const ExternalIncidentServiceConfiguration = { } ), updateIncidentJson: schema.string(), - createCommentUrl: schema.maybe(schema.string()), - createCommentMethod: schema.maybe( + createCommentUrl: schema.nullable(schema.string()), + createCommentMethod: schema.nullable( schema.oneOf( [ schema.literal(CasesWebhookMethods.POST), @@ -51,7 +51,7 @@ export const ExternalIncidentServiceConfiguration = { } ) ), - createCommentJson: schema.maybe(schema.string()), + createCommentJson: schema.nullable(schema.string()), headers: nullableType(HeadersSchema), hasAuth: schema.boolean({ defaultValue: true }), }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts index 2675530b660c97..2de06d1fd63262 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts @@ -167,7 +167,7 @@ describe('Cases webhook service', () => { throw error; }); await expect(service.getIncident('1')).rejects.toThrow( - '[Action][Webhook - Case Management]: Unable to get case with id 1. Error: An error has occurred. Reason: Required field' + '[Action][Webhook - Case Management]: Unable to get case with id 1. Error: An error has occurred. Reason: Required field' ); }); @@ -276,7 +276,7 @@ describe('Cases webhook service', () => { }); await expect(service.createIncident(incident)).rejects.toThrow( - '[Action][Webhook - Case Management]: Unable to create case. Error: An error has occurred. Reason: Required field' + '[Action][Webhook - Case Management]: Unable to create case. Error: An error has occurred. Reason: Required field' ); }); @@ -369,7 +369,7 @@ describe('Cases webhook service', () => { }); await expect(service.updateIncident(incident)).rejects.toThrow( - '[Action][Webhook - Case Management]: Unable to update case with id 1. Error: An error has occurred. Reason: Required field' + '[Action][Webhook - Case Management]: Unable to update case with id 1. Error: An error has occurred. Reason: Required field' ); }); @@ -439,7 +439,7 @@ describe('Cases webhook service', () => { }); await expect(service.createComment(commentReq)).rejects.toThrow( - '[Action][Webhook - Case Management]: Unable to create comment at case with id 1. Error: An error has occurred. Reason: Required field' + '[Action][Webhook - Case Management]: Unable to create comment at case with id 1. Error: An error has occurred. Reason: Required field' ); }); From da17ac14edeb4fdd981358e785fe5c536568de09 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 25 Jul 2022 11:01:52 -0600 Subject: [PATCH 89/97] add tests for bad url and bad protocol --- .../cases_webhook/service.test.ts | 147 ++++++++++++++++++ .../cases_webhook/service.ts | 2 +- 2 files changed, 148 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts index 2de06d1fd63262..7f5fb757cd295e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts @@ -483,4 +483,151 @@ describe('Cases webhook service', () => { expect(res).toBeUndefined(); }); }); + + describe('bad urls', () => { + beforeAll(() => { + service = createExternalService( + actionId, + { + config, + secrets, + }, + logger, + { + ...configurationUtilities, + ensureUriAllowed: jest.fn().mockImplementation(() => { + throw new Error('Uri not allowed'); + }), + } + ); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + test('getIncident- throws for bad url', async () => { + await expect(service.getIncident('whack')).rejects.toThrow( + '[Action][Webhook - Case Management]: Unable to get case with id whack. Error: Invalid Get case URL: https://siem-kibana.atlassian.net/rest/api/2/issue/whack. Error: error configuring connector action: Uri not allowed.' + ); + }); + test('createIncident- throws for bad url', async () => { + const incident = { + incident: { + title: 'title', + description: 'desc', + tags: ['hello', 'world'], + issueType: '10006', + priority: 'High', + parent: 'RJ-107', + }, + }; + + await expect(service.createIncident(incident)).rejects.toThrow( + '[Action][Webhook - Case Management]: Unable to create case. Error: Invalid Create case URL: https://siem-kibana.atlassian.net/rest/api/2/issue. Error: error configuring connector action: Uri not allowed.' + ); + }); + test('updateIncident- throws for bad url', async () => { + const incident = { + incidentId: '123', + incident: { + title: 'title', + description: 'desc', + tags: ['hello', 'world'], + issueType: '10006', + priority: 'High', + parent: 'RJ-107', + }, + }; + + await expect(service.updateIncident(incident)).rejects.toThrow( + '[Action][Webhook - Case Management]: Unable to update case with id 123. Error: Invalid Update case URL: https://siem-kibana.atlassian.net/rest/api/2/issue/123. Error: error configuring connector action: Uri not allowed.' + ); + }); + test('createComment- throws for bad url', async () => { + const commentReq = { + incidentId: '1', + comment: { + comment: 'comment', + commentId: 'comment-1', + }, + }; + await expect(service.createComment(commentReq)).rejects.toThrow( + '[Action][Webhook - Case Management]: Unable to create comment at case with id 1. Error: Invalid Create comment URL: https://siem-kibana.atlassian.net/rest/api/2/issue/1/comment. Error: error configuring connector action: Uri not allowed.' + ); + }); + }); + + describe('bad protocol', () => { + beforeAll(() => { + service = createExternalService( + actionId, + { + config: { + ...config, + getIncidentUrl: 'ftp://bad.com', + createIncidentUrl: 'ftp://bad.com', + updateIncidentUrl: 'ftp://bad.com', + createCommentUrl: 'ftp://bad.com', + }, + secrets, + }, + logger, + configurationUtilities + ); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + test('getIncident- throws for bad protocol', async () => { + await expect(service.getIncident('whack')).rejects.toThrow( + '[Action][Webhook - Case Management]: Unable to get case with id whack. Error: Invalid Get case URL: ftp://bad.com. Error: Invalid protocol.' + ); + }); + test('createIncident- throws for bad protocol', async () => { + const incident = { + incident: { + title: 'title', + description: 'desc', + tags: ['hello', 'world'], + issueType: '10006', + priority: 'High', + parent: 'RJ-107', + }, + }; + + await expect(service.createIncident(incident)).rejects.toThrow( + '[Action][Webhook - Case Management]: Unable to create case. Error: Invalid Create case URL: ftp://bad.com. Error: Invalid protocol.' + ); + }); + test('updateIncident- throws for bad protocol', async () => { + const incident = { + incidentId: '123', + incident: { + title: 'title', + description: 'desc', + tags: ['hello', 'world'], + issueType: '10006', + priority: 'High', + parent: 'RJ-107', + }, + }; + + await expect(service.updateIncident(incident)).rejects.toThrow( + '[Action][Webhook - Case Management]: Unable to update case with id 123. Error: Invalid Update case URL: ftp://bad.com. Error: Invalid protocol.' + ); + }); + test('createComment- throws for bad protocol', async () => { + const commentReq = { + incidentId: '1', + comment: { + comment: 'comment', + commentId: 'comment-1', + }, + }; + await expect(service.createComment(commentReq)).rejects.toThrow( + '[Action][Webhook - Case Management]: Unable to create comment at case with id 1. Error: Invalid Create comment URL: ftp://bad.com. Error: Invalid protocol.' + ); + }); + }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts index dd9644d708e467..a534d3e58041fa 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts @@ -259,7 +259,7 @@ export const createExternalService = ( const createComment = async ({ incidentId, comment }: CreateCommentParams): Promise => { try { - if (!createCommentUrl || !createCommentJson) { + if (!createCommentUrl || !createCommentJson || !createCommentMethod) { return; } const commentUrl = renderMustacheStringNoEscape(createCommentUrl, { From 31f1303d12665512483ff145f6206559c8dcbddf Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 25 Jul 2022 11:10:25 -0600 Subject: [PATCH 90/97] add test for showButtonTitle prop --- .../components/add_message_variables.test.tsx | 18 ++++++++++++++++++ .../components/add_message_variables.tsx | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.test.tsx index 39867ca690f0fc..ecc6de82514742 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.test.tsx @@ -126,4 +126,22 @@ describe('AddMessageVariables', () => { expect(wrapper.find('[data-test-subj="fooAddVariableButton"]')).toHaveLength(0); }); + + test('it renders button title when passed', () => { + const wrapper = mountWithIntl( + + ); + + expect(wrapper.find('[data-test-subj="fooAddVariableButton-Title"]').exists()).toEqual(true); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx index 0d8775853c215a..6cfcf09d7387ae 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx @@ -73,7 +73,7 @@ export const AddMessageVariables: React.FunctionComponent = ({ showButtonTitle ? ( setIsVariablesPopoverOpen(true)} iconType="indexOpen" From 73efd759a6f954206d2a53c74b3122096c5c8777 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 25 Jul 2022 12:28:51 -0600 Subject: [PATCH 91/97] rm url err --- .../cases_webhook/service.test.ts | 16 ++++++++-------- .../cases_webhook/validators.ts | 2 +- .../builtin_action_types/cases_webhook.ts | 6 +++--- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts index 7f5fb757cd295e..132a717c6068b8 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts @@ -507,7 +507,7 @@ describe('Cases webhook service', () => { }); test('getIncident- throws for bad url', async () => { await expect(service.getIncident('whack')).rejects.toThrow( - '[Action][Webhook - Case Management]: Unable to get case with id whack. Error: Invalid Get case URL: https://siem-kibana.atlassian.net/rest/api/2/issue/whack. Error: error configuring connector action: Uri not allowed.' + '[Action][Webhook - Case Management]: Unable to get case with id whack. Error: Invalid Get case URL: Error: error configuring connector action: Uri not allowed.' ); }); test('createIncident- throws for bad url', async () => { @@ -523,7 +523,7 @@ describe('Cases webhook service', () => { }; await expect(service.createIncident(incident)).rejects.toThrow( - '[Action][Webhook - Case Management]: Unable to create case. Error: Invalid Create case URL: https://siem-kibana.atlassian.net/rest/api/2/issue. Error: error configuring connector action: Uri not allowed.' + '[Action][Webhook - Case Management]: Unable to create case. Error: Invalid Create case URL: Error: error configuring connector action: Uri not allowed.' ); }); test('updateIncident- throws for bad url', async () => { @@ -540,7 +540,7 @@ describe('Cases webhook service', () => { }; await expect(service.updateIncident(incident)).rejects.toThrow( - '[Action][Webhook - Case Management]: Unable to update case with id 123. Error: Invalid Update case URL: https://siem-kibana.atlassian.net/rest/api/2/issue/123. Error: error configuring connector action: Uri not allowed.' + '[Action][Webhook - Case Management]: Unable to update case with id 123. Error: Invalid Update case URL: Error: error configuring connector action: Uri not allowed.' ); }); test('createComment- throws for bad url', async () => { @@ -552,7 +552,7 @@ describe('Cases webhook service', () => { }, }; await expect(service.createComment(commentReq)).rejects.toThrow( - '[Action][Webhook - Case Management]: Unable to create comment at case with id 1. Error: Invalid Create comment URL: https://siem-kibana.atlassian.net/rest/api/2/issue/1/comment. Error: error configuring connector action: Uri not allowed.' + '[Action][Webhook - Case Management]: Unable to create comment at case with id 1. Error: Invalid Create comment URL: Error: error configuring connector action: Uri not allowed.' ); }); }); @@ -581,7 +581,7 @@ describe('Cases webhook service', () => { }); test('getIncident- throws for bad protocol', async () => { await expect(service.getIncident('whack')).rejects.toThrow( - '[Action][Webhook - Case Management]: Unable to get case with id whack. Error: Invalid Get case URL: ftp://bad.com. Error: Invalid protocol.' + '[Action][Webhook - Case Management]: Unable to get case with id whack. Error: Invalid Get case URL: Error: Invalid protocol.' ); }); test('createIncident- throws for bad protocol', async () => { @@ -597,7 +597,7 @@ describe('Cases webhook service', () => { }; await expect(service.createIncident(incident)).rejects.toThrow( - '[Action][Webhook - Case Management]: Unable to create case. Error: Invalid Create case URL: ftp://bad.com. Error: Invalid protocol.' + '[Action][Webhook - Case Management]: Unable to create case. Error: Invalid Create case URL: Error: Invalid protocol.' ); }); test('updateIncident- throws for bad protocol', async () => { @@ -614,7 +614,7 @@ describe('Cases webhook service', () => { }; await expect(service.updateIncident(incident)).rejects.toThrow( - '[Action][Webhook - Case Management]: Unable to update case with id 123. Error: Invalid Update case URL: ftp://bad.com. Error: Invalid protocol.' + '[Action][Webhook - Case Management]: Unable to update case with id 123. Error: Invalid Update case URL: Error: Invalid protocol.' ); }); test('createComment- throws for bad protocol', async () => { @@ -626,7 +626,7 @@ describe('Cases webhook service', () => { }, }; await expect(service.createComment(commentReq)).rejects.toThrow( - '[Action][Webhook - Case Management]: Unable to create comment at case with id 1. Error: Invalid Create comment URL: ftp://bad.com. Error: Invalid protocol.' + '[Action][Webhook - Case Management]: Unable to create comment at case with id 1. Error: Invalid Create comment URL: Error: Invalid protocol.' ); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts index 98ab28444933e4..618ef2428f5fdd 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/validators.ts @@ -114,7 +114,7 @@ export const validateAndNormalizeUrl = ( ensureUriAllowed(url, configurationUtilities); return normalizeURL(url); } catch (e) { - throw Error(`Invalid ${urlDesc}: ${url}. ${e}`); + throw Error(`Invalid ${urlDesc}: ${e}`); } }; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/cases_webhook.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/cases_webhook.ts index 8e6d2fb33dce5b..89c92b22659837 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/cases_webhook.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/cases_webhook.ts @@ -627,7 +627,7 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { retry: false, message: 'an error occurred while running the action', service_message: - '[Action][Webhook - Case Management]: Unable to create case. Error: Invalid Create case URL: httpshttp://elastic:changeme@localhost:5620/api/_actions-FTS-external-service-simulators/jira. Error: Invalid protocol. ', + '[Action][Webhook - Case Management]: Unable to create case. Error: Invalid Create case URL: Error: Invalid protocol. ', }); }); expect(proxyHaveBeenCalled).to.equal(false); @@ -657,7 +657,7 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { retry: false, message: 'an error occurred while running the action', service_message: - '[Action][Webhook - Case Management]: Unable to update case with id 12345. Error: Invalid Update case URL: error rendering mustache template "http://elastic:changeme@localhost:5620/api/_actions-FTS-external-service-simulators/jira/rest/api/2/issue/{{{external.system.id}}}e\\\\whoathisisbad4{}{{": Unclosed tag at 151. Error: Invalid URL. ', + '[Action][Webhook - Case Management]: Unable to update case with id 12345. Error: Invalid Update case URL: Error: Invalid URL. ', }); }); expect(proxyHaveBeenCalled).to.equal(false); @@ -724,7 +724,7 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { retry: false, message: 'an error occurred while running the action', service_message: - '[Action][Webhook - Case Management]: Unable to create comment at case with id 123. Error: Invalid Create comment URL: error rendering mustache template "http://elastic:changeme@localhost:5620/api/_actions-FTS-external-service-simulators/jira/rest/api/2/issue/{{{external.system.id}}}e\\\\whoathisisbad4{}{{": Unclosed tag at 151. Error: Invalid URL. ', + '[Action][Webhook - Case Management]: Unable to create comment at case with id 123. Error: Invalid Create comment URL: Error: Invalid URL. ', }); }); expect(proxyHaveBeenCalled).to.equal(true); // called for the create case successful call From 2036e82e58181bafc16fe333cb00750229cb775d Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 25 Jul 2022 12:33:33 -0600 Subject: [PATCH 92/97] rm SecurityConnectorFeatureId --- .../server/builtin_action_types/cases_webhook/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts index a2584b33869bdc..c9b6c41d478cfb 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/index.ts @@ -8,7 +8,7 @@ import { curry } from 'lodash'; import { schema } from '@kbn/config-schema'; import { Logger } from '@kbn/core/server'; -import { CasesConnectorFeatureId, SecurityConnectorFeatureId } from '../../../common'; +import { CasesConnectorFeatureId } from '../../../common'; import { CasesWebhookActionParamsType, CasesWebhookExecutorResultData, @@ -60,7 +60,7 @@ export function getActionType({ connector: validate.connector, }, executor: curry(executor)({ logger, configurationUtilities }), - supportedFeatureIds: [CasesConnectorFeatureId, SecurityConnectorFeatureId], + supportedFeatureIds: [CasesConnectorFeatureId], }; } From 7d71f3ac38c9c98aa2e1f70d561b9eb0896e627e Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 25 Jul 2022 13:01:21 -0600 Subject: [PATCH 93/97] revert escape mustache vars in url --- .../cases_webhook/service.test.ts | 79 ++++++++++++++++++- .../server/lib/mustache_renderer.test.ts | 2 +- .../cases_webhook/action_variables.ts | 4 +- .../cases_webhook/steps/update.tsx | 10 ++- .../cases_webhook/webhook_connectors.test.tsx | 12 +-- .../builtin_action_types/cases_webhook.ts | 17 ++-- .../builtin_action_types/cases_webhook.ts | 29 ++++--- .../cases_api_integration/common/lib/utils.ts | 8 +- .../tests/trial/configure/get_connectors.ts | 8 +- .../tests/trial/configure/get_connectors.ts | 8 +- 10 files changed, 126 insertions(+), 51 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts index 132a717c6068b8..785c6f8e3c7c28 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts @@ -31,7 +31,7 @@ const config: CasesWebhookPublicConfigurationType = { createCommentJson: '{"body":{{{case.comment}}}}', createCommentMethod: CasesWebhookMethods.POST, createCommentUrl: - 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}/comment', + 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{external.system.id}}/comment', createIncidentJson: '{"fields":{"title":{{{case.title}}},"description":{{{case.description}}},"tags":{{{case.tags}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', createIncidentMethod: CasesWebhookMethods.POST, @@ -42,12 +42,12 @@ const config: CasesWebhookPublicConfigurationType = { getIncidentResponseUpdatedDateKey: 'fields.updated', hasAuth: true, headers: { ['content-type']: 'application/json' }, - incidentViewUrl: 'https://siem-kibana.atlassian.net/browse/{{{external.system.title}}}', - getIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}', + incidentViewUrl: 'https://siem-kibana.atlassian.net/browse/{{external.system.title}}', + getIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{external.system.id}}', updateIncidentJson: '{"fields":{"title":{{{case.title}}},"description":{{{case.description}}},"tags":{{{case.tags}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', updateIncidentMethod: CasesWebhookMethods.PUT, - updateIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}', + updateIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{external.system.id}}', }; const secrets = { user: 'user', @@ -630,4 +630,75 @@ describe('Cases webhook service', () => { ); }); }); + + describe('escape urls', () => { + beforeAll(() => { + service = createExternalService( + actionId, + { + config, + secrets, + }, + logger, + { + ...configurationUtilities, + } + ); + requestMock.mockImplementation(() => + createAxiosResponse({ + data: { + id: '1', + key: 'CK-1', + fields: { + updated: '2020-04-27T10:59:46.202Z', + created: '2020-04-27T10:59:46.202Z', + }, + }, + }) + ); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + test('getIncident- escapes url', async () => { + await service.getIncident('../../malicious-app/malicious-endpoint/'); + expect(requestMock.mock.calls[0][0].url).toEqual( + 'https://siem-kibana.atlassian.net/rest/api/2/issue/../../malicious-app/malicious-endpoint/' + ); + }); + + test('updateIncident- escapes url', async () => { + const incident = { + incidentId: '../../malicious-app/malicious-endpoint/', + incident: { + title: 'title', + description: 'desc', + tags: ['hello', 'world'], + issueType: '10006', + priority: 'High', + parent: 'RJ-107', + }, + }; + + await service.updateIncident(incident); + expect(requestMock.mock.calls[0][0].url).toEqual( + 'https://siem-kibana.atlassian.net/rest/api/2/issue/../../malicious-app/malicious-endpoint/' + ); + }); + test('createComment- escapes url', async () => { + const commentReq = { + incidentId: '../../malicious-app/malicious-endpoint/', + comment: { + comment: 'comment', + commentId: 'comment-1', + }, + }; + + await service.createComment(commentReq); + expect(requestMock.mock.calls[0][0].url).toEqual( + 'https://siem-kibana.atlassian.net/rest/api/2/issue/../../malicious-app/malicious-endpoint//comment' + ); + }); + }); }); diff --git a/x-pack/plugins/actions/server/lib/mustache_renderer.test.ts b/x-pack/plugins/actions/server/lib/mustache_renderer.test.ts index 154937c6f8065f..a37643faed7acc 100644 --- a/x-pack/plugins/actions/server/lib/mustache_renderer.test.ts +++ b/x-pack/plugins/actions/server/lib/mustache_renderer.test.ts @@ -131,7 +131,7 @@ describe('mustache_renderer', () => { const summary = 'A cool good summary'; const description = 'A cool good description'; const tags = ['cool', 'neat', 'nice']; - const str = 'https://siem-kibana.atlassian.net/browse/{{{external.system.title}}}'; + const str = 'https://siem-kibana.atlassian.net/browse/{{external.system.title}}'; const objStr = '{\n' + diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/action_variables.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/action_variables.ts index 7ec8b3e3d5f6d1..abd3e69ee8676d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/action_variables.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/action_variables.ts @@ -30,7 +30,7 @@ export const urlVars: ActionVariable[] = [ { name: 'external.system.id', description: i18n.EXTERNAL_ID_DESC, - useWithTripleBracesInTemplates: true, + useWithTripleBracesInTemplates: false, }, ]; @@ -39,6 +39,6 @@ export const urlVarsExt: ActionVariable[] = [ { name: 'external.system.title', description: i18n.EXTERNAL_TITLE_DESC, - useWithTripleBracesInTemplates: true, + useWithTripleBracesInTemplates: false, }, ]; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/update.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/update.tsx index c8bfa4ad350b13..d7cb12f1779c11 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/update.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/update.tsx @@ -106,7 +106,10 @@ export const UpdateStep: FunctionComponent = ({ display, readOnly }) => ( 'data-test-subj': 'webhookUpdateIncidentJson', ['aria-label']: i18n.CODE_EDITOR, }, - messageVariables: [...casesVars, ...urlVars], + messageVariables: [ + ...casesVars, + ...urlVars.map((urlVar) => ({ ...urlVar, useWithTripleBracesInTemplates: true })), + ], paramsProperty: 'updateIncidentJson', buttonTitle: i18n.ADD_CASES_VARIABLE, showButtonTitle: true, @@ -193,7 +196,10 @@ export const UpdateStep: FunctionComponent = ({ display, readOnly }) => ( 'data-test-subj': 'webhookCreateCommentJson', ['aria-label']: i18n.CODE_EDITOR, }, - messageVariables: [...commentVars, ...urlVars], + messageVariables: [ + ...commentVars, + ...urlVars.map((urlVar) => ({ ...urlVar, useWithTripleBracesInTemplates: true })), + ], paramsProperty: 'createCommentJson', buttonTitle: i18n.ADD_CASES_VARIABLE, showButtonTitle: true, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx index 3ce481f3b3466c..5384ec7e072911 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx @@ -42,7 +42,7 @@ const config = { createCommentJson: '{"body":{{{case.comment}}}}', createCommentMethod: 'post', createCommentUrl: - 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}/comment', + 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{external.system.id}}/comment', createIncidentJson: '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', createIncidentMethod: 'post', @@ -53,12 +53,12 @@ const config = { getIncidentResponseUpdatedDateKey: 'fields.updated', hasAuth: true, headers: [{ key: 'content-type', value: 'text' }], - incidentViewUrl: 'https://siem-kibana.atlassian.net/browse/{{{external.system.title}}}', - getIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}', + incidentViewUrl: 'https://siem-kibana.atlassian.net/browse/{{external.system.title}}', + getIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{external.system.id}}', updateIncidentJson: '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', updateIncidentMethod: 'put', - updateIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}', + updateIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{external.system.id}}', }; const actionConnector = { secrets: { @@ -359,9 +359,9 @@ describe('CasesWebhookActionConnectorFields renders', () => { [ 'incidentViewUrl', 'https://missingexternalid.com', - ['{{{external.system.id}}}', '{{{external.system.title}}}'], + ['{{external.system.id}}', '{{external.system.title}}'], ], - ['getIncidentUrl', 'https://missingexternalid.com', ['{{{external.system.id}}}']], + ['getIncidentUrl', 'https://missingexternalid.com', ['{{external.system.id}}']], ]; it('connector validation succeeds when connector config is valid', async () => { diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/cases_webhook.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/cases_webhook.ts index f767382a779ac2..d30f7cd7076532 100644 --- a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/cases_webhook.ts +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/cases_webhook.ts @@ -21,7 +21,7 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { createCommentJson: '{"body":{{{case.comment}}}}', createCommentMethod: 'post', createCommentUrl: - 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}/comment', + 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{external.system.id}}/comment', createIncidentJson: '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"labels":{{{case.tags}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', createIncidentMethod: 'post', @@ -32,13 +32,12 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { getIncidentResponseUpdatedDateKey: 'fields.updated', hasAuth: true, headers: { ['content-type']: 'application/json' }, - incidentViewUrl: 'https://siem-kibana.atlassian.net/browse/{{{external.system.title}}}', - getIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}', + incidentViewUrl: 'https://siem-kibana.atlassian.net/browse/{{external.system.title}}', + getIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{external.system.id}}', updateIncidentJson: '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"labels":{{{case.tags}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', updateIncidentMethod: 'put', - updateIncidentUrl: - 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}', + updateIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{external.system.id}}', }; const mockCasesWebhook = { @@ -79,11 +78,11 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { actionTypeId: '.cases-webhook', config: { ...config, - createCommentUrl: `${casesWebhookSimulatorURL}/{{{external.system.id}}}/comments`, + createCommentUrl: `${casesWebhookSimulatorURL}/{{external.system.id}}/comments`, createIncidentUrl: casesWebhookSimulatorURL, - incidentViewUrl: `${casesWebhookSimulatorURL}/{{{external.system.title}}}`, - getIncidentUrl: `${casesWebhookSimulatorURL}/{{{external.system.id}}}`, - updateIncidentUrl: `${casesWebhookSimulatorURL}/{{{external.system.id}}}`, + incidentViewUrl: `${casesWebhookSimulatorURL}/{{external.system.title}}`, + getIncidentUrl: `${casesWebhookSimulatorURL}/{{external.system.id}}`, + updateIncidentUrl: `${casesWebhookSimulatorURL}/{{external.system.id}}`, }, secrets: mockCasesWebhook.secrets, }) diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/cases_webhook.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/cases_webhook.ts index 89c92b22659837..637535fb51f37e 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/cases_webhook.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/cases_webhook.ts @@ -25,7 +25,7 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { createCommentJson: '{"body":{{{case.comment}}}}', createCommentMethod: 'post', createCommentUrl: - 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}/comment', + 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{external.system.id}}/comment', createIncidentJson: '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"labels":{{{case.tags}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', createIncidentMethod: 'post', @@ -36,13 +36,12 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { getIncidentResponseUpdatedDateKey: 'fields.updated', hasAuth: true, headers: { ['content-type']: 'application/json', ['kbn-xsrf']: 'abcd' }, - incidentViewUrl: 'https://siem-kibana.atlassian.net/browse/{{{external.system.title}}}', - getIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}', + incidentViewUrl: 'https://siem-kibana.atlassian.net/browse/{{external.system.title}}', + getIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{external.system.id}}', updateIncidentJson: '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"labels":{{{case.tags}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', updateIncidentMethod: 'put', - updateIncidentUrl: - 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}', + updateIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{external.system.id}}', }; const requiredFields = [ 'createIncidentJson', @@ -92,11 +91,11 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { simulatorConfig = { ...mockCasesWebhook.config, - createCommentUrl: `${casesWebhookSimulatorURL}/rest/api/2/issue/{{{external.system.id}}}/comment`, + createCommentUrl: `${casesWebhookSimulatorURL}/rest/api/2/issue/{{external.system.id}}/comment`, createIncidentUrl: `${casesWebhookSimulatorURL}/rest/api/2/issue`, - incidentViewUrl: `${casesWebhookSimulatorURL}/browse/{{{external.system.title}}}`, - getIncidentUrl: `${casesWebhookSimulatorURL}/rest/api/2/issue/{{{external.system.id}}}`, - updateIncidentUrl: `${casesWebhookSimulatorURL}/rest/api/2/issue/{{{external.system.id}}}`, + incidentViewUrl: `${casesWebhookSimulatorURL}/browse/{{external.system.title}}`, + getIncidentUrl: `${casesWebhookSimulatorURL}/rest/api/2/issue/{{external.system.id}}`, + updateIncidentUrl: `${casesWebhookSimulatorURL}/rest/api/2/issue/{{external.system.id}}`, }; }); describe('CasesWebhook - Action Creation', () => { @@ -172,11 +171,11 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { connector_type_id: '.cases-webhook', config: { ...mockCasesWebhook.config, - createCommentUrl: `${badUrl}/{{{external.system.id}}}/comments`, + createCommentUrl: `${badUrl}/{{external.system.id}}/comments`, createIncidentUrl: badUrl, - incidentViewUrl: `${badUrl}/{{{external.system.title}}}`, - getIncidentUrl: `${badUrl}/{{{external.system.id}}}`, - updateIncidentUrl: `${badUrl}/{{{external.system.id}}}`, + incidentViewUrl: `${badUrl}/{{external.system.title}}`, + getIncidentUrl: `${badUrl}/{{external.system.id}}`, + updateIncidentUrl: `${badUrl}/{{external.system.id}}`, }, secrets, }) @@ -589,7 +588,7 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { config: { ...simulatorConfig, createIncidentUrl: `https${casesWebhookSimulatorURL}`, - updateIncidentUrl: `${casesWebhookSimulatorURL}/rest/api/2/issue/{{{external.system.id}}}e\\\\whoathisisbad4{}\{\{`, + updateIncidentUrl: `${casesWebhookSimulatorURL}/rest/api/2/issue/{{external.system.id}}e\\\\whoathisisbad4{}\{\{`, }, secrets, }); @@ -681,7 +680,7 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { connector_type_id: '.cases-webhook', config: { ...simulatorConfig, - createCommentUrl: `${casesWebhookSimulatorURL}/rest/api/2/issue/{{{external.system.id}}}e\\\\whoathisisbad4{}\{\{`, + createCommentUrl: `${casesWebhookSimulatorURL}/rest/api/2/issue/{{external.system.id}}e\\\\whoathisisbad4{}\{\{`, }, secrets, }); diff --git a/x-pack/test/cases_api_integration/common/lib/utils.ts b/x-pack/test/cases_api_integration/common/lib/utils.ts index 5f68fafff85a45..92d8f691d4947b 100644 --- a/x-pack/test/cases_api_integration/common/lib/utils.ts +++ b/x-pack/test/cases_api_integration/common/lib/utils.ts @@ -260,7 +260,7 @@ export const getCasesWebhookConnector = () => ({ config: { createCommentJson: '{"body":{{{case.comment}}}}', createCommentMethod: 'post', - createCommentUrl: 'http://some.non.existent.com/{{{external.system.id}}}/comment', + createCommentUrl: 'http://some.non.existent.com/{{external.system.id}}/comment', createIncidentJson: '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', createIncidentMethod: 'post', @@ -271,12 +271,12 @@ export const getCasesWebhookConnector = () => ({ getIncidentResponseUpdatedDateKey: 'fields.updated', hasAuth: true, headers: { [`content-type`]: 'application/json' }, - incidentViewUrl: 'http://some.non.existent.com/browse/{{{external.system.title}}}', - getIncidentUrl: 'http://some.non.existent.com/{{{external.system.id}}}', + incidentViewUrl: 'http://some.non.existent.com/browse/{{external.system.title}}', + getIncidentUrl: 'http://some.non.existent.com/{{external.system.id}}', updateIncidentJson: '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', updateIncidentMethod: 'put', - updateIncidentUrl: 'http://some.non.existent.com/{{{external.system.id}}}', + updateIncidentUrl: 'http://some.non.existent.com/{{external.system.id}}', }, }); diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts index 4109913d9f4fb3..b3886ef32d6260 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts @@ -65,7 +65,7 @@ export default ({ getService }: FtrProviderContext): void => { config: { createCommentJson: '{"body":{{{case.comment}}}}', createCommentMethod: 'post', - createCommentUrl: 'http://some.non.existent.com/{{{external.system.id}}}/comment', + createCommentUrl: 'http://some.non.existent.com/{{external.system.id}}/comment', createIncidentJson: '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', createIncidentMethod: 'post', @@ -76,12 +76,12 @@ export default ({ getService }: FtrProviderContext): void => { getIncidentResponseUpdatedDateKey: 'fields.updated', hasAuth: true, headers: { [`content-type`]: 'application/json' }, - incidentViewUrl: 'http://some.non.existent.com/browse/{{{external.system.title}}}', - getIncidentUrl: 'http://some.non.existent.com/{{{external.system.id}}}', + incidentViewUrl: 'http://some.non.existent.com/browse/{{external.system.title}}', + getIncidentUrl: 'http://some.non.existent.com/{{external.system.id}}', updateIncidentJson: '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', updateIncidentMethod: 'put', - updateIncidentUrl: 'http://some.non.existent.com/{{{external.system.id}}}', + updateIncidentUrl: 'http://some.non.existent.com/{{external.system.id}}', }, isPreconfigured: false, isDeprecated: false, diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/get_connectors.ts b/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/get_connectors.ts index c6d2165f2bf8b3..959f43facd1d26 100644 --- a/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/get_connectors.ts +++ b/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/get_connectors.ts @@ -99,7 +99,7 @@ export default ({ getService }: FtrProviderContext): void => { config: { createCommentJson: '{"body":{{{case.comment}}}}', createCommentMethod: 'post', - createCommentUrl: 'http://some.non.existent.com/{{{external.system.id}}}/comment', + createCommentUrl: 'http://some.non.existent.com/{{external.system.id}}/comment', createIncidentJson: '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', createIncidentMethod: 'post', @@ -110,12 +110,12 @@ export default ({ getService }: FtrProviderContext): void => { getIncidentResponseUpdatedDateKey: 'fields.updated', hasAuth: true, headers: { [`content-type`]: 'application/json' }, - incidentViewUrl: 'http://some.non.existent.com/browse/{{{external.system.title}}}', - getIncidentUrl: 'http://some.non.existent.com/{{{external.system.id}}}', + incidentViewUrl: 'http://some.non.existent.com/browse/{{external.system.title}}', + getIncidentUrl: 'http://some.non.existent.com/{{external.system.id}}', updateIncidentJson: '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', updateIncidentMethod: 'put', - updateIncidentUrl: 'http://some.non.existent.com/{{{external.system.id}}}', + updateIncidentUrl: 'http://some.non.existent.com/{{external.system.id}}', }, isPreconfigured: false, isDeprecated: false, From 9aee3f37fb9008f9efbe95ad43d1a09c4ba51d52 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 25 Jul 2022 13:05:19 -0600 Subject: [PATCH 94/97] better esc --- .../cases_webhook/service.test.ts | 8 ++++---- .../builtin_action_types/cases_webhook/service.ts | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts index 785c6f8e3c7c28..9c993ec9bdef73 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts @@ -42,7 +42,7 @@ const config: CasesWebhookPublicConfigurationType = { getIncidentResponseUpdatedDateKey: 'fields.updated', hasAuth: true, headers: { ['content-type']: 'application/json' }, - incidentViewUrl: 'https://siem-kibana.atlassian.net/browse/{{external.system.title}}', + incidentViewUrl: 'https://siem-kibana.atlassian.net/browse/{{{external.system.title}}}', getIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{external.system.id}}', updateIncidentJson: '{"fields":{"title":{{{case.title}}},"description":{{{case.description}}},"tags":{{{case.tags}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', @@ -664,7 +664,7 @@ describe('Cases webhook service', () => { test('getIncident- escapes url', async () => { await service.getIncident('../../malicious-app/malicious-endpoint/'); expect(requestMock.mock.calls[0][0].url).toEqual( - 'https://siem-kibana.atlassian.net/rest/api/2/issue/../../malicious-app/malicious-endpoint/' + 'https://siem-kibana.atlassian.net/rest/api/2/issue/..%2F..%2Fmalicious-app%2Fmalicious-endpoint%2F' ); }); @@ -683,7 +683,7 @@ describe('Cases webhook service', () => { await service.updateIncident(incident); expect(requestMock.mock.calls[0][0].url).toEqual( - 'https://siem-kibana.atlassian.net/rest/api/2/issue/../../malicious-app/malicious-endpoint/' + 'https://siem-kibana.atlassian.net/rest/api/2/issue/..%2F..%2Fmalicious-app%2Fmalicious-endpoint%2F' ); }); test('createComment- escapes url', async () => { @@ -697,7 +697,7 @@ describe('Cases webhook service', () => { await service.createComment(commentReq); expect(requestMock.mock.calls[0][0].url).toEqual( - 'https://siem-kibana.atlassian.net/rest/api/2/issue/../../malicious-app/malicious-endpoint//comment' + 'https://siem-kibana.atlassian.net/rest/api/2/issue/..%2F..%2Fmalicious-app%2Fmalicious-endpoint%2F/comment' ); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts index a534d3e58041fa..04b3e2fdbaff98 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.ts @@ -88,7 +88,7 @@ export const createExternalService = ( const getUrl = renderMustacheStringNoEscape(getIncidentUrl, { external: { system: { - id, + id: encodeURIComponent(id), }, }, }); @@ -166,8 +166,8 @@ export const createExternalService = ( const viewUrl = renderMustacheStringNoEscape(incidentViewUrl, { external: { system: { - id: externalId, - title: insertedIncident.title, + id: encodeURIComponent(externalId), + title: encodeURIComponent(insertedIncident.title), }, }, }); @@ -195,7 +195,7 @@ export const createExternalService = ( const updateUrl = renderMustacheStringNoEscape(updateIncidentUrl, { external: { system: { - id: incidentId, + id: encodeURIComponent(incidentId), }, }, }); @@ -236,8 +236,8 @@ export const createExternalService = ( const viewUrl = renderMustacheStringNoEscape(incidentViewUrl, { external: { system: { - id: incidentId, - title: updatedIncident.title, + id: encodeURIComponent(incidentId), + title: encodeURIComponent(updatedIncident.title), }, }, }); @@ -265,7 +265,7 @@ export const createExternalService = ( const commentUrl = renderMustacheStringNoEscape(createCommentUrl, { external: { system: { - id: incidentId, + id: encodeURIComponent(incidentId), }, }, }); From 485e86fa72c0aab321f81efd204e0af3de61b1ba Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 25 Jul 2022 13:09:20 -0600 Subject: [PATCH 95/97] revert triple to double --- .../cases_webhook/service.test.ts | 6 ++-- .../server/lib/mustache_renderer.test.ts | 2 +- .../cases_webhook/action_variables.ts | 4 +-- .../cases_webhook/steps/update.tsx | 10 ++----- .../cases_webhook/webhook_connectors.test.tsx | 8 ++--- .../builtin_action_types/cases_webhook.ts | 17 ++++++----- .../builtin_action_types/cases_webhook.ts | 29 ++++++++++--------- .../cases_api_integration/common/lib/utils.ts | 8 ++--- .../tests/trial/configure/get_connectors.ts | 8 ++--- .../tests/trial/configure/get_connectors.ts | 8 ++--- 10 files changed, 48 insertions(+), 52 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts index 9c993ec9bdef73..ff91374c8dfac6 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts @@ -31,7 +31,7 @@ const config: CasesWebhookPublicConfigurationType = { createCommentJson: '{"body":{{{case.comment}}}}', createCommentMethod: CasesWebhookMethods.POST, createCommentUrl: - 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{external.system.id}}/comment', + 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}/comment', createIncidentJson: '{"fields":{"title":{{{case.title}}},"description":{{{case.description}}},"tags":{{{case.tags}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', createIncidentMethod: CasesWebhookMethods.POST, @@ -43,11 +43,11 @@ const config: CasesWebhookPublicConfigurationType = { hasAuth: true, headers: { ['content-type']: 'application/json' }, incidentViewUrl: 'https://siem-kibana.atlassian.net/browse/{{{external.system.title}}}', - getIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{external.system.id}}', + getIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}', updateIncidentJson: '{"fields":{"title":{{{case.title}}},"description":{{{case.description}}},"tags":{{{case.tags}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', updateIncidentMethod: CasesWebhookMethods.PUT, - updateIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{external.system.id}}', + updateIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}', }; const secrets = { user: 'user', diff --git a/x-pack/plugins/actions/server/lib/mustache_renderer.test.ts b/x-pack/plugins/actions/server/lib/mustache_renderer.test.ts index a37643faed7acc..154937c6f8065f 100644 --- a/x-pack/plugins/actions/server/lib/mustache_renderer.test.ts +++ b/x-pack/plugins/actions/server/lib/mustache_renderer.test.ts @@ -131,7 +131,7 @@ describe('mustache_renderer', () => { const summary = 'A cool good summary'; const description = 'A cool good description'; const tags = ['cool', 'neat', 'nice']; - const str = 'https://siem-kibana.atlassian.net/browse/{{external.system.title}}'; + const str = 'https://siem-kibana.atlassian.net/browse/{{{external.system.title}}}'; const objStr = '{\n' + diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/action_variables.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/action_variables.ts index abd3e69ee8676d..7ec8b3e3d5f6d1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/action_variables.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/action_variables.ts @@ -30,7 +30,7 @@ export const urlVars: ActionVariable[] = [ { name: 'external.system.id', description: i18n.EXTERNAL_ID_DESC, - useWithTripleBracesInTemplates: false, + useWithTripleBracesInTemplates: true, }, ]; @@ -39,6 +39,6 @@ export const urlVarsExt: ActionVariable[] = [ { name: 'external.system.title', description: i18n.EXTERNAL_TITLE_DESC, - useWithTripleBracesInTemplates: false, + useWithTripleBracesInTemplates: true, }, ]; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/update.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/update.tsx index d7cb12f1779c11..c8bfa4ad350b13 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/update.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/steps/update.tsx @@ -106,10 +106,7 @@ export const UpdateStep: FunctionComponent = ({ display, readOnly }) => ( 'data-test-subj': 'webhookUpdateIncidentJson', ['aria-label']: i18n.CODE_EDITOR, }, - messageVariables: [ - ...casesVars, - ...urlVars.map((urlVar) => ({ ...urlVar, useWithTripleBracesInTemplates: true })), - ], + messageVariables: [...casesVars, ...urlVars], paramsProperty: 'updateIncidentJson', buttonTitle: i18n.ADD_CASES_VARIABLE, showButtonTitle: true, @@ -196,10 +193,7 @@ export const UpdateStep: FunctionComponent = ({ display, readOnly }) => ( 'data-test-subj': 'webhookCreateCommentJson', ['aria-label']: i18n.CODE_EDITOR, }, - messageVariables: [ - ...commentVars, - ...urlVars.map((urlVar) => ({ ...urlVar, useWithTripleBracesInTemplates: true })), - ], + messageVariables: [...commentVars, ...urlVars], paramsProperty: 'createCommentJson', buttonTitle: i18n.ADD_CASES_VARIABLE, showButtonTitle: true, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx index 5384ec7e072911..e40e551d826f28 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx @@ -42,7 +42,7 @@ const config = { createCommentJson: '{"body":{{{case.comment}}}}', createCommentMethod: 'post', createCommentUrl: - 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{external.system.id}}/comment', + 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}/comment', createIncidentJson: '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', createIncidentMethod: 'post', @@ -53,12 +53,12 @@ const config = { getIncidentResponseUpdatedDateKey: 'fields.updated', hasAuth: true, headers: [{ key: 'content-type', value: 'text' }], - incidentViewUrl: 'https://siem-kibana.atlassian.net/browse/{{external.system.title}}', - getIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{external.system.id}}', + incidentViewUrl: 'https://siem-kibana.atlassian.net/browse/{{{external.system.title}}}', + getIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}', updateIncidentJson: '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', updateIncidentMethod: 'put', - updateIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{external.system.id}}', + updateIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}', }; const actionConnector = { secrets: { diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/cases_webhook.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/cases_webhook.ts index d30f7cd7076532..f767382a779ac2 100644 --- a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/cases_webhook.ts +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/cases_webhook.ts @@ -21,7 +21,7 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { createCommentJson: '{"body":{{{case.comment}}}}', createCommentMethod: 'post', createCommentUrl: - 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{external.system.id}}/comment', + 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}/comment', createIncidentJson: '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"labels":{{{case.tags}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', createIncidentMethod: 'post', @@ -32,12 +32,13 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { getIncidentResponseUpdatedDateKey: 'fields.updated', hasAuth: true, headers: { ['content-type']: 'application/json' }, - incidentViewUrl: 'https://siem-kibana.atlassian.net/browse/{{external.system.title}}', - getIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{external.system.id}}', + incidentViewUrl: 'https://siem-kibana.atlassian.net/browse/{{{external.system.title}}}', + getIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}', updateIncidentJson: '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"labels":{{{case.tags}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', updateIncidentMethod: 'put', - updateIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{external.system.id}}', + updateIncidentUrl: + 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}', }; const mockCasesWebhook = { @@ -78,11 +79,11 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { actionTypeId: '.cases-webhook', config: { ...config, - createCommentUrl: `${casesWebhookSimulatorURL}/{{external.system.id}}/comments`, + createCommentUrl: `${casesWebhookSimulatorURL}/{{{external.system.id}}}/comments`, createIncidentUrl: casesWebhookSimulatorURL, - incidentViewUrl: `${casesWebhookSimulatorURL}/{{external.system.title}}`, - getIncidentUrl: `${casesWebhookSimulatorURL}/{{external.system.id}}`, - updateIncidentUrl: `${casesWebhookSimulatorURL}/{{external.system.id}}`, + incidentViewUrl: `${casesWebhookSimulatorURL}/{{{external.system.title}}}`, + getIncidentUrl: `${casesWebhookSimulatorURL}/{{{external.system.id}}}`, + updateIncidentUrl: `${casesWebhookSimulatorURL}/{{{external.system.id}}}`, }, secrets: mockCasesWebhook.secrets, }) diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/cases_webhook.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/cases_webhook.ts index 637535fb51f37e..89c92b22659837 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/cases_webhook.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/cases_webhook.ts @@ -25,7 +25,7 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { createCommentJson: '{"body":{{{case.comment}}}}', createCommentMethod: 'post', createCommentUrl: - 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{external.system.id}}/comment', + 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}/comment', createIncidentJson: '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"labels":{{{case.tags}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', createIncidentMethod: 'post', @@ -36,12 +36,13 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { getIncidentResponseUpdatedDateKey: 'fields.updated', hasAuth: true, headers: { ['content-type']: 'application/json', ['kbn-xsrf']: 'abcd' }, - incidentViewUrl: 'https://siem-kibana.atlassian.net/browse/{{external.system.title}}', - getIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{external.system.id}}', + incidentViewUrl: 'https://siem-kibana.atlassian.net/browse/{{{external.system.title}}}', + getIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}', updateIncidentJson: '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"labels":{{{case.tags}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', updateIncidentMethod: 'put', - updateIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{external.system.id}}', + updateIncidentUrl: + 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}', }; const requiredFields = [ 'createIncidentJson', @@ -91,11 +92,11 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { simulatorConfig = { ...mockCasesWebhook.config, - createCommentUrl: `${casesWebhookSimulatorURL}/rest/api/2/issue/{{external.system.id}}/comment`, + createCommentUrl: `${casesWebhookSimulatorURL}/rest/api/2/issue/{{{external.system.id}}}/comment`, createIncidentUrl: `${casesWebhookSimulatorURL}/rest/api/2/issue`, - incidentViewUrl: `${casesWebhookSimulatorURL}/browse/{{external.system.title}}`, - getIncidentUrl: `${casesWebhookSimulatorURL}/rest/api/2/issue/{{external.system.id}}`, - updateIncidentUrl: `${casesWebhookSimulatorURL}/rest/api/2/issue/{{external.system.id}}`, + incidentViewUrl: `${casesWebhookSimulatorURL}/browse/{{{external.system.title}}}`, + getIncidentUrl: `${casesWebhookSimulatorURL}/rest/api/2/issue/{{{external.system.id}}}`, + updateIncidentUrl: `${casesWebhookSimulatorURL}/rest/api/2/issue/{{{external.system.id}}}`, }; }); describe('CasesWebhook - Action Creation', () => { @@ -171,11 +172,11 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { connector_type_id: '.cases-webhook', config: { ...mockCasesWebhook.config, - createCommentUrl: `${badUrl}/{{external.system.id}}/comments`, + createCommentUrl: `${badUrl}/{{{external.system.id}}}/comments`, createIncidentUrl: badUrl, - incidentViewUrl: `${badUrl}/{{external.system.title}}`, - getIncidentUrl: `${badUrl}/{{external.system.id}}`, - updateIncidentUrl: `${badUrl}/{{external.system.id}}`, + incidentViewUrl: `${badUrl}/{{{external.system.title}}}`, + getIncidentUrl: `${badUrl}/{{{external.system.id}}}`, + updateIncidentUrl: `${badUrl}/{{{external.system.id}}}`, }, secrets, }) @@ -588,7 +589,7 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { config: { ...simulatorConfig, createIncidentUrl: `https${casesWebhookSimulatorURL}`, - updateIncidentUrl: `${casesWebhookSimulatorURL}/rest/api/2/issue/{{external.system.id}}e\\\\whoathisisbad4{}\{\{`, + updateIncidentUrl: `${casesWebhookSimulatorURL}/rest/api/2/issue/{{{external.system.id}}}e\\\\whoathisisbad4{}\{\{`, }, secrets, }); @@ -680,7 +681,7 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { connector_type_id: '.cases-webhook', config: { ...simulatorConfig, - createCommentUrl: `${casesWebhookSimulatorURL}/rest/api/2/issue/{{external.system.id}}e\\\\whoathisisbad4{}\{\{`, + createCommentUrl: `${casesWebhookSimulatorURL}/rest/api/2/issue/{{{external.system.id}}}e\\\\whoathisisbad4{}\{\{`, }, secrets, }); diff --git a/x-pack/test/cases_api_integration/common/lib/utils.ts b/x-pack/test/cases_api_integration/common/lib/utils.ts index 92d8f691d4947b..5f68fafff85a45 100644 --- a/x-pack/test/cases_api_integration/common/lib/utils.ts +++ b/x-pack/test/cases_api_integration/common/lib/utils.ts @@ -260,7 +260,7 @@ export const getCasesWebhookConnector = () => ({ config: { createCommentJson: '{"body":{{{case.comment}}}}', createCommentMethod: 'post', - createCommentUrl: 'http://some.non.existent.com/{{external.system.id}}/comment', + createCommentUrl: 'http://some.non.existent.com/{{{external.system.id}}}/comment', createIncidentJson: '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', createIncidentMethod: 'post', @@ -271,12 +271,12 @@ export const getCasesWebhookConnector = () => ({ getIncidentResponseUpdatedDateKey: 'fields.updated', hasAuth: true, headers: { [`content-type`]: 'application/json' }, - incidentViewUrl: 'http://some.non.existent.com/browse/{{external.system.title}}', - getIncidentUrl: 'http://some.non.existent.com/{{external.system.id}}', + incidentViewUrl: 'http://some.non.existent.com/browse/{{{external.system.title}}}', + getIncidentUrl: 'http://some.non.existent.com/{{{external.system.id}}}', updateIncidentJson: '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', updateIncidentMethod: 'put', - updateIncidentUrl: 'http://some.non.existent.com/{{external.system.id}}', + updateIncidentUrl: 'http://some.non.existent.com/{{{external.system.id}}}', }, }); diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts index b3886ef32d6260..4109913d9f4fb3 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts @@ -65,7 +65,7 @@ export default ({ getService }: FtrProviderContext): void => { config: { createCommentJson: '{"body":{{{case.comment}}}}', createCommentMethod: 'post', - createCommentUrl: 'http://some.non.existent.com/{{external.system.id}}/comment', + createCommentUrl: 'http://some.non.existent.com/{{{external.system.id}}}/comment', createIncidentJson: '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', createIncidentMethod: 'post', @@ -76,12 +76,12 @@ export default ({ getService }: FtrProviderContext): void => { getIncidentResponseUpdatedDateKey: 'fields.updated', hasAuth: true, headers: { [`content-type`]: 'application/json' }, - incidentViewUrl: 'http://some.non.existent.com/browse/{{external.system.title}}', - getIncidentUrl: 'http://some.non.existent.com/{{external.system.id}}', + incidentViewUrl: 'http://some.non.existent.com/browse/{{{external.system.title}}}', + getIncidentUrl: 'http://some.non.existent.com/{{{external.system.id}}}', updateIncidentJson: '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', updateIncidentMethod: 'put', - updateIncidentUrl: 'http://some.non.existent.com/{{external.system.id}}', + updateIncidentUrl: 'http://some.non.existent.com/{{{external.system.id}}}', }, isPreconfigured: false, isDeprecated: false, diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/get_connectors.ts b/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/get_connectors.ts index 959f43facd1d26..c6d2165f2bf8b3 100644 --- a/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/get_connectors.ts +++ b/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/get_connectors.ts @@ -99,7 +99,7 @@ export default ({ getService }: FtrProviderContext): void => { config: { createCommentJson: '{"body":{{{case.comment}}}}', createCommentMethod: 'post', - createCommentUrl: 'http://some.non.existent.com/{{external.system.id}}/comment', + createCommentUrl: 'http://some.non.existent.com/{{{external.system.id}}}/comment', createIncidentJson: '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', createIncidentMethod: 'post', @@ -110,12 +110,12 @@ export default ({ getService }: FtrProviderContext): void => { getIncidentResponseUpdatedDateKey: 'fields.updated', hasAuth: true, headers: { [`content-type`]: 'application/json' }, - incidentViewUrl: 'http://some.non.existent.com/browse/{{external.system.title}}', - getIncidentUrl: 'http://some.non.existent.com/{{external.system.id}}', + incidentViewUrl: 'http://some.non.existent.com/browse/{{{external.system.title}}}', + getIncidentUrl: 'http://some.non.existent.com/{{{external.system.id}}}', updateIncidentJson: '{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}', updateIncidentMethod: 'put', - updateIncidentUrl: 'http://some.non.existent.com/{{external.system.id}}', + updateIncidentUrl: 'http://some.non.existent.com/{{{external.system.id}}}', }, isPreconfigured: false, isDeprecated: false, From 219018c041e7f5272adfaa41f90d88df3c86e8a0 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 25 Jul 2022 13:16:11 -0600 Subject: [PATCH 96/97] one more test --- .../cases_webhook/service.test.ts | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts index ff91374c8dfac6..cf8464d71d8a2c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/cases_webhook/service.test.ts @@ -647,8 +647,8 @@ describe('Cases webhook service', () => { requestMock.mockImplementation(() => createAxiosResponse({ data: { - id: '1', - key: 'CK-1', + id: '../../malicious-app/malicious-endpoint/', + key: '../../malicious-app/malicious-endpoint/', fields: { updated: '2020-04-27T10:59:46.202Z', created: '2020-04-27T10:59:46.202Z', @@ -668,6 +668,23 @@ describe('Cases webhook service', () => { ); }); + test('createIncident- escapes url', async () => { + const incident = { + incident: { + title: 'title', + description: 'desc', + tags: ['hello', 'world'], + issueType: '10006', + priority: 'High', + parent: 'RJ-107', + }, + }; + const res = await service.createIncident(incident); + expect(res.url).toEqual( + 'https://siem-kibana.atlassian.net/browse/..%2F..%2Fmalicious-app%2Fmalicious-endpoint%2F' + ); + }); + test('updateIncident- escapes url', async () => { const incident = { incidentId: '../../malicious-app/malicious-endpoint/', From c41636364e90ec4e2a53fcb94f67e96d15c8d405 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 25 Jul 2022 16:00:02 -0600 Subject: [PATCH 97/97] fix jest --- .../cases_webhook/webhook_connectors.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx index e40e551d826f28..3ce481f3b3466c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/cases_webhook/webhook_connectors.test.tsx @@ -359,9 +359,9 @@ describe('CasesWebhookActionConnectorFields renders', () => { [ 'incidentViewUrl', 'https://missingexternalid.com', - ['{{external.system.id}}', '{{external.system.title}}'], + ['{{{external.system.id}}}', '{{{external.system.title}}}'], ], - ['getIncidentUrl', 'https://missingexternalid.com', ['{{external.system.id}}']], + ['getIncidentUrl', 'https://missingexternalid.com', ['{{{external.system.id}}}']], ]; it('connector validation succeeds when connector config is valid', async () => {