Skip to content

Commit 9df3d9f

Browse files
[SIEM][Detection Engine] Add validation for Rule Actions (#63332)
1 parent 976ff05 commit 9df3d9f

File tree

13 files changed

+628
-68
lines changed

13 files changed

+628
-68
lines changed

x-pack/plugins/siem/public/alerts/components/rules/rule_actions_field/index.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { useFormFieldMock } from '../../../../common/mock';
1313
jest.mock('../../../../common/lib/kibana');
1414

1515
describe('RuleActionsField', () => {
16-
it('should not render ActionForm is no actions are supported', () => {
16+
it('should not render ActionForm if no actions are supported', () => {
1717
(useKibana as jest.Mock).mockReturnValue({
1818
services: {
1919
triggers_actions_ui: {

x-pack/plugins/siem/public/alerts/components/rules/rule_actions_field/index.tsx

Lines changed: 76 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,38 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7-
import React, { useCallback, useEffect, useState } from 'react';
7+
import { isEmpty } from 'lodash/fp';
8+
import { EuiSpacer, EuiCallOut } from '@elastic/eui';
9+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
810
import deepMerge from 'deepmerge';
11+
import ReactMarkdown from 'react-markdown';
12+
import styled from 'styled-components';
913

1014
import { NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS } from '../../../../../common/constants';
11-
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
12-
import { loadActionTypes } from '../../../../../../triggers_actions_ui/public/application/lib/action_connector_api';
1315
import { SelectField } from '../../../../shared_imports';
14-
import { ActionForm, ActionType } from '../../../../../../triggers_actions_ui/public';
16+
import {
17+
ActionForm,
18+
ActionType,
19+
loadActionTypes,
20+
} from '../../../../../../triggers_actions_ui/public';
1521
import { AlertAction } from '../../../../../../alerting/common';
1622
import { useKibana } from '../../../../common/lib/kibana';
23+
import { FORM_ERRORS_TITLE } from './translations';
1724

1825
type ThrottleSelectField = typeof SelectField;
1926

2027
const DEFAULT_ACTION_GROUP_ID = 'default';
2128
const DEFAULT_ACTION_MESSAGE =
2229
'Rule {{context.rule.name}} generated {{state.signals_count}} signals';
2330

31+
const FieldErrorsContainer = styled.div`
32+
p {
33+
margin-bottom: 0;
34+
}
35+
`;
36+
2437
export const RuleActionsField: ThrottleSelectField = ({ field, messageVariables }) => {
38+
const [fieldErrors, setFieldErrors] = useState<string | null>(null);
2539
const [supportedActionTypes, setSupportedActionTypes] = useState<ActionType[] | undefined>();
2640
const {
2741
http,
@@ -31,13 +45,18 @@ export const RuleActionsField: ThrottleSelectField = ({ field, messageVariables
3145
application: { capabilities },
3246
} = useKibana().services;
3347

48+
const actions: AlertAction[] = useMemo(
49+
() => (!isEmpty(field.value) ? (field.value as AlertAction[]) : []),
50+
[field.value]
51+
);
52+
3453
const setActionIdByIndex = useCallback(
3554
(id: string, index: number) => {
36-
const updatedActions = [...(field.value as Array<Partial<AlertAction>>)];
55+
const updatedActions = [...(actions as Array<Partial<AlertAction>>)];
3756
updatedActions[index] = deepMerge(updatedActions[index], { id });
3857
field.setValue(updatedActions);
3958
},
40-
[field]
59+
[field.setValue, actions]
4160
);
4261

4362
const setAlertProperty = useCallback(
@@ -48,11 +67,11 @@ export const RuleActionsField: ThrottleSelectField = ({ field, messageVariables
4867
const setActionParamsProperty = useCallback(
4968
// eslint-disable-next-line @typescript-eslint/no-explicit-any
5069
(key: string, value: any, index: number) => {
51-
const updatedActions = [...(field.value as AlertAction[])];
70+
const updatedActions = [...actions];
5271
updatedActions[index].params[key] = value;
5372
field.setValue(updatedActions);
5473
},
55-
[field]
74+
[field.setValue, actions]
5675
);
5776

5877
useEffect(() => {
@@ -65,23 +84,57 @@ export const RuleActionsField: ThrottleSelectField = ({ field, messageVariables
6584
})();
6685
}, []);
6786

87+
useEffect(() => {
88+
if (field.form.isSubmitting || !field.errors.length) {
89+
return setFieldErrors(null);
90+
}
91+
if (
92+
field.form.isSubmitted &&
93+
!field.form.isSubmitting &&
94+
field.form.isValid === false &&
95+
field.errors.length
96+
) {
97+
const errorsString = field.errors.map(({ message }) => message).join('\n');
98+
return setFieldErrors(errorsString);
99+
}
100+
}, [
101+
field.form.isSubmitted,
102+
field.form.isSubmitting,
103+
field.isChangingValue,
104+
field.form.isValid,
105+
field.errors,
106+
setFieldErrors,
107+
]);
108+
68109
if (!supportedActionTypes) return <></>;
69110

70111
return (
71-
<ActionForm
72-
actions={field.value as AlertAction[]}
73-
messageVariables={messageVariables}
74-
defaultActionGroupId={DEFAULT_ACTION_GROUP_ID}
75-
setActionIdByIndex={setActionIdByIndex}
76-
setAlertProperty={setAlertProperty}
77-
setActionParamsProperty={setActionParamsProperty}
78-
http={http}
79-
actionTypeRegistry={actionTypeRegistry}
80-
actionTypes={supportedActionTypes}
81-
defaultActionMessage={DEFAULT_ACTION_MESSAGE}
82-
toastNotifications={notifications.toasts}
83-
docLinks={docLinks}
84-
capabilities={capabilities}
85-
/>
112+
<>
113+
{fieldErrors ? (
114+
<>
115+
<FieldErrorsContainer>
116+
<EuiCallOut title={FORM_ERRORS_TITLE} color="danger" iconType="alert">
117+
<ReactMarkdown source={fieldErrors} />
118+
</EuiCallOut>
119+
</FieldErrorsContainer>
120+
<EuiSpacer />
121+
</>
122+
) : null}
123+
<ActionForm
124+
actions={actions}
125+
docLinks={docLinks}
126+
capabilities={capabilities}
127+
messageVariables={messageVariables}
128+
defaultActionGroupId={DEFAULT_ACTION_GROUP_ID}
129+
setActionIdByIndex={setActionIdByIndex}
130+
setAlertProperty={setAlertProperty}
131+
setActionParamsProperty={setActionParamsProperty}
132+
http={http}
133+
actionTypeRegistry={actionTypeRegistry}
134+
actionTypes={supportedActionTypes}
135+
defaultActionMessage={DEFAULT_ACTION_MESSAGE}
136+
toastNotifications={notifications.toasts}
137+
/>
138+
</>
86139
);
87140
};
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { i18n } from '@kbn/i18n';
8+
9+
export const FORM_ERRORS_TITLE = i18n.translate(
10+
'xpack.siem.detectionEngine.createRule.ruleActionsField.ruleActionsFormErrorsTitle',
11+
{
12+
defaultMessage: 'Please fix issues listed below',
13+
}
14+
);

x-pack/plugins/siem/public/alerts/components/rules/step_rule_actions/index.test.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,18 @@ import { shallow } from 'enzyme';
99

1010
import { StepRuleActions } from './index';
1111

12-
jest.mock('../../../../common/lib/kibana');
12+
jest.mock('../../../../common/lib/kibana', () => ({
13+
useKibana: jest.fn().mockReturnValue({
14+
services: {
15+
application: {
16+
getUrlForApp: jest.fn(),
17+
},
18+
triggers_actions_ui: {
19+
actionTypeRegistry: jest.fn(),
20+
},
21+
},
22+
}),
23+
}));
1324

1425
describe('StepRuleActions', () => {
1526
it('renders correctly', () => {

x-pack/plugins/siem/public/alerts/components/rules/step_rule_actions/index.tsx

Lines changed: 59 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,15 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7-
import { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem, EuiButton, EuiSpacer } from '@elastic/eui';
7+
import {
8+
EuiHorizontalRule,
9+
EuiForm,
10+
EuiFlexGroup,
11+
EuiFlexItem,
12+
EuiButton,
13+
EuiSpacer,
14+
} from '@elastic/eui';
15+
import { findIndex } from 'lodash/fp';
816
import React, { FC, memo, useCallback, useEffect, useMemo, useState } from 'react';
917
import deepEqual from 'fast-deep-equal';
1018

@@ -24,7 +32,7 @@ import {
2432
} from '../throttle_select_field';
2533
import { RuleActionsField } from '../rule_actions_field';
2634
import { useKibana } from '../../../../common/lib/kibana';
27-
import { schema } from './schema';
35+
import { getSchema } from './schema';
2836
import * as I18n from './translations';
2937

3038
interface StepRuleActionsProps extends RuleStepProps {
@@ -42,6 +50,15 @@ const stepActionsDefaultValue = {
4250

4351
const GhostFormField = () => <></>;
4452

53+
const getThrottleOptions = (throttle?: string | null) => {
54+
// Add support for throttle options set by the API
55+
if (throttle && findIndex(['value', throttle], THROTTLE_OPTIONS) < 0) {
56+
return [...THROTTLE_OPTIONS, { value: throttle, text: throttle }];
57+
}
58+
59+
return THROTTLE_OPTIONS;
60+
};
61+
4562
const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({
4663
addPadding = false,
4764
defaultValues,
@@ -54,8 +71,12 @@ const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({
5471
}) => {
5572
const [myStepData, setMyStepData] = useState<ActionsStepRule>(stepActionsDefaultValue);
5673
const {
57-
services: { application },
74+
services: {
75+
application,
76+
triggers_actions_ui: { actionTypeRegistry },
77+
},
5878
} = useKibana();
79+
const schema = useMemo(() => getSchema({ actionTypeRegistry }), [actionTypeRegistry]);
5980

6081
const { form } = useForm({
6182
defaultValue: myStepData,
@@ -104,6 +125,12 @@ const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({
104125
setMyStepData,
105126
]);
106127

128+
const throttleOptions = useMemo(() => {
129+
const throttle = myStepData.throttle;
130+
131+
return getThrottleOptions(throttle);
132+
}, [myStepData]);
133+
107134
const throttleFieldComponentProps = useMemo(
108135
() => ({
109136
idAria: 'detectionEngineStepRuleActionsThrottle',
@@ -112,7 +139,7 @@ const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({
112139
hasNoInitialSelection: false,
113140
handleChange: updateThrottle,
114141
euiFieldProps: {
115-
options: THROTTLE_OPTIONS,
142+
options: throttleOptions,
116143
},
117144
}),
118145
[isLoading, updateThrottle]
@@ -126,30 +153,39 @@ const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({
126153
<>
127154
<StepContentWrapper addPadding={!isUpdateView}>
128155
<Form form={form} data-test-subj="stepRuleActions">
129-
<UseField
130-
path="throttle"
131-
component={ThrottleSelectField}
132-
componentProps={throttleFieldComponentProps}
133-
/>
134-
{myStepData.throttle !== stepActionsDefaultValue.throttle && (
135-
<>
136-
<EuiSpacer />
156+
<EuiForm>
157+
<UseField
158+
path="throttle"
159+
component={ThrottleSelectField}
160+
componentProps={throttleFieldComponentProps}
161+
/>
162+
{myStepData.throttle !== stepActionsDefaultValue.throttle ? (
163+
<>
164+
<EuiSpacer />
165+
166+
<UseField
167+
path="actions"
168+
defaultValue={myStepData.actions}
169+
component={RuleActionsField}
170+
componentProps={{
171+
messageVariables: actionMessageParams,
172+
}}
173+
/>
174+
<UseField
175+
path="kibanaSiemAppUrl"
176+
defaultValue={kibanaAbsoluteUrl}
177+
component={GhostFormField}
178+
/>
179+
</>
180+
) : (
137181
<UseField
138182
path="actions"
139183
defaultValue={myStepData.actions}
140-
component={RuleActionsField}
141-
componentProps={{
142-
messageVariables: actionMessageParams,
143-
}}
144-
/>
145-
<UseField
146-
path="kibanaSiemAppUrl"
147-
defaultValue={kibanaAbsoluteUrl}
148184
component={GhostFormField}
149185
/>
150-
</>
151-
)}
152-
<UseField path="enabled" defaultValue={myStepData.enabled} component={GhostFormField} />
186+
)}
187+
<UseField path="enabled" defaultValue={myStepData.enabled} component={GhostFormField} />
188+
</EuiForm>
153189
</Form>
154190
</StepContentWrapper>
155191

0 commit comments

Comments
 (0)