Skip to content

Commit

Permalink
[7.8] [SIEM][Detection Engine] Add validation for Rule Actions (#63332)…
Browse files Browse the repository at this point in the history
… (#66755)
  • Loading branch information
patrykkopycinski authored May 15, 2020
1 parent d2afe6e commit 5627897
Show file tree
Hide file tree
Showing 13 changed files with 633 additions and 68 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,38 @@
* 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;

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,
Expand All @@ -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<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(
Expand All @@ -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(() => {
Expand All @@ -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 (
<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}
docLinks={docLinks}
capabilities={capabilities}
/>
<>
{fieldErrors ? (
<>
<FieldErrorsContainer>
<EuiCallOut title={FORM_ERRORS_TITLE} color="danger" iconType="alert">
<ReactMarkdown source={fieldErrors} />
</EuiCallOut>
</FieldErrorsContainer>
<EuiSpacer />
</>
) : null}
<ActionForm
actions={actions}
docLinks={docLinks}
capabilities={capabilities}
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}
/>
</>
);
};
Original file line number Diff line number Diff line change
@@ -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',
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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 {
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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',
Expand All @@ -108,7 +135,7 @@ const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({
hasNoInitialSelection: false,
handleChange: updateThrottle,
euiFieldProps: {
options: THROTTLE_OPTIONS,
options: throttleOptions,
},
}),
[isLoading, updateThrottle]
Expand All @@ -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>

Expand Down
Loading

0 comments on commit 5627897

Please sign in to comment.