Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SIEM][Detection Engine] Add validation for Rule Actions #63332

Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
0b09ac8
WIP
patrykkopycinski Apr 10, 2020
03330d3
Merge branch 'master' of github.com:elastic/kibana into fix/siem-rule…
patrykkopycinski Apr 10, 2020
dc1a1b8
Merge branch 'master' of github.com:elastic/kibana into fix/siem-rule…
patrykkopycinski Apr 12, 2020
c19d940
WIP
patrykkopycinski Apr 13, 2020
cbf97d6
cleanup
patrykkopycinski Apr 13, 2020
46e828f
add support for custom throttle values
patrykkopycinski Apr 13, 2020
963d74c
add unit tests
patrykkopycinski Apr 13, 2020
583f868
Merge branch 'master' of github.com:elastic/kibana into fix/siem-rule…
patrykkopycinski Apr 15, 2020
d22ff59
PR comments
patrykkopycinski Apr 15, 2020
6d4b340
Fix actions value
patrykkopycinski Apr 15, 2020
d2ee58d
hide errors callout when not needed
patrykkopycinski Apr 15, 2020
f85acdd
fix types
patrykkopycinski Apr 15, 2020
4c5ce1a
cleanup types
patrykkopycinski Apr 15, 2020
36c7b36
fix build
patrykkopycinski Apr 15, 2020
3393fa1
Merge branch 'master' into fix/siem-rule-actions-form-validation
spong Apr 20, 2020
71f0ea9
Fixing lint error
spong Apr 20, 2020
2917be4
Merge branch 'master' of github.com:elastic/kibana into fix/siem-rule…
patrykkopycinski May 11, 2020
025fbb3
merge conflict
patrykkopycinski May 11, 2020
41443bb
fix types
patrykkopycinski May 11, 2020
1c600b9
Merge branch 'master' into fix/siem-rule-actions-form-validation
elasticmachine May 11, 2020
5ca6ed4
Merge branch 'master' of github.com:elastic/kibana into fix/siem-rule…
patrykkopycinski May 12, 2020
3fba518
Merge branch 'master' of github.com:elastic/kibana into fix/siem-rule…
patrykkopycinski May 12, 2020
5ac991f
fix import
patrykkopycinski May 13, 2020
a389651
Merge branch 'master' into fix/siem-rule-actions-form-validation
elasticmachine May 13, 2020
8bad7de
Merge branch 'master' into fix/siem-rule-actions-form-validation
elasticmachine May 14, 2020
f69b6d4
Add more unit tests
patrykkopycinski May 14, 2020
8ea06e4
Merge branch 'master' into fix/siem-rule-actions-form-validation
elasticmachine May 15, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions x-pack/legacy/plugins/siem/public/mock/test_providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { Store } from 'redux';
import { BehaviorSubject } from 'rxjs';
import { ThemeProvider } from 'styled-components';

import { FieldHook, useForm } from '../shared_imports';
import { createStore, State } from '../store';
import { mockGlobalState } from './global_state';
import { createKibanaContextProviderMock } from './kibana_react';
Expand Down Expand Up @@ -91,3 +92,29 @@ const TestProviderWithoutDragAndDropComponent: React.FC<Props> = ({
);

export const TestProviderWithoutDragAndDrop = React.memo(TestProviderWithoutDragAndDropComponent);

export const useFormFieldMock = (options?: Partial<FieldHook>): FieldHook => {
const { form } = useForm();

return {
path: 'path',
type: 'type',
value: [],
isPristine: false,
isValidating: false,
isValidated: false,
isChangingValue: false,
form,
errors: [],
isValid: true,
getErrorsMessages: jest.fn(),
onChange: jest.fn(),
setValue: jest.fn(),
setErrors: jest.fn(),
clearErrors: jest.fn(),
validate: jest.fn(),
reset: jest.fn(),
__serializeOutput: jest.fn(),
...options,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* 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 React from 'react';
import { shallow } from 'enzyme';

import { RuleActionsField } from './index';
import { useKibana } from '../../../../../lib/kibana';
import { useFormFieldMock } from '../../../../../mock';
jest.mock('../../../../../lib/kibana');

describe('RuleActionsField', () => {
it('should not render ActionForm is no actions are supported', () => {
patrykkopycinski marked this conversation as resolved.
Show resolved Hide resolved
(useKibana as jest.Mock).mockReturnValue({
services: {
triggers_actions_ui: {
actionTypeRegistry: {},
},
},
});
const Component = () => {
const field = useFormFieldMock();

return <RuleActionsField euiFieldProps={{ options: [] }} field={field} />;
};
const wrapper = shallow(<Component />);

expect(wrapper.dive().find('ActionForm')).toHaveLength(0);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@
* 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';
Expand All @@ -17,14 +21,22 @@ import {
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;

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 Down Expand Up @@ -66,21 +78,60 @@ export const RuleActionsField: ThrottleSelectField = ({ field, messageVariables
})();
}, []);

useEffect(() => {
if (field.form.isSubmitting) {
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,
]);

const actions: AlertAction[] = useMemo(
() => (!isEmpty(field.value) ? (field.value as AlertAction[]) : []),
[field.value]
);

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}
/>
</>
);
};
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 @@ -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 @@ -16,7 +24,7 @@ import { StepContentWrapper } from '../step_content_wrapper';
import { ThrottleSelectField, THROTTLE_OPTIONS } 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 @@ -34,6 +42,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;
};
patrykkopycinski marked this conversation as resolved.
Show resolved Hide resolved

const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({
addPadding = false,
defaultValues,
Expand All @@ -46,8 +63,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 @@ -96,6 +117,12 @@ const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({
setMyStepData,
]);

const throttleOptions = useMemo(() => {
const throttle = myStepData.throttle;

return getThrottleOptions(throttle);
}, [myStepData]);

const throttleFieldComponentProps = useMemo(
() => ({
idAria: 'detectionEngineStepRuleActionsThrottle',
Expand All @@ -104,7 +131,7 @@ const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({
hasNoInitialSelection: false,
handleChange: updateThrottle,
euiFieldProps: {
options: THROTTLE_OPTIONS,
options: throttleOptions,
},
}),
[isLoading, updateThrottle]
Expand All @@ -118,30 +145,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