diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.test.tsx index 13ae3ee3d3b7d..c7fa4a39ac043 100644 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.test.tsx +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.test.tsx @@ -13,7 +13,7 @@ import { useFormFieldMock } from '../../../../../mock'; jest.mock('../../../../../lib/kibana'); describe('RuleActionsField', () => { - it('should not render ActionForm is no actions are supported', () => { + it('should not render ActionForm if no actions are supported', () => { (useKibana as jest.Mock).mockReturnValue({ services: { triggers_actions_ui: { diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.tsx index d53cf52cc67b9..240630c4d4d54 100644 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.tsx +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.tsx @@ -4,16 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useEffect, useState } from 'react'; +import { isEmpty } from 'lodash/fp'; +import { EuiSpacer, EuiCallOut } from '@elastic/eui'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import deepMerge from 'deepmerge'; +import ReactMarkdown from 'react-markdown'; +import styled from 'styled-components'; import { NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS } from '../../../../../../common/constants'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { loadActionTypes } from '../../../../../../../triggers_actions_ui/public/application/lib/action_connector_api'; import { SelectField } from '../../../../../shared_imports'; -import { ActionForm, ActionType } from '../../../../../../../triggers_actions_ui/public'; +import { + ActionForm, + ActionType, + loadActionTypes, +} from '../../../../../../../triggers_actions_ui/public'; import { AlertAction } from '../../../../../../../alerting/common'; import { useKibana } from '../../../../../lib/kibana'; +import { FORM_ERRORS_TITLE } from './translations'; type ThrottleSelectField = typeof SelectField; @@ -21,7 +28,14 @@ const DEFAULT_ACTION_GROUP_ID = 'default'; const DEFAULT_ACTION_MESSAGE = 'Rule {{context.rule.name}} generated {{state.signals_count}} signals'; +const FieldErrorsContainer = styled.div` + p { + margin-bottom: 0; + } +`; + export const RuleActionsField: ThrottleSelectField = ({ field, messageVariables }) => { + const [fieldErrors, setFieldErrors] = useState(null); const [supportedActionTypes, setSupportedActionTypes] = useState(); const { http, @@ -31,13 +45,18 @@ export const RuleActionsField: ThrottleSelectField = ({ field, messageVariables application: { capabilities }, } = useKibana().services; + const actions: AlertAction[] = useMemo( + () => (!isEmpty(field.value) ? (field.value as AlertAction[]) : []), + [field.value] + ); + const setActionIdByIndex = useCallback( (id: string, index: number) => { - const updatedActions = [...(field.value as Array>)]; + const updatedActions = [...(actions as Array>)]; updatedActions[index] = deepMerge(updatedActions[index], { id }); field.setValue(updatedActions); }, - [field] + [field.setValue, actions] ); const setAlertProperty = useCallback( @@ -48,11 +67,11 @@ export const RuleActionsField: ThrottleSelectField = ({ field, messageVariables const setActionParamsProperty = useCallback( // eslint-disable-next-line @typescript-eslint/no-explicit-any (key: string, value: any, index: number) => { - const updatedActions = [...(field.value as AlertAction[])]; + const updatedActions = [...actions]; updatedActions[index].params[key] = value; field.setValue(updatedActions); }, - [field] + [field.setValue, actions] ); useEffect(() => { @@ -65,23 +84,57 @@ export const RuleActionsField: ThrottleSelectField = ({ field, messageVariables })(); }, []); + useEffect(() => { + if (field.form.isSubmitting || !field.errors.length) { + return setFieldErrors(null); + } + if ( + field.form.isSubmitted && + !field.form.isSubmitting && + field.form.isValid === false && + field.errors.length + ) { + const errorsString = field.errors.map(({ message }) => message).join('\n'); + return setFieldErrors(errorsString); + } + }, [ + field.form.isSubmitted, + field.form.isSubmitting, + field.isChangingValue, + field.form.isValid, + field.errors, + setFieldErrors, + ]); + if (!supportedActionTypes) return <>; return ( - + <> + {fieldErrors ? ( + <> + + + + + + + + ) : null} + + ); }; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/translations.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/translations.tsx new file mode 100644 index 0000000000000..8bf8e39685dfd --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/translations.tsx @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const FORM_ERRORS_TITLE = i18n.translate( + 'xpack.siem.detectionEngine.createRule.ruleActionsField.ruleActionsFormErrorsTitle', + { + defaultMessage: 'Please fix issues listed below', + } +); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/index.test.tsx index 69d118ba9f28e..3338905ea32ae 100644 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/index.test.tsx +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/index.test.tsx @@ -9,7 +9,18 @@ import { shallow } from 'enzyme'; import { StepRuleActions } from './index'; -jest.mock('../../../../../lib/kibana'); +jest.mock('../../../../../lib/kibana', () => ({ + useKibana: jest.fn().mockReturnValue({ + services: { + application: { + getUrlForApp: jest.fn(), + }, + triggers_actions_ui: { + actionTypeRegistry: jest.fn(), + }, + }, + }), +})); describe('StepRuleActions', () => { it('renders correctly', () => { diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/index.tsx index 7ffb49840eeb5..803bc3f653c10 100644 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/index.tsx +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/index.tsx @@ -4,7 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem, EuiButton, EuiSpacer } from '@elastic/eui'; +import { + EuiHorizontalRule, + EuiForm, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiSpacer, +} from '@elastic/eui'; +import { findIndex } from 'lodash/fp'; import React, { FC, memo, useCallback, useEffect, useMemo, useState } from 'react'; import deepEqual from 'fast-deep-equal'; @@ -20,7 +28,7 @@ import { } from '../throttle_select_field'; import { RuleActionsField } from '../rule_actions_field'; import { useKibana } from '../../../../../lib/kibana'; -import { schema } from './schema'; +import { getSchema } from './schema'; import * as I18n from './translations'; interface StepRuleActionsProps extends RuleStepProps { @@ -38,6 +46,15 @@ const stepActionsDefaultValue = { const GhostFormField = () => <>; +const getThrottleOptions = (throttle?: string | null) => { + // Add support for throttle options set by the API + if (throttle && findIndex(['value', throttle], THROTTLE_OPTIONS) < 0) { + return [...THROTTLE_OPTIONS, { value: throttle, text: throttle }]; + } + + return THROTTLE_OPTIONS; +}; + const StepRuleActionsComponent: FC = ({ addPadding = false, defaultValues, @@ -50,8 +67,12 @@ const StepRuleActionsComponent: FC = ({ }) => { const [myStepData, setMyStepData] = useState(stepActionsDefaultValue); const { - services: { application }, + services: { + application, + triggers_actions_ui: { actionTypeRegistry }, + }, } = useKibana(); + const schema = useMemo(() => getSchema({ actionTypeRegistry }), [actionTypeRegistry]); const { form } = useForm({ defaultValue: myStepData, @@ -100,6 +121,12 @@ const StepRuleActionsComponent: FC = ({ setMyStepData, ]); + const throttleOptions = useMemo(() => { + const throttle = myStepData.throttle; + + return getThrottleOptions(throttle); + }, [myStepData]); + const throttleFieldComponentProps = useMemo( () => ({ idAria: 'detectionEngineStepRuleActionsThrottle', @@ -108,7 +135,7 @@ const StepRuleActionsComponent: FC = ({ hasNoInitialSelection: false, handleChange: updateThrottle, euiFieldProps: { - options: THROTTLE_OPTIONS, + options: throttleOptions, }, }), [isLoading, updateThrottle] @@ -122,30 +149,39 @@ const StepRuleActionsComponent: FC = ({ <>
- - {myStepData.throttle !== stepActionsDefaultValue.throttle && ( - <> - + + + {myStepData.throttle !== stepActionsDefaultValue.throttle ? ( + <> + + + + + + ) : ( - - - )} - + )} + +
diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/schema.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/schema.test.tsx new file mode 100644 index 0000000000000..2e6d5aab797e9 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/schema.test.tsx @@ -0,0 +1,166 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { validateSingleAction, validateRuleActionsField } from './schema'; +import { isUuidv4, getActionTypeName, validateMustache, validateActionParams } from './utils'; +import { actionTypeRegistryMock } from '../../../../../../../triggers_actions_ui/public/application/action_type_registry.mock'; +import { FormHook } from '../../../../../shared_imports'; +jest.mock('./utils'); + +describe('stepRuleActions schema', () => { + const actionTypeRegistry = actionTypeRegistryMock.create(); + + describe('validateSingleAction', () => { + it('should validate single action', () => { + (isUuidv4 as jest.Mock).mockReturnValue(true); + (validateActionParams as jest.Mock).mockReturnValue([]); + (validateMustache as jest.Mock).mockReturnValue([]); + + expect( + validateSingleAction( + { + id: '817b8bca-91d1-4729-8ee1-3a83aaafd9d4', + group: 'default', + actionTypeId: '.slack', + params: {}, + }, + actionTypeRegistry + ) + ).toHaveLength(0); + }); + + it('should validate single action with invalid mustache template', () => { + (isUuidv4 as jest.Mock).mockReturnValue(true); + (validateActionParams as jest.Mock).mockReturnValue([]); + (validateMustache as jest.Mock).mockReturnValue(['Message is not valid mustache template']); + + const errors = validateSingleAction( + { + id: '817b8bca-91d1-4729-8ee1-3a83aaafd9d4', + group: 'default', + actionTypeId: '.slack', + params: { + message: '{{{mustache}}', + }, + }, + actionTypeRegistry + ); + + expect(errors).toHaveLength(1); + expect(errors[0]).toEqual('Message is not valid mustache template'); + }); + + it('should validate single action with incorrect id', () => { + (isUuidv4 as jest.Mock).mockReturnValue(false); + (validateMustache as jest.Mock).mockReturnValue([]); + (validateActionParams as jest.Mock).mockReturnValue([]); + + const errors = validateSingleAction( + { + id: '823d4', + group: 'default', + actionTypeId: '.slack', + params: {}, + }, + actionTypeRegistry + ); + expect(errors).toHaveLength(1); + expect(errors[0]).toEqual('No connector selected'); + }); + }); + + describe('validateRuleActionsField', () => { + it('should validate rule actions field', () => { + const validator = validateRuleActionsField(actionTypeRegistry); + + const result = validator({ + path: '', + value: [], + form: {} as FormHook, + formData: jest.fn(), + errors: [], + }); + + expect(result).toEqual(undefined); + }); + + it('should validate incorrect rule actions field', () => { + (getActionTypeName as jest.Mock).mockReturnValue('Slack'); + const validator = validateRuleActionsField(actionTypeRegistry); + + const result = validator({ + path: '', + value: [ + { + id: '3', + group: 'default', + actionTypeId: '.slack', + params: {}, + }, + ], + form: {} as FormHook, + formData: jest.fn(), + errors: [], + }); + + expect(result).toEqual({ + code: 'ERR_FIELD_FORMAT', + message: ` +**Slack:** +* No connector selected +`, + path: '', + }); + }); + + it('should validate multiple incorrect rule actions field', () => { + (isUuidv4 as jest.Mock).mockReturnValueOnce(false); + (getActionTypeName as jest.Mock).mockReturnValueOnce('Slack'); + (isUuidv4 as jest.Mock).mockReturnValueOnce(true); + (getActionTypeName as jest.Mock).mockReturnValueOnce('Pagerduty'); + (validateActionParams as jest.Mock).mockReturnValue(['Summary is required']); + (validateMustache as jest.Mock).mockReturnValue(['Component is not valid mustache template']); + const validator = validateRuleActionsField(actionTypeRegistry); + + const result = validator({ + path: '', + value: [ + { + id: '817b8bca-91d1-4729-8ee1-3a83aaafd9d4', + group: 'default', + actionTypeId: '.slack', + params: {}, + }, + { + id: 'a8d1ef21-dcb9-4ac6-9e52-961f938a4c17', + group: 'default', + actionTypeId: '.pagerduty', + params: { + component: '{{{', + }, + }, + ], + form: {} as FormHook, + formData: jest.fn(), + errors: [], + }); + + expect(result).toEqual({ + code: 'ERR_FIELD_FORMAT', + message: ` +**Slack:** +* No connector selected + + +**Pagerduty:** +* Summary is required +* Component is not valid mustache template +`, + path: '', + }); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/schema.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/schema.tsx index 78715eb5b222c..c56980115e7af 100644 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/schema.tsx +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/schema.tsx @@ -8,9 +8,69 @@ import { i18n } from '@kbn/i18n'; -import { FormSchema } from '../../../../../shared_imports'; +import { + AlertAction, + ActionTypeRegistryContract, +} from '../../../../../../../triggers_actions_ui/public'; +import { FormSchema, FormData, ValidationFunc, ERROR_CODE } from '../../../../../shared_imports'; +import * as I18n from './translations'; +import { isUuidv4, getActionTypeName, validateMustache, validateActionParams } from './utils'; -export const schema: FormSchema = { +export const validateSingleAction = ( + actionItem: AlertAction, + actionTypeRegistry: ActionTypeRegistryContract +): string[] => { + if (!isUuidv4(actionItem.id)) { + return [I18n.NO_CONNECTOR_SELECTED]; + } + + const actionParamsErrors = validateActionParams(actionItem, actionTypeRegistry); + const mustacheErrors = validateMustache(actionItem.params); + + return [...actionParamsErrors, ...mustacheErrors]; +}; + +export const validateRuleActionsField = (actionTypeRegistry: ActionTypeRegistryContract) => ( + ...data: Parameters +): ReturnType> | undefined => { + const [{ value, path }] = data as [{ value: AlertAction[]; path: string }]; + + const errors = value.reduce((acc, actionItem) => { + const errorsArray = validateSingleAction(actionItem, actionTypeRegistry); + + if (errorsArray.length) { + const actionTypeName = getActionTypeName(actionItem.actionTypeId); + const errorsListItems = errorsArray.map(error => `* ${error}\n`); + + return [...acc, `\n**${actionTypeName}:**\n${errorsListItems.join('')}`]; + } + + return acc; + }, [] as string[]); + + if (errors.length) { + return { + code: 'ERR_FIELD_FORMAT', + path, + message: `${errors.join('\n')}`, + }; + } +}; + +export const getSchema = ({ + actionTypeRegistry, +}: { + actionTypeRegistry: ActionTypeRegistryContract; +}): FormSchema => ({ + actions: { + validations: [ + { + validator: validateRuleActionsField(actionTypeRegistry), + }, + ], + }, + enabled: {}, + kibanaSiemAppUrl: {}, throttle: { label: i18n.translate( 'xpack.siem.detectionEngine.createRule.stepRuleActions.fieldThrottleLabel', @@ -26,14 +86,4 @@ export const schema: FormSchema = { } ), }, - actions: { - label: i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepRuleActions.fieldActionsLabel', - { - defaultMessage: 'Actions', - } - ), - }, - enabled: {}, - kibanaSiemAppUrl: {}, -}; +}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/translations.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/translations.tsx index 67bcc1af8150b..d0c990df81ffe 100644 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/translations.tsx +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/translations.tsx @@ -5,17 +5,36 @@ */ import { i18n } from '@kbn/i18n'; +import { startCase } from 'lodash/fp'; export const COMPLETE_WITHOUT_ACTIVATING = i18n.translate( - 'xpack.siem.detectionEngine.createRule. stepScheduleRule.completeWithoutActivatingTitle', + 'xpack.siem.detectionEngine.createRule.stepScheduleRule.completeWithoutActivatingTitle', { defaultMessage: 'Create rule without activating it', } ); export const COMPLETE_WITH_ACTIVATING = i18n.translate( - 'xpack.siem.detectionEngine.createRule. stepScheduleRule.completeWithActivatingTitle', + 'xpack.siem.detectionEngine.createRule.stepScheduleRule.completeWithActivatingTitle', { defaultMessage: 'Create & activate rule', } ); + +export const NO_CONNECTOR_SELECTED = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepRuleActions.noConnectorSelectedErrorMessage', + { + defaultMessage: 'No connector selected', + } +); + +export const INVALID_MUSTACHE_TEMPLATE = (paramKey: string) => + i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepRuleActions.invalidMustacheTemplateErrorMessage', + { + defaultMessage: '{key} is not valid mustache template', + values: { + key: startCase(paramKey), + }, + } + ); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/utils.test.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/utils.test.ts new file mode 100644 index 0000000000000..1d5476a3d1fad --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { actionTypeRegistryMock } from '../../../../../../../triggers_actions_ui/public/application/action_type_registry.mock'; +import { isUuidv4, getActionTypeName, validateMustache, validateActionParams } from './utils'; + +describe('stepRuleActions utils', () => { + describe('isUuidv4', () => { + it('should validate proper uuid v4 value', () => { + expect(isUuidv4('817b8bca-91d1-4729-8ee1-3a83aaafd9d4')).toEqual(true); + }); + + it('should validate incorrect uuid v4 value', () => { + expect(isUuidv4('ad9d4')).toEqual(false); + }); + }); + + describe('getActionTypeName', () => { + it('should return capitalized action type name', () => { + expect(getActionTypeName('.slack')).toEqual('Slack'); + }); + + it('should return empty string actionTypeId had improper format', () => { + expect(getActionTypeName('slack')).toEqual(''); + }); + }); + + describe('validateMustache', () => { + it('should validate mustache template', () => { + expect( + validateMustache({ + message: 'Mustache Template {{variable}}', + }) + ).toHaveLength(0); + }); + + it('should validate incorrect mustache template', () => { + expect( + validateMustache({ + message: 'Mustache Template {{{variable}}', + }) + ).toHaveLength(1); + }); + }); + + describe('validateActionParams', () => { + const validateParamsMock = jest.fn(); + const actionTypeRegistry = actionTypeRegistryMock.create(); + + beforeAll(() => { + const actionMock = { + id: 'id', + iconClass: 'iconClass', + validateParams: validateParamsMock, + selectMessage: 'message', + validateConnector: jest.fn(), + actionConnectorFields: null, + actionParamsFields: null, + }; + actionTypeRegistry.get.mockReturnValue(actionMock); + }); + + it('should validate action params', () => { + validateParamsMock.mockReturnValue({ errors: [] }); + + expect( + validateActionParams( + { + id: '817b8bca-91d1-4729-8ee1-3a83aaafd9d4', + group: 'default', + actionTypeId: '.slack', + params: { + message: 'Message', + }, + }, + actionTypeRegistry + ) + ).toHaveLength(0); + }); + + it('should validate incorrect action params', () => { + validateParamsMock.mockReturnValue({ + errors: ['Message is required'], + }); + + expect( + validateActionParams( + { + id: '817b8bca-91d1-4729-8ee1-3a83aaafd9d4', + group: 'default', + actionTypeId: '.slack', + params: {}, + }, + actionTypeRegistry + ) + ).toHaveLength(1); + }); + + it('should validate incorrect action params and filter error objects', () => { + validateParamsMock.mockReturnValue({ + errors: [ + { + message: 'Message is required', + }, + ], + }); + + expect( + validateActionParams( + { + id: '817b8bca-91d1-4729-8ee1-3a83aaafd9d4', + group: 'default', + actionTypeId: '.slack', + params: {}, + }, + actionTypeRegistry + ) + ).toHaveLength(0); + }); + + it('should validate incorrect action params and filter duplicated errors', () => { + validateParamsMock.mockReturnValue({ + errors: ['Message is required', 'Message is required', 'Message is required'], + }); + + expect( + validateActionParams( + { + id: '817b8bca-91d1-4729-8ee1-3a83aaafd9d4', + group: 'default', + actionTypeId: '.slack', + params: {}, + }, + actionTypeRegistry + ) + ).toHaveLength(1); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/utils.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/utils.ts new file mode 100644 index 0000000000000..d2ff157a7d098 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/utils.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import mustache from 'mustache'; +import { uniq, startCase, flattenDeep, isArray, isString } from 'lodash/fp'; + +import { + AlertAction, + ActionTypeRegistryContract, +} from '../../../../../../../triggers_actions_ui/public'; +import * as I18n from './translations'; + +const UUID_V4_REGEX = /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i; + +export const isUuidv4 = (id: AlertAction['id']) => !!id.match(UUID_V4_REGEX); + +export const getActionTypeName = (actionTypeId: AlertAction['actionTypeId']) => { + if (!actionTypeId) return ''; + const actionType = actionTypeId.split('.')[1]; + + if (!actionType) return ''; + + return startCase(actionType); +}; + +export const validateMustache = (params: AlertAction['params']) => { + const errors: string[] = []; + Object.entries(params).forEach(([paramKey, paramValue]) => { + if (!isString(paramValue)) return; + try { + mustache.render(paramValue, {}); + } catch (e) { + errors.push(I18n.INVALID_MUSTACHE_TEMPLATE(paramKey)); + } + }); + + return errors; +}; + +export const validateActionParams = ( + actionItem: AlertAction, + actionTypeRegistry: ActionTypeRegistryContract +): string[] => { + const actionErrors = actionTypeRegistry + .get(actionItem.actionTypeId) + ?.validateParams(actionItem.params); + + if (actionErrors) { + const actionErrorsValues = Object.values(actionErrors.errors); + + if (actionErrorsValues.length) { + const filteredObjects: Array = actionErrorsValues.filter( + item => isString(item) || isArray(item) + ) as Array; + const uniqActionErrors = uniq(flattenDeep(filteredObjects)); + + if (uniqActionErrors.length) { + return uniqActionErrors; + } + } + } + + return []; +}; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 75222afe99d10..c6c8169a81e7b 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -13336,8 +13336,8 @@ "xpack.siem.detectionEngine.components.importRuleModal.overwriteDescription": "保存されたオブジェクトを同じルールIDで自動的に上書きします", "xpack.siem.detectionEngine.components.importRuleModal.selectRuleDescription": "インポートする SIEM ルール (検出エンジンビューからエクスポートしたもの) を選択します", "xpack.siem.detectionEngine.components.importRuleModal.successfullyImportedRulesTitle": "{totalRules} {totalRules, plural, =1 {ルール} other {ルール}}を正常にインポートしました", - "xpack.siem.detectionEngine.createRule. stepScheduleRule.completeWithActivatingTitle": "ルールの作成と有効化", - "xpack.siem.detectionEngine.createRule. stepScheduleRule.completeWithoutActivatingTitle": "有効化せずにルールを作成", + "xpack.siem.detectionEngine.createRule.stepScheduleRule.completeWithActivatingTitle": "ルールの作成と有効化", + "xpack.siem.detectionEngine.createRule.stepScheduleRule.completeWithoutActivatingTitle": "有効化せずにルールを作成", "xpack.siem.detectionEngine.createRule.backToRulesDescription": "シグナル検出ルールに戻る", "xpack.siem.detectionEngine.createRule.editRuleButton": "編集", "xpack.siem.detectionEngine.createRule.filtersLabel": "フィルター", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index b227181326601..4b9cc01aad5b7 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -13343,8 +13343,8 @@ "xpack.siem.detectionEngine.components.importRuleModal.overwriteDescription": "自动覆盖具有相同规则 ID 的已保存对象", "xpack.siem.detectionEngine.components.importRuleModal.selectRuleDescription": "选择要导入的 SIEM 规则(如从检测引擎视图导出的)", "xpack.siem.detectionEngine.components.importRuleModal.successfullyImportedRulesTitle": "已成功导入 {totalRules} 个{totalRules, plural, =1 {规则} other {规则}}", - "xpack.siem.detectionEngine.createRule. stepScheduleRule.completeWithActivatingTitle": "创建并激活规则", - "xpack.siem.detectionEngine.createRule. stepScheduleRule.completeWithoutActivatingTitle": "创建规则但不激活", + "xpack.siem.detectionEngine.createRule.stepScheduleRule.completeWithActivatingTitle": "创建并激活规则", + "xpack.siem.detectionEngine.createRule.stepScheduleRule.completeWithoutActivatingTitle": "创建规则但不激活", "xpack.siem.detectionEngine.createRule.backToRulesDescription": "返回到信号检测规则", "xpack.siem.detectionEngine.createRule.editRuleButton": "编辑", "xpack.siem.detectionEngine.createRule.filtersLabel": "筛选", diff --git a/x-pack/plugins/triggers_actions_ui/public/index.ts b/x-pack/plugins/triggers_actions_ui/public/index.ts index 96645e856e418..5bff9e5acbe13 100644 --- a/x-pack/plugins/triggers_actions_ui/public/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/index.ts @@ -11,11 +11,18 @@ export { AlertsContextProvider } from './application/context/alerts_context'; export { ActionsConnectorsContextProvider } from './application/context/actions_connectors_context'; export { AlertAdd } from './application/sections/alert_form'; export { ActionForm } from './application/sections/action_connector_form'; -export { AlertAction, Alert, AlertTypeModel, ActionType } from './types'; +export { + AlertAction, + Alert, + AlertTypeModel, + ActionType, + ActionTypeRegistryContract, +} from './types'; export { ConnectorAddFlyout, ConnectorEditFlyout, } from './application/sections/action_connector_form'; +export { loadActionTypes } from './application/lib/action_connector_api'; export function plugin(ctx: PluginInitializerContext) { return new Plugin(ctx);