mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
This commit is contained in:
parent
f3da8d06a6
commit
75e5edb346
11 changed files with 615 additions and 64 deletions
|
@ -4,19 +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';
|
||||
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { loadActionTypes } from '../../../../../../../../../plugins/triggers_actions_ui/public/application/lib/action_connector_api';
|
||||
import { NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS } from '../../../../../../common/constants';
|
||||
import { SelectField } from '../../../../../shared_imports';
|
||||
import {
|
||||
ActionForm,
|
||||
ActionType,
|
||||
loadActionTypes,
|
||||
} from '../../../../../../../../../plugins/triggers_actions_ui/public';
|
||||
import { AlertAction } from '../../../../../../../../../plugins/alerting/common';
|
||||
import { useKibana } from '../../../../../lib/kibana';
|
||||
import { NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS } from '../../../../../../common/constants';
|
||||
import { FORM_ERRORS_TITLE } from './translations';
|
||||
|
||||
type ThrottleSelectField = typeof SelectField;
|
||||
|
||||
|
@ -24,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<string | null>(null);
|
||||
const [supportedActionTypes, setSupportedActionTypes] = useState<ActionType[] | undefined>();
|
||||
const {
|
||||
http,
|
||||
|
@ -32,13 +43,18 @@ export const RuleActionsField: ThrottleSelectField = ({ field, messageVariables
|
|||
notifications,
|
||||
} = 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<Partial<AlertAction>>)];
|
||||
const updatedActions = [...(actions as Array<Partial<AlertAction>>)];
|
||||
updatedActions[index] = deepMerge(updatedActions[index], { id });
|
||||
field.setValue(updatedActions);
|
||||
},
|
||||
[field]
|
||||
[field.setValue, actions]
|
||||
);
|
||||
|
||||
const setAlertProperty = useCallback(
|
||||
|
@ -49,11 +65,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(() => {
|
||||
|
@ -66,21 +82,55 @@ 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 (
|
||||
<ActionForm
|
||||
actions={field.value as AlertAction[]}
|
||||
messageVariables={messageVariables}
|
||||
defaultActionGroupId={DEFAULT_ACTION_GROUP_ID}
|
||||
setActionIdByIndex={setActionIdByIndex}
|
||||
setAlertProperty={setAlertProperty}
|
||||
setActionParamsProperty={setActionParamsProperty}
|
||||
http={http}
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
actionTypes={supportedActionTypes}
|
||||
defaultActionMessage={DEFAULT_ACTION_MESSAGE}
|
||||
toastNotifications={notifications.toasts}
|
||||
/>
|
||||
<>
|
||||
{fieldErrors ? (
|
||||
<>
|
||||
<FieldErrorsContainer>
|
||||
<EuiCallOut title={FORM_ERRORS_TITLE} color="danger" iconType="alert">
|
||||
<ReactMarkdown source={fieldErrors} />
|
||||
</EuiCallOut>
|
||||
</FieldErrorsContainer>
|
||||
<EuiSpacer />
|
||||
</>
|
||||
) : null}
|
||||
<ActionForm
|
||||
actions={actions}
|
||||
messageVariables={messageVariables}
|
||||
defaultActionGroupId={DEFAULT_ACTION_GROUP_ID}
|
||||
setActionIdByIndex={setActionIdByIndex}
|
||||
setAlertProperty={setAlertProperty}
|
||||
setActionParamsProperty={setActionParamsProperty}
|
||||
http={http}
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
actionTypes={supportedActionTypes}
|
||||
defaultActionMessage={DEFAULT_ACTION_MESSAGE}
|
||||
toastNotifications={notifications.toasts}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
);
|
|
@ -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<StepRuleActionsProps> = ({
|
||||
addPadding = false,
|
||||
defaultValues,
|
||||
|
@ -50,8 +67,12 @@ const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({
|
|||
}) => {
|
||||
const [myStepData, setMyStepData] = useState<ActionsStepRule>(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<StepRuleActionsProps> = ({
|
|||
setMyStepData,
|
||||
]);
|
||||
|
||||
const throttleOptions = useMemo(() => {
|
||||
const throttle = myStepData.throttle;
|
||||
|
||||
return getThrottleOptions(throttle);
|
||||
}, [myStepData]);
|
||||
|
||||
const throttleFieldComponentProps = useMemo(
|
||||
() => ({
|
||||
idAria: 'detectionEngineStepRuleActionsThrottle',
|
||||
|
@ -108,7 +135,7 @@ const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({
|
|||
hasNoInitialSelection: false,
|
||||
handleChange: updateThrottle,
|
||||
euiFieldProps: {
|
||||
options: THROTTLE_OPTIONS,
|
||||
options: throttleOptions,
|
||||
},
|
||||
}),
|
||||
[isLoading, updateThrottle]
|
||||
|
@ -122,30 +149,39 @@ const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({
|
|||
<>
|
||||
<StepContentWrapper addPadding={!isUpdateView}>
|
||||
<Form form={form} data-test-subj="stepRuleActions">
|
||||
<UseField
|
||||
path="throttle"
|
||||
component={ThrottleSelectField}
|
||||
componentProps={throttleFieldComponentProps}
|
||||
/>
|
||||
{myStepData.throttle !== stepActionsDefaultValue.throttle && (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<EuiForm>
|
||||
<UseField
|
||||
path="throttle"
|
||||
component={ThrottleSelectField}
|
||||
componentProps={throttleFieldComponentProps}
|
||||
/>
|
||||
{myStepData.throttle !== stepActionsDefaultValue.throttle ? (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
|
||||
<UseField
|
||||
path="actions"
|
||||
defaultValue={myStepData.actions}
|
||||
component={RuleActionsField}
|
||||
componentProps={{
|
||||
messageVariables: actionMessageParams,
|
||||
}}
|
||||
/>
|
||||
<UseField
|
||||
path="kibanaSiemAppUrl"
|
||||
defaultValue={kibanaAbsoluteUrl}
|
||||
component={GhostFormField}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<UseField
|
||||
path="actions"
|
||||
defaultValue={myStepData.actions}
|
||||
component={RuleActionsField}
|
||||
componentProps={{
|
||||
messageVariables: actionMessageParams,
|
||||
}}
|
||||
/>
|
||||
<UseField
|
||||
path="kibanaSiemAppUrl"
|
||||
defaultValue={kibanaAbsoluteUrl}
|
||||
component={GhostFormField}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<UseField path="enabled" defaultValue={myStepData.enabled} component={GhostFormField} />
|
||||
)}
|
||||
<UseField path="enabled" defaultValue={myStepData.enabled} component={GhostFormField} />
|
||||
</EuiForm>
|
||||
</Form>
|
||||
</StepContentWrapper>
|
||||
|
||||
|
|
|
@ -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 '../../../../../../../../../plugins/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: '',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -6,9 +6,69 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { FormSchema } from '../../../../../shared_imports';
|
||||
import {
|
||||
AlertAction,
|
||||
ActionTypeRegistryContract,
|
||||
} from '../../../../../../../../../plugins/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<ValidationFunc>
|
||||
): ReturnType<ValidationFunc<{}, ERROR_CODE>> | 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<FormData> => ({
|
||||
actions: {
|
||||
validations: [
|
||||
{
|
||||
validator: validateRuleActionsField(actionTypeRegistry),
|
||||
},
|
||||
],
|
||||
},
|
||||
enabled: {},
|
||||
kibanaSiemAppUrl: {},
|
||||
throttle: {
|
||||
label: i18n.translate(
|
||||
'xpack.siem.detectionEngine.createRule.stepRuleActions.fieldThrottleLabel',
|
||||
|
@ -24,14 +84,4 @@ export const schema: FormSchema = {
|
|||
}
|
||||
),
|
||||
},
|
||||
actions: {
|
||||
label: i18n.translate(
|
||||
'xpack.siem.detectionEngine.createRule.stepRuleActions.fieldActionsLabel',
|
||||
{
|
||||
defaultMessage: 'Actions',
|
||||
}
|
||||
),
|
||||
},
|
||||
enabled: {},
|
||||
kibanaSiemAppUrl: {},
|
||||
};
|
||||
});
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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 '../../../../../../../../../plugins/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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 '../../../../../../../../../plugins/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<string | string[]> = actionErrorsValues.filter(
|
||||
item => isString(item) || isArray(item)
|
||||
) as Array<string | string[]>;
|
||||
const uniqActionErrors = uniq(flattenDeep(filteredObjects));
|
||||
|
||||
if (uniqActionErrors.length) {
|
||||
return uniqActionErrors;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
|
@ -13324,8 +13324,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": "フィルター",
|
||||
|
|
|
@ -13328,8 +13328,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": "筛选",
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue