mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
## Summary Allow defining notifications that will trigger whenever the rule created new signals. Requires: - https://github.com/elastic/kibana/pull/58395 - https://github.com/elastic/kibana/pull/58964 - https://github.com/elastic/kibana/pull/60832   ### Checklist Delete any items that are not applicable to this PR. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md) - [ ] [Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios - [ ] This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server) - [ ] This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process) Co-authored-by: patrykkopycinski <patryk.kopycinski@elastic.co>
This commit is contained in:
parent
c11a27634d
commit
31084975fd
43 changed files with 1054 additions and 184 deletions
|
@ -65,6 +65,8 @@ export const INTERNAL_IDENTIFIER = '__internal';
|
|||
export const INTERNAL_RULE_ID_KEY = `${INTERNAL_IDENTIFIER}_rule_id`;
|
||||
export const INTERNAL_RULE_ALERT_ID_KEY = `${INTERNAL_IDENTIFIER}_rule_alert_id`;
|
||||
export const INTERNAL_IMMUTABLE_KEY = `${INTERNAL_IDENTIFIER}_immutable`;
|
||||
export const INTERNAL_NOTIFICATION_ID_KEY = `${INTERNAL_IDENTIFIER}_notification_id`;
|
||||
export const INTERNAL_NOTIFICATION_RULE_ID_KEY = `${INTERNAL_IDENTIFIER}_notification_rule_id`;
|
||||
|
||||
/**
|
||||
* Detection engine routes
|
||||
|
@ -99,4 +101,14 @@ export const UNAUTHENTICATED_USER = 'Unauthenticated';
|
|||
*/
|
||||
export const MINIMUM_ML_LICENSE = 'platinum';
|
||||
|
||||
/*
|
||||
Rule notifications options
|
||||
*/
|
||||
export const NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS = [
|
||||
'.email',
|
||||
'.slack',
|
||||
'.pagerduty',
|
||||
'.webhook',
|
||||
];
|
||||
export const NOTIFICATION_THROTTLE_NO_ACTIONS = 'no_actions';
|
||||
export const NOTIFICATION_THROTTLE_RULE = 'rule';
|
||||
|
|
|
@ -20,7 +20,9 @@ export const CREATE_AND_ACTIVATE_BTN = '[data-test-subj="create-activate"]';
|
|||
|
||||
export const CUSTOM_QUERY_INPUT = '[data-test-subj="queryInput"]';
|
||||
|
||||
export const DEFINE_CONTINUE_BUTTON = '[data-test-subj="continue"]';
|
||||
export const DEFINE_CONTINUE_BUTTON = '[data-test-subj="define-continue"]';
|
||||
|
||||
export const SCHEDULE_CONTINUE_BUTTON = '[data-test-subj="schedule-continue"]';
|
||||
|
||||
export const FALSE_POSITIVES_INPUT =
|
||||
'[data-test-subj="detectionEngineStepAboutRuleFalsePositives"] input';
|
||||
|
@ -43,7 +45,8 @@ export const RULE_DESCRIPTION_INPUT =
|
|||
export const RULE_NAME_INPUT =
|
||||
'[data-test-subj="detectionEngineStepAboutRuleName"] [data-test-subj="input"]';
|
||||
|
||||
export const SEVERITY_DROPDOWN = '[data-test-subj="select"]';
|
||||
export const SEVERITY_DROPDOWN =
|
||||
'[data-test-subj="detectionEngineStepAboutRuleSeverity"] [data-test-subj="select"]';
|
||||
|
||||
export const TAGS_INPUT =
|
||||
'[data-test-subj="detectionEngineStepAboutRuleTags"] [data-test-subj="comboBoxSearchInput"]';
|
||||
|
|
|
@ -21,11 +21,13 @@ import {
|
|||
REFERENCE_URLS_INPUT,
|
||||
RULE_DESCRIPTION_INPUT,
|
||||
RULE_NAME_INPUT,
|
||||
SCHEDULE_CONTINUE_BUTTON,
|
||||
SEVERITY_DROPDOWN,
|
||||
TAGS_INPUT,
|
||||
} from '../screens/create_new_rule';
|
||||
|
||||
export const createAndActivateRule = () => {
|
||||
cy.get(SCHEDULE_CONTINUE_BUTTON).click({ force: true });
|
||||
cy.get(CREATE_AND_ACTIVATE_BTN).click({ force: true });
|
||||
cy.get(CREATE_AND_ACTIVATE_BTN).should('not.exist');
|
||||
};
|
||||
|
|
|
@ -39,7 +39,7 @@ describe('Detections Rules API', () => {
|
|||
await addRule({ rule: ruleMock, signal: abortCtrl.signal });
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules', {
|
||||
body:
|
||||
'{"description":"some desc","enabled":true,"false_positives":[],"filters":[],"from":"now-360s","index":["apm-*-transaction*","auditbeat-*","endgame-*","filebeat-*","packetbeat-*","winlogbeat-*"],"interval":"5m","rule_id":"bbd3106e-b4b5-4d7c-a1a2-47531d6a2baf","language":"kuery","risk_score":75,"name":"Test rule","query":"user.email: \'root@elastic.co\'","references":[],"severity":"high","tags":["APM"],"to":"now","type":"query","threat":[]}',
|
||||
'{"description":"some desc","enabled":true,"false_positives":[],"filters":[],"from":"now-360s","index":["apm-*-transaction*","auditbeat-*","endgame-*","filebeat-*","packetbeat-*","winlogbeat-*"],"interval":"5m","rule_id":"bbd3106e-b4b5-4d7c-a1a2-47531d6a2baf","language":"kuery","risk_score":75,"name":"Test rule","query":"user.email: \'root@elastic.co\'","references":[],"severity":"high","tags":["APM"],"to":"now","type":"query","threat":[],"throttle":null}',
|
||||
method: 'POST',
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
|
@ -291,7 +291,7 @@ describe('Detections Rules API', () => {
|
|||
await duplicateRules({ rules: rulesMock.data });
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_bulk_create', {
|
||||
body:
|
||||
'[{"description":"Elastic Endpoint detected Credential Dumping. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.","enabled":false,"false_positives":[],"from":"now-660s","index":["endgame-*"],"interval":"10m","language":"kuery","output_index":".siem-signals-default","max_signals":100,"risk_score":73,"name":"Credential Dumping - Detected - Elastic Endpoint [Duplicate]","query":"event.kind:alert and event.module:endgame and event.action:cred_theft_event and endgame.metadata.type:detection","filters":[],"references":[],"severity":"high","tags":["Elastic","Endpoint"],"to":"now","type":"query","threat":[],"version":1},{"description":"Elastic Endpoint detected an Adversary Behavior. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.","enabled":false,"false_positives":[],"from":"now-660s","index":["endgame-*"],"interval":"10m","language":"kuery","output_index":".siem-signals-default","max_signals":100,"risk_score":47,"name":"Adversary Behavior - Detected - Elastic Endpoint [Duplicate]","query":"event.kind:alert and event.module:endgame and event.action:rules_engine_event","filters":[],"references":[],"severity":"medium","tags":["Elastic","Endpoint"],"to":"now","type":"query","threat":[],"version":1}]',
|
||||
'[{"actions":[],"description":"Elastic Endpoint detected Credential Dumping. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.","enabled":false,"false_positives":[],"from":"now-660s","index":["endgame-*"],"interval":"10m","language":"kuery","output_index":".siem-signals-default","max_signals":100,"risk_score":73,"name":"Credential Dumping - Detected - Elastic Endpoint [Duplicate]","query":"event.kind:alert and event.module:endgame and event.action:cred_theft_event and endgame.metadata.type:detection","filters":[],"references":[],"severity":"high","tags":["Elastic","Endpoint"],"to":"now","type":"query","threat":[],"throttle":null,"version":1},{"actions":[],"description":"Elastic Endpoint detected an Adversary Behavior. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.","enabled":false,"false_positives":[],"from":"now-660s","index":["endgame-*"],"interval":"10m","language":"kuery","output_index":".siem-signals-default","max_signals":100,"risk_score":47,"name":"Adversary Behavior - Detected - Elastic Endpoint [Duplicate]","query":"event.kind:alert and event.module:endgame and event.action:rules_engine_event","filters":[],"references":[],"severity":"medium","tags":["Elastic","Endpoint"],"to":"now","type":"query","threat":[],"throttle":null,"version":1}]',
|
||||
method: 'POST',
|
||||
});
|
||||
});
|
||||
|
|
|
@ -32,9 +32,11 @@ export const ruleMock: NewRule = {
|
|||
to: 'now',
|
||||
type: 'query',
|
||||
threat: [],
|
||||
throttle: null,
|
||||
};
|
||||
|
||||
export const savedRuleMock: Rule = {
|
||||
actions: [],
|
||||
created_at: 'mm/dd/yyyyTHH:MM:sssz',
|
||||
created_by: 'mockUser',
|
||||
description: 'some desc',
|
||||
|
@ -65,6 +67,7 @@ export const savedRuleMock: Rule = {
|
|||
to: 'now',
|
||||
type: 'query',
|
||||
threat: [],
|
||||
throttle: null,
|
||||
updated_at: 'mm/dd/yyyyTHH:MM:sssz',
|
||||
updated_by: 'mockUser',
|
||||
};
|
||||
|
@ -75,6 +78,7 @@ export const rulesMock: FetchRulesResponse = {
|
|||
total: 2,
|
||||
data: [
|
||||
{
|
||||
actions: [],
|
||||
created_at: '2020-02-14T19:49:28.178Z',
|
||||
updated_at: '2020-02-14T19:49:28.320Z',
|
||||
created_by: 'elastic',
|
||||
|
@ -103,9 +107,11 @@ export const rulesMock: FetchRulesResponse = {
|
|||
to: 'now',
|
||||
type: 'query',
|
||||
threat: [],
|
||||
throttle: null,
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
actions: [],
|
||||
created_at: '2020-02-14T19:49:28.189Z',
|
||||
updated_at: '2020-02-14T19:49:28.326Z',
|
||||
created_by: 'elastic',
|
||||
|
@ -133,6 +139,7 @@ export const rulesMock: FetchRulesResponse = {
|
|||
to: 'now',
|
||||
type: 'query',
|
||||
threat: [],
|
||||
throttle: null,
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
|
|
|
@ -13,6 +13,19 @@ export const RuleTypeSchema = t.keyof({
|
|||
});
|
||||
export type RuleType = t.TypeOf<typeof RuleTypeSchema>;
|
||||
|
||||
/**
|
||||
* Params is an "record", since it is a type of AlertActionParams which is action templates.
|
||||
* @see x-pack/plugins/alerting/common/alert.ts
|
||||
*/
|
||||
export const action = t.exact(
|
||||
t.type({
|
||||
group: t.string,
|
||||
id: t.string,
|
||||
action_type_id: t.string,
|
||||
params: t.record(t.string, t.any),
|
||||
})
|
||||
);
|
||||
|
||||
export const NewRuleSchema = t.intersection([
|
||||
t.type({
|
||||
description: t.string,
|
||||
|
@ -24,6 +37,7 @@ export const NewRuleSchema = t.intersection([
|
|||
type: RuleTypeSchema,
|
||||
}),
|
||||
t.partial({
|
||||
actions: t.array(action),
|
||||
anomaly_threshold: t.number,
|
||||
created_by: t.string,
|
||||
false_positives: t.array(t.string),
|
||||
|
@ -40,6 +54,7 @@ export const NewRuleSchema = t.intersection([
|
|||
saved_id: t.string,
|
||||
tags: t.array(t.string),
|
||||
threat: t.array(t.unknown),
|
||||
throttle: t.union([t.string, t.null]),
|
||||
to: t.string,
|
||||
updated_by: t.string,
|
||||
note: t.string,
|
||||
|
@ -54,9 +69,15 @@ export interface AddRulesProps {
|
|||
signal: AbortSignal;
|
||||
}
|
||||
|
||||
const MetaRule = t.type({
|
||||
from: t.string,
|
||||
});
|
||||
const MetaRule = t.intersection([
|
||||
t.type({
|
||||
from: t.string,
|
||||
}),
|
||||
t.partial({
|
||||
throttle: t.string,
|
||||
kibanaSiemAppUrl: t.string,
|
||||
}),
|
||||
]);
|
||||
|
||||
export const RuleSchema = t.intersection([
|
||||
t.type({
|
||||
|
@ -81,6 +102,8 @@ export const RuleSchema = t.intersection([
|
|||
threat: t.array(t.unknown),
|
||||
updated_at: t.string,
|
||||
updated_by: t.string,
|
||||
actions: t.array(action),
|
||||
throttle: t.union([t.string, t.null]),
|
||||
}),
|
||||
t.partial({
|
||||
anomaly_threshold: t.number,
|
||||
|
|
|
@ -31,6 +31,7 @@ describe('useRule', () => {
|
|||
expect(result.current).toEqual([
|
||||
false,
|
||||
{
|
||||
actions: [],
|
||||
created_at: 'mm/dd/yyyyTHH:MM:sssz',
|
||||
created_by: 'mockUser',
|
||||
description: 'some desc',
|
||||
|
@ -59,6 +60,7 @@ describe('useRule', () => {
|
|||
severity: 'high',
|
||||
tags: ['APM'],
|
||||
threat: [],
|
||||
throttle: null,
|
||||
to: 'now',
|
||||
type: 'query',
|
||||
updated_at: 'mm/dd/yyyyTHH:MM:sssz',
|
||||
|
|
|
@ -58,6 +58,7 @@ describe('useRules', () => {
|
|||
{
|
||||
data: [
|
||||
{
|
||||
actions: [],
|
||||
created_at: '2020-02-14T19:49:28.178Z',
|
||||
created_by: 'elastic',
|
||||
description:
|
||||
|
@ -82,6 +83,7 @@ describe('useRules', () => {
|
|||
severity: 'high',
|
||||
tags: ['Elastic', 'Endpoint'],
|
||||
threat: [],
|
||||
throttle: null,
|
||||
to: 'now',
|
||||
type: 'query',
|
||||
updated_at: '2020-02-14T19:49:28.320Z',
|
||||
|
@ -89,6 +91,7 @@ describe('useRules', () => {
|
|||
version: 1,
|
||||
},
|
||||
{
|
||||
actions: [],
|
||||
created_at: '2020-02-14T19:49:28.189Z',
|
||||
created_by: 'elastic',
|
||||
description:
|
||||
|
@ -113,6 +116,7 @@ describe('useRules', () => {
|
|||
severity: 'medium',
|
||||
tags: ['Elastic', 'Endpoint'],
|
||||
threat: [],
|
||||
throttle: null,
|
||||
to: 'now',
|
||||
type: 'query',
|
||||
updated_at: '2020-02-14T19:49:28.326Z',
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { esFilters } from '../../../../../../../../../../src/plugins/data/public';
|
||||
import { Rule, RuleError } from '../../../../../containers/detection_engine/rules';
|
||||
import { AboutStepRule, DefineStepRule, ScheduleStepRule } from '../../types';
|
||||
import { AboutStepRule, ActionsStepRule, DefineStepRule, ScheduleStepRule } from '../../types';
|
||||
import { FieldValueQueryBar } from '../../components/query_bar';
|
||||
|
||||
export const mockQueryBar: FieldValueQueryBar = {
|
||||
|
@ -40,6 +40,7 @@ export const mockQueryBar: FieldValueQueryBar = {
|
|||
};
|
||||
|
||||
export const mockRule = (id: string): Rule => ({
|
||||
actions: [],
|
||||
created_at: '2020-01-10T21:11:45.839Z',
|
||||
updated_at: '2020-01-10T21:11:45.839Z',
|
||||
created_by: 'elastic',
|
||||
|
@ -70,11 +71,13 @@ export const mockRule = (id: string): Rule => ({
|
|||
to: 'now',
|
||||
type: 'saved_query',
|
||||
threat: [],
|
||||
throttle: null,
|
||||
note: '# this is some markdown documentation',
|
||||
version: 1,
|
||||
});
|
||||
|
||||
export const mockRuleWithEverything = (id: string): Rule => ({
|
||||
actions: [],
|
||||
created_at: '2020-01-10T21:11:45.839Z',
|
||||
updated_at: '2020-01-10T21:11:45.839Z',
|
||||
created_by: 'elastic',
|
||||
|
@ -142,6 +145,7 @@ export const mockRuleWithEverything = (id: string): Rule => ({
|
|||
],
|
||||
},
|
||||
],
|
||||
throttle: null,
|
||||
note: '# this is some markdown documentation',
|
||||
version: 1,
|
||||
});
|
||||
|
@ -175,6 +179,14 @@ export const mockAboutStepRule = (isNew = false): AboutStepRule => ({
|
|||
note: '# this is some markdown documentation',
|
||||
});
|
||||
|
||||
export const mockActionsStepRule = (isNew = false, enabled = false): ActionsStepRule => ({
|
||||
isNew,
|
||||
actions: [],
|
||||
kibanaSiemAppUrl: 'http://localhost:5601/app/siem',
|
||||
enabled,
|
||||
throttle: null,
|
||||
});
|
||||
|
||||
export const mockDefineStepRule = (isNew = false): DefineStepRule => ({
|
||||
isNew,
|
||||
ruleType: 'query',
|
||||
|
@ -188,9 +200,8 @@ export const mockDefineStepRule = (isNew = false): DefineStepRule => ({
|
|||
},
|
||||
});
|
||||
|
||||
export const mockScheduleStepRule = (isNew = false, enabled = false): ScheduleStepRule => ({
|
||||
export const mockScheduleStepRule = (isNew = false): ScheduleStepRule => ({
|
||||
isNew,
|
||||
enabled,
|
||||
interval: '5m',
|
||||
from: '6m',
|
||||
to: 'now',
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`NextStep renders correctly against snapshot 1`] = `
|
||||
<Fragment>
|
||||
<EuiHorizontalRule
|
||||
margin="m"
|
||||
/>
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
gutterSize="xs"
|
||||
justifyContent="flexEnd"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiButton
|
||||
data-test-subj="nextStep-continue"
|
||||
fill={true}
|
||||
isDisabled={false}
|
||||
onClick={[MockFunction]}
|
||||
>
|
||||
Continue
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Fragment>
|
||||
`;
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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 { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { NextStep } from './index';
|
||||
|
||||
describe('NextStep', () => {
|
||||
test('renders correctly against snapshot', () => {
|
||||
const wrapper = shallow(<NextStep onClick={jest.fn()} isDisabled={false} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui';
|
||||
import * as RuleI18n from '../../translations';
|
||||
|
||||
interface NextStepProps {
|
||||
onClick: () => Promise<void>;
|
||||
isDisabled: boolean;
|
||||
dataTestSubj?: string;
|
||||
}
|
||||
|
||||
export const NextStep = React.memo<NextStepProps>(
|
||||
({ onClick, isDisabled, dataTestSubj = 'nextStep-continue' }) => (
|
||||
<>
|
||||
<EuiHorizontalRule margin="m" />
|
||||
<EuiFlexGroup alignItems="center" justifyContent="flexEnd" gutterSize="xs" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton fill onClick={onClick} isDisabled={isDisabled} data-test-subj={dataTestSubj}>
|
||||
{RuleI18n.CONTINUE}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
)
|
||||
);
|
||||
|
||||
NextStep.displayName = 'NextStep';
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* 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, { useCallback, useEffect, useState } from 'react';
|
||||
import deepMerge from 'deepmerge';
|
||||
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { loadActionTypes } from '../../../../../../../../../plugins/triggers_actions_ui/public/application/lib/action_connector_api';
|
||||
import { SelectField } from '../../../../../shared_imports';
|
||||
import {
|
||||
ActionForm,
|
||||
ActionType,
|
||||
} 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';
|
||||
|
||||
type ThrottleSelectField = typeof SelectField;
|
||||
|
||||
const DEFAULT_ACTION_GROUP_ID = 'default';
|
||||
const DEFAULT_ACTION_MESSAGE =
|
||||
'Rule {{context.rule.name}} generated {{state.signals_count}} signals';
|
||||
|
||||
export const RuleActionsField: ThrottleSelectField = ({ field, messageVariables }) => {
|
||||
const [supportedActionTypes, setSupportedActionTypes] = useState<ActionType[] | undefined>();
|
||||
const {
|
||||
http,
|
||||
triggers_actions_ui: { actionTypeRegistry },
|
||||
notifications,
|
||||
} = useKibana().services;
|
||||
|
||||
const setActionIdByIndex = useCallback(
|
||||
(id: string, index: number) => {
|
||||
const updatedActions = [...(field.value as Array<Partial<AlertAction>>)];
|
||||
updatedActions[index] = deepMerge(updatedActions[index], { id });
|
||||
field.setValue(updatedActions);
|
||||
},
|
||||
[field]
|
||||
);
|
||||
|
||||
const setAlertProperty = useCallback(
|
||||
(updatedActions: AlertAction[]) => field.setValue(updatedActions),
|
||||
[field]
|
||||
);
|
||||
|
||||
const setActionParamsProperty = useCallback(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(key: string, value: any, index: number) => {
|
||||
const updatedActions = [...(field.value as AlertAction[])];
|
||||
updatedActions[index].params[key] = value;
|
||||
field.setValue(updatedActions);
|
||||
},
|
||||
[field]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
(async function() {
|
||||
const actionTypes = await loadActionTypes({ http });
|
||||
const supportedTypes = actionTypes.filter(actionType =>
|
||||
NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS.includes(actionType.id)
|
||||
);
|
||||
setSupportedActionTypes(supportedTypes);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
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}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -15,6 +15,17 @@ import { stepAboutDefaultValue } from './default_value';
|
|||
|
||||
const theme = () => ({ eui: euiDarkVars, darkMode: true });
|
||||
|
||||
/* eslint-disable no-console */
|
||||
// Silence until enzyme fixed to use ReactTestUtils.act()
|
||||
const originalError = console.error;
|
||||
beforeAll(() => {
|
||||
console.error = jest.fn();
|
||||
});
|
||||
afterAll(() => {
|
||||
console.error = originalError;
|
||||
});
|
||||
/* eslint-enable no-console */
|
||||
|
||||
describe('StepAboutRuleComponent', () => {
|
||||
test('it renders StepRuleDescription if isReadOnlyView is true and "name" property exists', () => {
|
||||
const wrapper = shallow(
|
||||
|
|
|
@ -4,22 +4,13 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiAccordion,
|
||||
EuiButton,
|
||||
EuiHorizontalRule,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
EuiButtonEmpty,
|
||||
} from '@elastic/eui';
|
||||
import { EuiAccordion, EuiFlexItem, EuiSpacer, EuiButtonEmpty } from '@elastic/eui';
|
||||
import React, { FC, memo, useCallback, useEffect, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
|
||||
import { setFieldValue } from '../../helpers';
|
||||
import { RuleStepProps, RuleStep, AboutStepRule } from '../../types';
|
||||
import * as RuleI18n from '../../translations';
|
||||
import { AddItem } from '../add_item_form';
|
||||
import { StepRuleDescription } from '../description_step';
|
||||
import { AddMitreThreat } from '../mitre';
|
||||
|
@ -38,6 +29,7 @@ import { isUrlInvalid } from './helpers';
|
|||
import { schema } from './schema';
|
||||
import * as I18n from './translations';
|
||||
import { StepContentWrapper } from '../step_content_wrapper';
|
||||
import { NextStep } from '../next_step';
|
||||
import { MarkdownEditorForm } from '../../../../../components/markdown_editor/form';
|
||||
|
||||
const CommonUseField = getUseField({ component: Field });
|
||||
|
@ -276,27 +268,9 @@ const StepAboutRuleComponent: FC<StepAboutRuleProps> = ({
|
|||
</FormDataProvider>
|
||||
</Form>
|
||||
</StepContentWrapper>
|
||||
|
||||
{!isUpdateView && (
|
||||
<>
|
||||
<EuiHorizontalRule margin="m" />
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
justifyContent="flexEnd"
|
||||
gutterSize="xs"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="about-continue"
|
||||
fill
|
||||
onClick={onSubmit}
|
||||
isDisabled={isLoading}
|
||||
>
|
||||
{RuleI18n.CONTINUE}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
<NextStep dataTestSubj="about-continue" onClick={onSubmit} isDisabled={isLoading} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
EuiFlexGroup,
|
||||
EuiResizeObserver,
|
||||
} from '@elastic/eui';
|
||||
import React, { memo, useState } from 'react';
|
||||
import React, { memo, useCallback, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
|
||||
|
@ -71,9 +71,12 @@ const StepAboutRuleToggleDetailsComponent: React.FC<StepPanelProps> = ({
|
|||
const [selectedToggleOption, setToggleOption] = useState('details');
|
||||
const [aboutPanelHeight, setAboutPanelHeight] = useState(0);
|
||||
|
||||
const onResize = (e: { height: number; width: number }) => {
|
||||
setAboutPanelHeight(e.height);
|
||||
};
|
||||
const onResize = useCallback(
|
||||
(e: { height: number; width: number }) => {
|
||||
setAboutPanelHeight(e.height);
|
||||
},
|
||||
[setAboutPanelHeight]
|
||||
);
|
||||
|
||||
return (
|
||||
<MyPanel>
|
||||
|
@ -85,7 +88,7 @@ const StepAboutRuleToggleDetailsComponent: React.FC<StepPanelProps> = ({
|
|||
)}
|
||||
{stepData != null && stepDataDetails != null && (
|
||||
<FlexGroupFullHeight gutterSize="xs" direction="column">
|
||||
<EuiFlexItem grow={1}>
|
||||
<EuiFlexItem grow={1} key="header">
|
||||
<HeaderSection title={i18n.ABOUT_TEXT}>
|
||||
{!isEmpty(stepDataDetails.note) && stepDataDetails.note.trim() !== '' && (
|
||||
<EuiButtonGroup
|
||||
|
@ -99,7 +102,7 @@ const StepAboutRuleToggleDetailsComponent: React.FC<StepPanelProps> = ({
|
|||
)}
|
||||
</HeaderSection>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={5}>
|
||||
<EuiFlexItem grow={5} key="details">
|
||||
{selectedToggleOption === 'details' ? (
|
||||
<EuiResizeObserver data-test-subj="stepAboutDetailsContent" onResize={onResize}>
|
||||
{resizeRef => (
|
||||
|
|
|
@ -4,14 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiHorizontalRule,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiButton,
|
||||
} from '@elastic/eui';
|
||||
import { EuiButtonEmpty, EuiFormRow } from '@elastic/eui';
|
||||
import React, { FC, memo, useCallback, useState, useEffect, useContext } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
|
@ -23,7 +16,6 @@ import { DEFAULT_TIMELINE_TITLE } from '../../../../../components/timeline/trans
|
|||
import { MlCapabilitiesContext } from '../../../../../components/ml/permissions/ml_capabilities_provider';
|
||||
import { useUiSetting$ } from '../../../../../lib/kibana';
|
||||
import { setFieldValue, isMlRule } from '../../helpers';
|
||||
import * as RuleI18n from '../../translations';
|
||||
import { DefineStepRule, RuleStep, RuleStepProps } from '../../types';
|
||||
import { StepRuleDescription } from '../description_step';
|
||||
import { QueryBarDefineRule } from '../query_bar';
|
||||
|
@ -32,6 +24,7 @@ import { AnomalyThresholdSlider } from '../anomaly_threshold_slider';
|
|||
import { MlJobSelect } from '../ml_job_select';
|
||||
import { PickTimeline } from '../pick_timeline';
|
||||
import { StepContentWrapper } from '../step_content_wrapper';
|
||||
import { NextStep } from '../next_step';
|
||||
import {
|
||||
Field,
|
||||
Form,
|
||||
|
@ -269,22 +262,9 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
|
|||
</FormDataProvider>
|
||||
</Form>
|
||||
</StepContentWrapper>
|
||||
|
||||
{!isUpdateView && (
|
||||
<>
|
||||
<EuiHorizontalRule margin="m" />
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
justifyContent="flexEnd"
|
||||
gutterSize="xs"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton fill onClick={onSubmit} isDisabled={isLoading} data-test-subj="continue">
|
||||
{RuleI18n.CONTINUE}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
<NextStep dataTestSubj="define-continue" onClick={onSubmit} isDisabled={isLoading} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,184 @@
|
|||
/*
|
||||
* 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 { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem, EuiButton, EuiSpacer } from '@elastic/eui';
|
||||
import React, { FC, memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
|
||||
import { setFieldValue } from '../../helpers';
|
||||
import { RuleStep, RuleStepProps, ActionsStepRule } from '../../types';
|
||||
import { StepRuleDescription } from '../description_step';
|
||||
import { Form, UseField, useForm } from '../../../../../shared_imports';
|
||||
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 * as I18n from './translations';
|
||||
|
||||
interface StepRuleActionsProps extends RuleStepProps {
|
||||
defaultValues?: ActionsStepRule | null;
|
||||
actionMessageParams: string[];
|
||||
}
|
||||
|
||||
const stepActionsDefaultValue = {
|
||||
enabled: true,
|
||||
isNew: true,
|
||||
actions: [],
|
||||
kibanaSiemAppUrl: '',
|
||||
throttle: THROTTLE_OPTIONS[0].value,
|
||||
};
|
||||
|
||||
const GhostFormField = () => <></>;
|
||||
|
||||
const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({
|
||||
addPadding = false,
|
||||
defaultValues,
|
||||
isReadOnlyView,
|
||||
isLoading,
|
||||
isUpdateView = false,
|
||||
setStepData,
|
||||
setForm,
|
||||
actionMessageParams,
|
||||
}) => {
|
||||
const [myStepData, setMyStepData] = useState<ActionsStepRule>(stepActionsDefaultValue);
|
||||
const {
|
||||
services: { application },
|
||||
} = useKibana();
|
||||
|
||||
const { form } = useForm({
|
||||
defaultValue: myStepData,
|
||||
options: { stripEmptyFields: false },
|
||||
schema,
|
||||
});
|
||||
|
||||
const kibanaAbsoluteUrl = useMemo(() => application.getUrlForApp('siem', { absolute: true }), [
|
||||
application,
|
||||
]);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (enabled: boolean) => {
|
||||
if (setStepData) {
|
||||
setStepData(RuleStep.ruleActions, null, false);
|
||||
const { isValid: newIsValid, data } = await form.submit();
|
||||
if (newIsValid) {
|
||||
setStepData(RuleStep.ruleActions, { ...data, enabled }, newIsValid);
|
||||
setMyStepData({ ...data, isNew: false } as ActionsStepRule);
|
||||
}
|
||||
}
|
||||
},
|
||||
[form]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const { isNew, ...initDefaultValue } = myStepData;
|
||||
if (defaultValues != null && !deepEqual(initDefaultValue, defaultValues)) {
|
||||
const myDefaultValues = {
|
||||
...defaultValues,
|
||||
isNew: false,
|
||||
};
|
||||
setMyStepData(myDefaultValues);
|
||||
setFieldValue(form, schema, myDefaultValues);
|
||||
}
|
||||
}, [defaultValues]);
|
||||
|
||||
useEffect(() => {
|
||||
if (setForm != null) {
|
||||
setForm(RuleStep.ruleActions, form);
|
||||
}
|
||||
}, [form]);
|
||||
|
||||
const updateThrottle = useCallback(throttle => setMyStepData({ ...myStepData, throttle }), [
|
||||
myStepData,
|
||||
setMyStepData,
|
||||
]);
|
||||
|
||||
const throttleFieldComponentProps = useMemo(
|
||||
() => ({
|
||||
idAria: 'detectionEngineStepRuleActionsThrottle',
|
||||
isDisabled: isLoading,
|
||||
dataTestSubj: 'detectionEngineStepRuleActionsThrottle',
|
||||
hasNoInitialSelection: false,
|
||||
handleChange: updateThrottle,
|
||||
euiFieldProps: {
|
||||
options: THROTTLE_OPTIONS,
|
||||
},
|
||||
}),
|
||||
[isLoading, updateThrottle]
|
||||
);
|
||||
|
||||
return isReadOnlyView && myStepData != null ? (
|
||||
<StepContentWrapper addPadding={addPadding}>
|
||||
<StepRuleDescription schema={schema} data={myStepData} />
|
||||
</StepContentWrapper>
|
||||
) : (
|
||||
<>
|
||||
<StepContentWrapper addPadding={!isUpdateView}>
|
||||
<Form form={form} data-test-subj="stepRuleActions">
|
||||
<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}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</StepContentWrapper>
|
||||
|
||||
{!isUpdateView && (
|
||||
<>
|
||||
<EuiHorizontalRule margin="m" />
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
justifyContent="flexEnd"
|
||||
gutterSize="xs"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill={false}
|
||||
isDisabled={isLoading}
|
||||
isLoading={isLoading}
|
||||
onClick={onSubmit.bind(null, false)}
|
||||
>
|
||||
{I18n.COMPLETE_WITHOUT_ACTIVATING}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
isDisabled={isLoading}
|
||||
isLoading={isLoading}
|
||||
onClick={onSubmit.bind(null, true)}
|
||||
data-test-subj="create-activate"
|
||||
>
|
||||
{I18n.COMPLETE_WITH_ACTIVATING}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const StepRuleActions = memo(StepRuleActionsComponent);
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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';
|
||||
|
||||
import { FormSchema } from '../../../../../shared_imports';
|
||||
|
||||
export const schema: FormSchema = {
|
||||
actions: {},
|
||||
kibanaSiemAppUrl: {},
|
||||
throttle: {
|
||||
label: i18n.translate(
|
||||
'xpack.siem.detectionEngine.createRule.stepRuleActions.fieldThrottleLabel',
|
||||
{
|
||||
defaultMessage: 'Actions frequency',
|
||||
}
|
||||
),
|
||||
helpText: i18n.translate(
|
||||
'xpack.siem.detectionEngine.createRule.stepRuleActions.fieldThrottleHelpText',
|
||||
{
|
||||
defaultMessage:
|
||||
'Select when automated actions should be performed if a rule evaluates as true.',
|
||||
}
|
||||
),
|
||||
},
|
||||
};
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 COMPLETE_WITHOUT_ACTIVATING = i18n.translate(
|
||||
'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',
|
||||
{
|
||||
defaultMessage: 'Create & activate rule',
|
||||
}
|
||||
);
|
|
@ -4,7 +4,6 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui';
|
||||
import React, { FC, memo, useCallback, useEffect, useState } from 'react';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import styled from 'styled-components';
|
||||
|
@ -15,8 +14,8 @@ import { StepRuleDescription } from '../description_step';
|
|||
import { ScheduleItem } from '../schedule_item_form';
|
||||
import { Form, UseField, useForm } from '../../../../../shared_imports';
|
||||
import { StepContentWrapper } from '../step_content_wrapper';
|
||||
import { NextStep } from '../next_step';
|
||||
import { schema } from './schema';
|
||||
import * as I18n from './translations';
|
||||
|
||||
interface StepScheduleRuleProps extends RuleStepProps {
|
||||
defaultValues?: ScheduleStepRule | null;
|
||||
|
@ -27,7 +26,6 @@ const RestrictedWidthContainer = styled.div`
|
|||
`;
|
||||
|
||||
const stepScheduleDefaultValue = {
|
||||
enabled: true,
|
||||
interval: '5m',
|
||||
isNew: true,
|
||||
from: '1m',
|
||||
|
@ -51,19 +49,16 @@ const StepScheduleRuleComponent: FC<StepScheduleRuleProps> = ({
|
|||
schema,
|
||||
});
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (enabled: boolean) => {
|
||||
if (setStepData) {
|
||||
setStepData(RuleStep.scheduleRule, null, false);
|
||||
const { isValid: newIsValid, data } = await form.submit();
|
||||
if (newIsValid) {
|
||||
setStepData(RuleStep.scheduleRule, { ...data, enabled }, newIsValid);
|
||||
setMyStepData({ ...data, isNew: false } as ScheduleStepRule);
|
||||
}
|
||||
const onSubmit = useCallback(async () => {
|
||||
if (setStepData) {
|
||||
setStepData(RuleStep.scheduleRule, null, false);
|
||||
const { isValid: newIsValid, data } = await form.submit();
|
||||
if (newIsValid) {
|
||||
setStepData(RuleStep.scheduleRule, { ...data }, newIsValid);
|
||||
setMyStepData({ ...data, isNew: false } as ScheduleStepRule);
|
||||
}
|
||||
},
|
||||
[form]
|
||||
);
|
||||
}
|
||||
}, [form]);
|
||||
|
||||
useEffect(() => {
|
||||
const { isNew, ...initDefaultValue } = myStepData;
|
||||
|
@ -118,37 +113,7 @@ const StepScheduleRuleComponent: FC<StepScheduleRuleProps> = ({
|
|||
</StepContentWrapper>
|
||||
|
||||
{!isUpdateView && (
|
||||
<>
|
||||
<EuiHorizontalRule margin="m" />
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
justifyContent="flexEnd"
|
||||
gutterSize="xs"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill={false}
|
||||
isDisabled={isLoading}
|
||||
isLoading={isLoading}
|
||||
onClick={onSubmit.bind(null, false)}
|
||||
>
|
||||
{I18n.COMPLETE_WITHOUT_ACTIVATING}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
isDisabled={isLoading}
|
||||
isLoading={isLoading}
|
||||
onClick={onSubmit.bind(null, true)}
|
||||
data-test-subj="create-activate"
|
||||
>
|
||||
{I18n.COMPLETE_WITH_ACTIVATING}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
<NextStep dataTestSubj="schedule-continue" onClick={onSubmit} isDisabled={isLoading} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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, { useCallback } from 'react';
|
||||
|
||||
import {
|
||||
NOTIFICATION_THROTTLE_RULE,
|
||||
NOTIFICATION_THROTTLE_NO_ACTIONS,
|
||||
} from '../../../../../../common/constants';
|
||||
import { SelectField } from '../../../../../shared_imports';
|
||||
|
||||
export const THROTTLE_OPTIONS = [
|
||||
{ value: NOTIFICATION_THROTTLE_NO_ACTIONS, text: 'Perform no actions' },
|
||||
{ value: NOTIFICATION_THROTTLE_RULE, text: 'On each rule execution' },
|
||||
{ value: '1h', text: 'Hourly' },
|
||||
{ value: '1d', text: 'Daily' },
|
||||
{ value: '7d', text: 'Weekly' },
|
||||
];
|
||||
|
||||
type ThrottleSelectField = typeof SelectField;
|
||||
|
||||
export const ThrottleSelectField: ThrottleSelectField = props => {
|
||||
const onChange = useCallback(
|
||||
e => {
|
||||
const throttle = e.target.value;
|
||||
props.field.setValue(throttle);
|
||||
props.handleChange(throttle);
|
||||
},
|
||||
[props.field.setValue, props.handleChange]
|
||||
);
|
||||
const newEuiFieldProps = { ...props.euiFieldProps, onChange };
|
||||
return <SelectField {...props} euiFieldProps={newEuiFieldProps} />;
|
||||
};
|
|
@ -9,7 +9,9 @@ import {
|
|||
DefineStepRuleJson,
|
||||
ScheduleStepRuleJson,
|
||||
AboutStepRuleJson,
|
||||
ActionsStepRuleJson,
|
||||
AboutStepRule,
|
||||
ActionsStepRule,
|
||||
ScheduleStepRule,
|
||||
DefineStepRule,
|
||||
} from '../types';
|
||||
|
@ -18,6 +20,7 @@ import {
|
|||
formatDefineStepData,
|
||||
formatScheduleStepData,
|
||||
formatAboutStepData,
|
||||
formatActionsStepData,
|
||||
formatRule,
|
||||
filterRuleFieldsForType,
|
||||
} from './helpers';
|
||||
|
@ -26,6 +29,7 @@ import {
|
|||
mockQueryBar,
|
||||
mockScheduleStepRule,
|
||||
mockAboutStepRule,
|
||||
mockActionsStepRule,
|
||||
} from '../all/__mocks__/mock';
|
||||
|
||||
describe('helpers', () => {
|
||||
|
@ -241,7 +245,6 @@ describe('helpers', () => {
|
|||
test('returns formatted object as ScheduleStepRuleJson', () => {
|
||||
const result: ScheduleStepRuleJson = formatScheduleStepData(mockData);
|
||||
const expected = {
|
||||
enabled: false,
|
||||
from: 'now-660s',
|
||||
to: 'now',
|
||||
interval: '5m',
|
||||
|
@ -260,7 +263,6 @@ describe('helpers', () => {
|
|||
delete mockStepData.to;
|
||||
const result: ScheduleStepRuleJson = formatScheduleStepData(mockStepData);
|
||||
const expected = {
|
||||
enabled: false,
|
||||
from: 'now-660s',
|
||||
to: 'now',
|
||||
interval: '5m',
|
||||
|
@ -279,7 +281,6 @@ describe('helpers', () => {
|
|||
};
|
||||
const result: ScheduleStepRuleJson = formatScheduleStepData(mockStepData);
|
||||
const expected = {
|
||||
enabled: false,
|
||||
from: 'now-660s',
|
||||
to: 'now',
|
||||
interval: '5m',
|
||||
|
@ -298,7 +299,6 @@ describe('helpers', () => {
|
|||
};
|
||||
const result: ScheduleStepRuleJson = formatScheduleStepData(mockStepData);
|
||||
const expected = {
|
||||
enabled: false,
|
||||
from: 'now-300s',
|
||||
to: 'now',
|
||||
interval: '5m',
|
||||
|
@ -317,7 +317,6 @@ describe('helpers', () => {
|
|||
};
|
||||
const result: ScheduleStepRuleJson = formatScheduleStepData(mockStepData);
|
||||
const expected = {
|
||||
enabled: false,
|
||||
from: 'now-360s',
|
||||
to: 'now',
|
||||
interval: 'random',
|
||||
|
@ -503,19 +502,164 @@ describe('helpers', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('formatActionsStepData', () => {
|
||||
let mockData: ActionsStepRule;
|
||||
|
||||
beforeEach(() => {
|
||||
mockData = mockActionsStepRule();
|
||||
});
|
||||
|
||||
test('returns formatted object as ActionsStepRuleJson', () => {
|
||||
const result: ActionsStepRuleJson = formatActionsStepData(mockData);
|
||||
const expected = {
|
||||
actions: [],
|
||||
enabled: false,
|
||||
meta: {
|
||||
throttle: 'no_actions',
|
||||
kibanaSiemAppUrl: 'http://localhost:5601/app/siem',
|
||||
},
|
||||
throttle: null,
|
||||
};
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
test('returns proper throttle value for no_actions', () => {
|
||||
const mockStepData = {
|
||||
...mockData,
|
||||
throttle: 'no_actions',
|
||||
};
|
||||
const result: ActionsStepRuleJson = formatActionsStepData(mockStepData);
|
||||
const expected = {
|
||||
actions: [],
|
||||
enabled: false,
|
||||
meta: {
|
||||
throttle: mockStepData.throttle,
|
||||
kibanaSiemAppUrl: mockStepData.kibanaSiemAppUrl,
|
||||
},
|
||||
throttle: null,
|
||||
};
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
test('returns proper throttle value for rule', () => {
|
||||
const mockStepData = {
|
||||
...mockData,
|
||||
throttle: 'rule',
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: 'id',
|
||||
actionTypeId: 'actionTypeId',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
const result: ActionsStepRuleJson = formatActionsStepData(mockStepData);
|
||||
const expected = {
|
||||
actions: [
|
||||
{
|
||||
group: mockStepData.actions[0].group,
|
||||
id: mockStepData.actions[0].id,
|
||||
action_type_id: mockStepData.actions[0].actionTypeId,
|
||||
params: mockStepData.actions[0].params,
|
||||
},
|
||||
],
|
||||
enabled: false,
|
||||
meta: {
|
||||
throttle: mockStepData.throttle,
|
||||
kibanaSiemAppUrl: mockStepData.kibanaSiemAppUrl,
|
||||
},
|
||||
throttle: null,
|
||||
};
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
test('returns proper throttle value for interval', () => {
|
||||
const mockStepData = {
|
||||
...mockData,
|
||||
throttle: '1d',
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: 'id',
|
||||
actionTypeId: 'actionTypeId',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
const result: ActionsStepRuleJson = formatActionsStepData(mockStepData);
|
||||
const expected = {
|
||||
actions: [
|
||||
{
|
||||
group: mockStepData.actions[0].group,
|
||||
id: mockStepData.actions[0].id,
|
||||
action_type_id: mockStepData.actions[0].actionTypeId,
|
||||
params: mockStepData.actions[0].params,
|
||||
},
|
||||
],
|
||||
enabled: false,
|
||||
meta: {
|
||||
throttle: mockStepData.throttle,
|
||||
kibanaSiemAppUrl: mockStepData.kibanaSiemAppUrl,
|
||||
},
|
||||
throttle: mockStepData.throttle,
|
||||
};
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
test('returns actions with action_type_id', () => {
|
||||
const mockAction = {
|
||||
group: 'default',
|
||||
id: '99403909-ca9b-49ba-9d7a-7e5320e68d05',
|
||||
params: { message: 'ML Rule generated {{state.signals_count}} signals' },
|
||||
actionTypeId: '.slack',
|
||||
};
|
||||
|
||||
const mockStepData = {
|
||||
...mockData,
|
||||
actions: [mockAction],
|
||||
};
|
||||
const result: ActionsStepRuleJson = formatActionsStepData(mockStepData);
|
||||
const expected = {
|
||||
actions: [
|
||||
{
|
||||
group: mockAction.group,
|
||||
id: mockAction.id,
|
||||
params: mockAction.params,
|
||||
action_type_id: mockAction.actionTypeId,
|
||||
},
|
||||
],
|
||||
enabled: false,
|
||||
meta: {
|
||||
throttle: null,
|
||||
kibanaSiemAppUrl: mockStepData.kibanaSiemAppUrl,
|
||||
},
|
||||
throttle: null,
|
||||
};
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatRule', () => {
|
||||
let mockAbout: AboutStepRule;
|
||||
let mockDefine: DefineStepRule;
|
||||
let mockSchedule: ScheduleStepRule;
|
||||
let mockActions: ActionsStepRule;
|
||||
|
||||
beforeEach(() => {
|
||||
mockAbout = mockAboutStepRule();
|
||||
mockDefine = mockDefineStepRule();
|
||||
mockSchedule = mockScheduleStepRule();
|
||||
mockActions = mockActionsStepRule();
|
||||
});
|
||||
|
||||
test('returns NewRule with type of saved_query when saved_id exists', () => {
|
||||
const result: NewRule = formatRule(mockDefine, mockAbout, mockSchedule);
|
||||
const result: NewRule = formatRule(mockDefine, mockAbout, mockSchedule, mockActions);
|
||||
|
||||
expect(result.type).toEqual('saved_query');
|
||||
});
|
||||
|
@ -528,13 +672,18 @@ describe('helpers', () => {
|
|||
saved_id: '',
|
||||
},
|
||||
};
|
||||
const result: NewRule = formatRule(mockDefineStepRuleWithoutSavedId, mockAbout, mockSchedule);
|
||||
const result: NewRule = formatRule(
|
||||
mockDefineStepRuleWithoutSavedId,
|
||||
mockAbout,
|
||||
mockSchedule,
|
||||
mockActions
|
||||
);
|
||||
|
||||
expect(result.type).toEqual('query');
|
||||
});
|
||||
|
||||
test('returns NewRule without id if ruleId does not exist', () => {
|
||||
const result: NewRule = formatRule(mockDefine, mockAbout, mockSchedule);
|
||||
const result: NewRule = formatRule(mockDefine, mockAbout, mockSchedule, mockActions);
|
||||
|
||||
expect(result.id).toBeUndefined();
|
||||
});
|
||||
|
|
|
@ -6,16 +6,24 @@
|
|||
|
||||
import { has, isEmpty } from 'lodash/fp';
|
||||
import moment from 'moment';
|
||||
import deepmerge from 'deepmerge';
|
||||
|
||||
import {
|
||||
NOTIFICATION_THROTTLE_RULE,
|
||||
NOTIFICATION_THROTTLE_NO_ACTIONS,
|
||||
} from '../../../../../common/constants';
|
||||
import { NewRule, RuleType } from '../../../../containers/detection_engine/rules';
|
||||
import { transformAlertToRuleAction } from '../../../../../common/detection_engine/transform_actions';
|
||||
|
||||
import {
|
||||
AboutStepRule,
|
||||
DefineStepRule,
|
||||
ScheduleStepRule,
|
||||
ActionsStepRule,
|
||||
DefineStepRuleJson,
|
||||
ScheduleStepRuleJson,
|
||||
AboutStepRuleJson,
|
||||
ActionsStepRuleJson,
|
||||
} from '../types';
|
||||
import { isMlRule } from '../helpers';
|
||||
|
||||
|
@ -136,12 +144,39 @@ export const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRule
|
|||
};
|
||||
};
|
||||
|
||||
export const getAlertThrottle = (throttle: string | null) =>
|
||||
throttle && ![NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE].includes(throttle)
|
||||
? throttle
|
||||
: null;
|
||||
|
||||
export const formatActionsStepData = (actionsStepData: ActionsStepRule): ActionsStepRuleJson => {
|
||||
const {
|
||||
actions = [],
|
||||
enabled,
|
||||
kibanaSiemAppUrl,
|
||||
throttle = NOTIFICATION_THROTTLE_NO_ACTIONS,
|
||||
} = actionsStepData;
|
||||
|
||||
return {
|
||||
actions: actions.map(transformAlertToRuleAction),
|
||||
enabled,
|
||||
throttle: actions.length ? getAlertThrottle(throttle) : null,
|
||||
meta: {
|
||||
throttle: actions.length ? throttle : NOTIFICATION_THROTTLE_NO_ACTIONS,
|
||||
kibanaSiemAppUrl,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const formatRule = (
|
||||
defineStepData: DefineStepRule,
|
||||
aboutStepData: AboutStepRule,
|
||||
scheduleData: ScheduleStepRule
|
||||
): NewRule => ({
|
||||
...formatDefineStepData(defineStepData),
|
||||
...formatAboutStepData(aboutStepData),
|
||||
...formatScheduleStepData(scheduleData),
|
||||
});
|
||||
scheduleData: ScheduleStepRule,
|
||||
actionsData: ActionsStepRule
|
||||
): NewRule =>
|
||||
deepmerge.all([
|
||||
formatDefineStepData(defineStepData),
|
||||
formatAboutStepData(aboutStepData),
|
||||
formatScheduleStepData(scheduleData),
|
||||
formatActionsStepData(actionsData),
|
||||
]) as NewRule;
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { EuiButtonEmpty, EuiAccordion, EuiHorizontalRule, EuiPanel, EuiSpacer } from '@elastic/eui';
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import React, { useCallback, useRef, useState, useMemo } from 'react';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import styled, { StyledComponent } from 'styled-components';
|
||||
|
||||
|
@ -21,14 +21,27 @@ import { FormData, FormHook } from '../../../../shared_imports';
|
|||
import { StepAboutRule } from '../components/step_about_rule';
|
||||
import { StepDefineRule } from '../components/step_define_rule';
|
||||
import { StepScheduleRule } from '../components/step_schedule_rule';
|
||||
import { StepRuleActions } from '../components/step_rule_actions';
|
||||
import { DetectionEngineHeaderPage } from '../../components/detection_engine_header_page';
|
||||
import * as RuleI18n from '../translations';
|
||||
import { redirectToDetections } from '../helpers';
|
||||
import { AboutStepRule, DefineStepRule, RuleStep, RuleStepData, ScheduleStepRule } from '../types';
|
||||
import { redirectToDetections, getActionMessageParams } from '../helpers';
|
||||
import {
|
||||
AboutStepRule,
|
||||
DefineStepRule,
|
||||
RuleStep,
|
||||
RuleStepData,
|
||||
ScheduleStepRule,
|
||||
ActionsStepRule,
|
||||
} from '../types';
|
||||
import { formatRule } from './helpers';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const stepsRuleOrder = [RuleStep.defineRule, RuleStep.aboutRule, RuleStep.scheduleRule];
|
||||
const stepsRuleOrder = [
|
||||
RuleStep.defineRule,
|
||||
RuleStep.aboutRule,
|
||||
RuleStep.scheduleRule,
|
||||
RuleStep.ruleActions,
|
||||
];
|
||||
|
||||
const MyEuiPanel = styled(EuiPanel)<{
|
||||
zindex?: number;
|
||||
|
@ -79,22 +92,31 @@ const CreateRulePageComponent: React.FC = () => {
|
|||
const defineRuleRef = useRef<EuiAccordion | null>(null);
|
||||
const aboutRuleRef = useRef<EuiAccordion | null>(null);
|
||||
const scheduleRuleRef = useRef<EuiAccordion | null>(null);
|
||||
const ruleActionsRef = useRef<EuiAccordion | null>(null);
|
||||
const stepsForm = useRef<Record<RuleStep, FormHook<FormData> | null>>({
|
||||
[RuleStep.defineRule]: null,
|
||||
[RuleStep.aboutRule]: null,
|
||||
[RuleStep.scheduleRule]: null,
|
||||
[RuleStep.ruleActions]: null,
|
||||
});
|
||||
const stepsData = useRef<Record<RuleStep, RuleStepData>>({
|
||||
[RuleStep.defineRule]: { isValid: false, data: {} },
|
||||
[RuleStep.aboutRule]: { isValid: false, data: {} },
|
||||
[RuleStep.scheduleRule]: { isValid: false, data: {} },
|
||||
[RuleStep.ruleActions]: { isValid: false, data: {} },
|
||||
});
|
||||
const [isStepRuleInReadOnlyView, setIsStepRuleInEditView] = useState<Record<RuleStep, boolean>>({
|
||||
[RuleStep.defineRule]: false,
|
||||
[RuleStep.aboutRule]: false,
|
||||
[RuleStep.scheduleRule]: false,
|
||||
[RuleStep.ruleActions]: false,
|
||||
});
|
||||
const [{ isLoading, isSaved }, setRule] = usePersistRule();
|
||||
const actionMessageParams = useMemo(
|
||||
() =>
|
||||
getActionMessageParams((stepsData.current['define-rule'].data as DefineStepRule).ruleType),
|
||||
[stepsData.current['define-rule'].data]
|
||||
);
|
||||
const userHasNoPermissions =
|
||||
canUserCRUD != null && hasManageApiKey != null ? !canUserCRUD || !hasManageApiKey : false;
|
||||
|
||||
|
@ -103,7 +125,7 @@ const CreateRulePageComponent: React.FC = () => {
|
|||
stepsData.current[step] = { ...stepsData.current[step], data, isValid };
|
||||
if (isValid) {
|
||||
const stepRuleIdx = stepsRuleOrder.findIndex(item => step === item);
|
||||
if ([0, 1].includes(stepRuleIdx)) {
|
||||
if ([0, 1, 2].includes(stepRuleIdx)) {
|
||||
if (isStepRuleInReadOnlyView[stepsRuleOrder[stepRuleIdx + 1]]) {
|
||||
setOpenAccordionId(stepsRuleOrder[stepRuleIdx + 1]);
|
||||
setIsStepRuleInEditView({
|
||||
|
@ -120,15 +142,17 @@ const CreateRulePageComponent: React.FC = () => {
|
|||
setOpenAccordionId(stepsRuleOrder[stepRuleIdx + 1]);
|
||||
}
|
||||
} else if (
|
||||
stepRuleIdx === 2 &&
|
||||
stepRuleIdx === 3 &&
|
||||
stepsData.current[RuleStep.defineRule].isValid &&
|
||||
stepsData.current[RuleStep.aboutRule].isValid
|
||||
stepsData.current[RuleStep.aboutRule].isValid &&
|
||||
stepsData.current[RuleStep.scheduleRule].isValid
|
||||
) {
|
||||
setRule(
|
||||
formatRule(
|
||||
stepsData.current[RuleStep.defineRule].data as DefineStepRule,
|
||||
stepsData.current[RuleStep.aboutRule].data as AboutStepRule,
|
||||
stepsData.current[RuleStep.scheduleRule].data as ScheduleStepRule
|
||||
stepsData.current[RuleStep.scheduleRule].data as ScheduleStepRule,
|
||||
stepsData.current[RuleStep.ruleActions].data as ActionsStepRule
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -177,6 +201,14 @@ const CreateRulePageComponent: React.FC = () => {
|
|||
/>
|
||||
);
|
||||
|
||||
const ruleActionsButton = (
|
||||
<AccordionTitle
|
||||
name="4"
|
||||
title={RuleI18n.RULE_ACTIONS}
|
||||
type={getAccordionType(RuleStep.ruleActions)}
|
||||
/>
|
||||
);
|
||||
|
||||
const openCloseAccordion = (accordionId: RuleStep | null) => {
|
||||
if (accordionId != null) {
|
||||
if (accordionId === RuleStep.defineRule && defineRuleRef.current != null) {
|
||||
|
@ -185,6 +217,8 @@ const CreateRulePageComponent: React.FC = () => {
|
|||
aboutRuleRef.current.onToggle();
|
||||
} else if (accordionId === RuleStep.scheduleRule && scheduleRuleRef.current != null) {
|
||||
scheduleRuleRef.current.onToggle();
|
||||
} else if (accordionId === RuleStep.ruleActions && ruleActionsRef.current != null) {
|
||||
ruleActionsRef.current.onToggle();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -253,7 +287,7 @@ const CreateRulePageComponent: React.FC = () => {
|
|||
isLoading={isLoading || loading}
|
||||
title={i18n.PAGE_TITLE}
|
||||
/>
|
||||
<MyEuiPanel zindex={3}>
|
||||
<MyEuiPanel zindex={4}>
|
||||
<StepDefineRuleAccordion
|
||||
initialIsOpen={true}
|
||||
id={RuleStep.defineRule}
|
||||
|
@ -288,7 +322,7 @@ const CreateRulePageComponent: React.FC = () => {
|
|||
</StepDefineRuleAccordion>
|
||||
</MyEuiPanel>
|
||||
<EuiSpacer size="l" />
|
||||
<MyEuiPanel zindex={2}>
|
||||
<MyEuiPanel zindex={3}>
|
||||
<EuiAccordion
|
||||
initialIsOpen={false}
|
||||
id={RuleStep.aboutRule}
|
||||
|
@ -321,7 +355,7 @@ const CreateRulePageComponent: React.FC = () => {
|
|||
</EuiAccordion>
|
||||
</MyEuiPanel>
|
||||
<EuiSpacer size="l" />
|
||||
<MyEuiPanel zindex={1}>
|
||||
<MyEuiPanel zindex={2}>
|
||||
<EuiAccordion
|
||||
initialIsOpen={false}
|
||||
id={RuleStep.scheduleRule}
|
||||
|
@ -355,6 +389,38 @@ const CreateRulePageComponent: React.FC = () => {
|
|||
/>
|
||||
</EuiAccordion>
|
||||
</MyEuiPanel>
|
||||
<EuiSpacer size="l" />
|
||||
<MyEuiPanel zindex={1}>
|
||||
<EuiAccordion
|
||||
initialIsOpen={false}
|
||||
id={RuleStep.ruleActions}
|
||||
buttonContent={ruleActionsButton}
|
||||
paddingSize="xs"
|
||||
ref={ruleActionsRef}
|
||||
onToggle={manageAccordions.bind(null, RuleStep.ruleActions)}
|
||||
extraAction={
|
||||
stepsData.current[RuleStep.ruleActions].isValid && (
|
||||
<EuiButtonEmpty
|
||||
iconType="pencil"
|
||||
size="xs"
|
||||
onClick={manageIsEditable.bind(null, RuleStep.ruleActions)}
|
||||
>
|
||||
{i18n.EDIT_RULE}
|
||||
</EuiButtonEmpty>
|
||||
)
|
||||
}
|
||||
>
|
||||
<EuiHorizontalRule margin="m" />
|
||||
<StepRuleActions
|
||||
addPadding={true}
|
||||
isReadOnlyView={isStepRuleInReadOnlyView[RuleStep.ruleActions]}
|
||||
isLoading={isLoading || loading}
|
||||
setForm={setStepsForm}
|
||||
setStepData={setStepData}
|
||||
actionMessageParams={actionMessageParams}
|
||||
/>
|
||||
</EuiAccordion>
|
||||
</MyEuiPanel>
|
||||
</WrapperPage>
|
||||
|
||||
<SpyRoute />
|
||||
|
|
|
@ -31,10 +31,17 @@ import { StepPanel } from '../components/step_panel';
|
|||
import { StepAboutRule } from '../components/step_about_rule';
|
||||
import { StepDefineRule } from '../components/step_define_rule';
|
||||
import { StepScheduleRule } from '../components/step_schedule_rule';
|
||||
import { StepRuleActions } from '../components/step_rule_actions';
|
||||
import { formatRule } from '../create/helpers';
|
||||
import { getStepsData, redirectToDetections } from '../helpers';
|
||||
import { getStepsData, redirectToDetections, getActionMessageParams } from '../helpers';
|
||||
import * as ruleI18n from '../translations';
|
||||
import { RuleStep, DefineStepRule, AboutStepRule, ScheduleStepRule } from '../types';
|
||||
import {
|
||||
RuleStep,
|
||||
DefineStepRule,
|
||||
AboutStepRule,
|
||||
ScheduleStepRule,
|
||||
ActionsStepRule,
|
||||
} from '../types';
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface StepRuleForm {
|
||||
|
@ -50,6 +57,10 @@ interface ScheduleStepRuleForm extends StepRuleForm {
|
|||
data: ScheduleStepRule | null;
|
||||
}
|
||||
|
||||
interface ActionsStepRuleForm extends StepRuleForm {
|
||||
data: ActionsStepRule | null;
|
||||
}
|
||||
|
||||
const EditRulePageComponent: FC = () => {
|
||||
const [, dispatchToaster] = useStateToaster();
|
||||
const {
|
||||
|
@ -79,14 +90,20 @@ const EditRulePageComponent: FC = () => {
|
|||
data: null,
|
||||
isValid: false,
|
||||
});
|
||||
const [myActionsRuleForm, setMyActionsRuleForm] = useState<ActionsStepRuleForm>({
|
||||
data: null,
|
||||
isValid: false,
|
||||
});
|
||||
const [selectedTab, setSelectedTab] = useState<EuiTabbedContentTab>();
|
||||
const stepsForm = useRef<Record<RuleStep, FormHook<FormData> | null>>({
|
||||
[RuleStep.defineRule]: null,
|
||||
[RuleStep.aboutRule]: null,
|
||||
[RuleStep.scheduleRule]: null,
|
||||
[RuleStep.ruleActions]: null,
|
||||
});
|
||||
const [{ isLoading, isSaved }, setRule] = usePersistRule();
|
||||
const [tabHasError, setTabHasError] = useState<RuleStep[]>([]);
|
||||
const actionMessageParams = useMemo(() => getActionMessageParams(rule?.type), [rule]);
|
||||
const setStepsForm = useCallback(
|
||||
(step: RuleStep, form: FormHook<FormData>) => {
|
||||
stepsForm.current[step] = form;
|
||||
|
@ -162,6 +179,28 @@ const EditRulePageComponent: FC = () => {
|
|||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: RuleStep.ruleActions,
|
||||
name: ruleI18n.ACTIONS,
|
||||
content: (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<StepPanel loading={loading || initLoading} title={ruleI18n.ACTIONS}>
|
||||
{myActionsRuleForm.data != null && (
|
||||
<StepRuleActions
|
||||
isReadOnlyView={false}
|
||||
isLoading={isLoading}
|
||||
isUpdateView
|
||||
defaultValues={myActionsRuleForm.data}
|
||||
setForm={setStepsForm}
|
||||
actionMessageParams={actionMessageParams}
|
||||
/>
|
||||
)}
|
||||
<EuiSpacer />
|
||||
</StepPanel>
|
||||
</>
|
||||
),
|
||||
},
|
||||
],
|
||||
[
|
||||
loading,
|
||||
|
@ -170,8 +209,10 @@ const EditRulePageComponent: FC = () => {
|
|||
myAboutRuleForm,
|
||||
myDefineRuleForm,
|
||||
myScheduleRuleForm,
|
||||
myActionsRuleForm,
|
||||
setStepsForm,
|
||||
stepsForm,
|
||||
actionMessageParams,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -179,14 +220,18 @@ const EditRulePageComponent: FC = () => {
|
|||
const activeFormId = selectedTab?.id as RuleStep;
|
||||
const activeForm = await stepsForm.current[activeFormId]?.submit();
|
||||
|
||||
const invalidForms = [RuleStep.aboutRule, RuleStep.defineRule, RuleStep.scheduleRule].reduce<
|
||||
RuleStep[]
|
||||
>((acc, step) => {
|
||||
const invalidForms = [
|
||||
RuleStep.aboutRule,
|
||||
RuleStep.defineRule,
|
||||
RuleStep.scheduleRule,
|
||||
RuleStep.ruleActions,
|
||||
].reduce<RuleStep[]>((acc, step) => {
|
||||
if (
|
||||
(step === activeFormId && activeForm != null && !activeForm?.isValid) ||
|
||||
(step === RuleStep.aboutRule && !myAboutRuleForm.isValid) ||
|
||||
(step === RuleStep.defineRule && !myDefineRuleForm.isValid) ||
|
||||
(step === RuleStep.scheduleRule && !myScheduleRuleForm.isValid)
|
||||
(step === RuleStep.scheduleRule && !myScheduleRuleForm.isValid) ||
|
||||
(step === RuleStep.ruleActions && !myActionsRuleForm.isValid)
|
||||
) {
|
||||
return [...acc, step];
|
||||
}
|
||||
|
@ -205,21 +250,35 @@ const EditRulePageComponent: FC = () => {
|
|||
: myAboutRuleForm.data) as AboutStepRule,
|
||||
(activeFormId === RuleStep.scheduleRule
|
||||
? activeForm.data
|
||||
: myScheduleRuleForm.data) as ScheduleStepRule
|
||||
: myScheduleRuleForm.data) as ScheduleStepRule,
|
||||
(activeFormId === RuleStep.ruleActions
|
||||
? activeForm.data
|
||||
: myActionsRuleForm.data) as ActionsStepRule
|
||||
),
|
||||
...(ruleId ? { id: ruleId } : {}),
|
||||
});
|
||||
} else {
|
||||
setTabHasError(invalidForms);
|
||||
}
|
||||
}, [stepsForm, myAboutRuleForm, myDefineRuleForm, myScheduleRuleForm, selectedTab, ruleId]);
|
||||
}, [
|
||||
stepsForm,
|
||||
myAboutRuleForm,
|
||||
myDefineRuleForm,
|
||||
myScheduleRuleForm,
|
||||
myActionsRuleForm,
|
||||
selectedTab,
|
||||
ruleId,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (rule != null) {
|
||||
const { aboutRuleData, defineRuleData, scheduleRuleData } = getStepsData({ rule });
|
||||
const { aboutRuleData, defineRuleData, scheduleRuleData, ruleActionsData } = getStepsData({
|
||||
rule,
|
||||
});
|
||||
setMyAboutRuleForm({ data: aboutRuleData, isValid: true });
|
||||
setMyDefineRuleForm({ data: defineRuleData, isValid: true });
|
||||
setMyScheduleRuleForm({ data: scheduleRuleData, isValid: true });
|
||||
setMyActionsRuleForm({ data: ruleActionsData, isValid: true });
|
||||
}
|
||||
}, [rule]);
|
||||
|
||||
|
@ -228,6 +287,7 @@ const EditRulePageComponent: FC = () => {
|
|||
if (selectedTab != null) {
|
||||
const ruleStep = selectedTab.id as RuleStep;
|
||||
const respForm = await stepsForm.current[ruleStep]?.submit();
|
||||
|
||||
if (respForm != null) {
|
||||
if (ruleStep === RuleStep.aboutRule) {
|
||||
setMyAboutRuleForm({
|
||||
|
@ -244,6 +304,11 @@ const EditRulePageComponent: FC = () => {
|
|||
data: respForm.data as ScheduleStepRule,
|
||||
isValid: respForm.isValid,
|
||||
});
|
||||
} else if (ruleStep === RuleStep.ruleActions) {
|
||||
setMyActionsRuleForm({
|
||||
data: respForm.data as ActionsStepRule,
|
||||
isValid: respForm.isValid,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -255,10 +320,13 @@ const EditRulePageComponent: FC = () => {
|
|||
|
||||
useEffect(() => {
|
||||
if (rule != null) {
|
||||
const { aboutRuleData, defineRuleData, scheduleRuleData } = getStepsData({ rule });
|
||||
const { aboutRuleData, defineRuleData, scheduleRuleData, ruleActionsData } = getStepsData({
|
||||
rule,
|
||||
});
|
||||
setMyAboutRuleForm({ data: aboutRuleData, isValid: true });
|
||||
setMyDefineRuleForm({ data: defineRuleData, isValid: true });
|
||||
setMyScheduleRuleForm({ data: scheduleRuleData, isValid: true });
|
||||
setMyActionsRuleForm({ data: ruleActionsData, isValid: true });
|
||||
}
|
||||
}, [rule]);
|
||||
|
||||
|
@ -303,6 +371,8 @@ const EditRulePageComponent: FC = () => {
|
|||
return ruleI18n.DEFINITION;
|
||||
} else if (t === RuleStep.scheduleRule) {
|
||||
return ruleI18n.SCHEDULE;
|
||||
} else if (t === RuleStep.ruleActions) {
|
||||
return ruleI18n.RULE_ACTIONS;
|
||||
}
|
||||
return t;
|
||||
})
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
getScheduleStepsData,
|
||||
getStepsData,
|
||||
getAboutStepsData,
|
||||
getActionsStepsData,
|
||||
getHumanizedDuration,
|
||||
getModifiedAboutDetailsData,
|
||||
determineDetailsValue,
|
||||
|
@ -17,16 +18,23 @@ import {
|
|||
import { mockRuleWithEverything, mockRule } from './all/__mocks__/mock';
|
||||
import { esFilters } from '../../../../../../../../src/plugins/data/public';
|
||||
import { Rule } from '../../../containers/detection_engine/rules';
|
||||
import { AboutStepRule, AboutStepRuleDetails, DefineStepRule, ScheduleStepRule } from './types';
|
||||
import {
|
||||
AboutStepRule,
|
||||
AboutStepRuleDetails,
|
||||
DefineStepRule,
|
||||
ScheduleStepRule,
|
||||
ActionsStepRule,
|
||||
} from './types';
|
||||
|
||||
describe('rule helpers', () => {
|
||||
describe('getStepsData', () => {
|
||||
test('returns object with about, define, and schedule step properties formatted', () => {
|
||||
test('returns object with about, define, schedule and actions step properties formatted', () => {
|
||||
const {
|
||||
defineRuleData,
|
||||
modifiedAboutRuleDetailsData,
|
||||
aboutRuleData,
|
||||
scheduleRuleData,
|
||||
ruleActionsData,
|
||||
}: GetStepsData = getStepsData({
|
||||
rule: mockRuleWithEverything('test-id'),
|
||||
});
|
||||
|
@ -98,7 +106,8 @@ describe('rule helpers', () => {
|
|||
},
|
||||
],
|
||||
};
|
||||
const scheduleRuleStepData = { enabled: true, from: '0s', interval: '5m', isNew: false };
|
||||
const scheduleRuleStepData = { from: '0s', interval: '5m', isNew: false };
|
||||
const ruleActionsStepData = { enabled: true, throttle: undefined, isNew: false, actions: [] };
|
||||
const aboutRuleDataDetailsData = {
|
||||
note: '# this is some markdown documentation',
|
||||
description: '24/7',
|
||||
|
@ -107,6 +116,7 @@ describe('rule helpers', () => {
|
|||
expect(defineRuleData).toEqual(defineRuleStepData);
|
||||
expect(aboutRuleData).toEqual(aboutRuleStepData);
|
||||
expect(scheduleRuleData).toEqual(scheduleRuleStepData);
|
||||
expect(ruleActionsData).toEqual(ruleActionsStepData);
|
||||
expect(modifiedAboutRuleDetailsData).toEqual(aboutRuleDataDetailsData);
|
||||
});
|
||||
});
|
||||
|
@ -274,7 +284,6 @@ describe('rule helpers', () => {
|
|||
const result: ScheduleStepRule = getScheduleStepsData(mockedRule);
|
||||
const expected = {
|
||||
isNew: false,
|
||||
enabled: mockedRule.enabled,
|
||||
interval: mockedRule.interval,
|
||||
from: '0s',
|
||||
};
|
||||
|
@ -283,6 +292,24 @@ describe('rule helpers', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('getActionsStepsData', () => {
|
||||
test('returns expected ActionsStepRule rule object', () => {
|
||||
const mockedRule = {
|
||||
...mockRule('test-id'),
|
||||
actions: [],
|
||||
};
|
||||
const result: ActionsStepRule = getActionsStepsData(mockedRule);
|
||||
const expected = {
|
||||
actions: [],
|
||||
enabled: mockedRule.enabled,
|
||||
isNew: false,
|
||||
throttle: undefined,
|
||||
};
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getModifiedAboutDetailsData', () => {
|
||||
test('returns object with "note" and "description" being those of passed in rule', () => {
|
||||
const result: AboutStepRuleDetails = getModifiedAboutDetailsData(
|
||||
|
|
|
@ -7,8 +7,11 @@
|
|||
import dateMath from '@elastic/datemath';
|
||||
import { get } from 'lodash/fp';
|
||||
import moment from 'moment';
|
||||
import memoizeOne from 'memoize-one';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { RuleAlertAction } from '../../../../common/detection_engine/types';
|
||||
import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions';
|
||||
import { Filter } from '../../../../../../../../src/plugins/data/public';
|
||||
import { Rule, RuleType } from '../../../containers/detection_engine/rules';
|
||||
import { FormData, FormHook, FormSchema } from '../../../shared_imports';
|
||||
|
@ -18,6 +21,7 @@ import {
|
|||
DefineStepRule,
|
||||
IMitreEnterpriseAttack,
|
||||
ScheduleStepRule,
|
||||
ActionsStepRule,
|
||||
} from './types';
|
||||
|
||||
export interface GetStepsData {
|
||||
|
@ -25,6 +29,7 @@ export interface GetStepsData {
|
|||
modifiedAboutRuleDetailsData: AboutStepRuleDetails;
|
||||
defineRuleData: DefineStepRule;
|
||||
scheduleRuleData: ScheduleStepRule;
|
||||
ruleActionsData: ActionsStepRule;
|
||||
}
|
||||
|
||||
export const getStepsData = ({
|
||||
|
@ -38,8 +43,29 @@ export const getStepsData = ({
|
|||
const aboutRuleData: AboutStepRule = getAboutStepsData(rule, detailsView);
|
||||
const modifiedAboutRuleDetailsData: AboutStepRuleDetails = getModifiedAboutDetailsData(rule);
|
||||
const scheduleRuleData: ScheduleStepRule = getScheduleStepsData(rule);
|
||||
const ruleActionsData: ActionsStepRule = getActionsStepsData(rule);
|
||||
|
||||
return { aboutRuleData, modifiedAboutRuleDetailsData, defineRuleData, scheduleRuleData };
|
||||
return {
|
||||
aboutRuleData,
|
||||
modifiedAboutRuleDetailsData,
|
||||
defineRuleData,
|
||||
scheduleRuleData,
|
||||
ruleActionsData,
|
||||
};
|
||||
};
|
||||
|
||||
export const getActionsStepsData = (
|
||||
rule: Omit<Rule, 'actions'> & { actions: RuleAlertAction[] }
|
||||
): ActionsStepRule => {
|
||||
const { enabled, actions = [], meta } = rule;
|
||||
|
||||
return {
|
||||
actions: actions?.map(transformRuleToAlertAction),
|
||||
isNew: false,
|
||||
throttle: meta?.throttle,
|
||||
kibanaSiemAppUrl: meta?.kibanaSiemAppUrl,
|
||||
enabled,
|
||||
};
|
||||
};
|
||||
|
||||
export const getDefineStepsData = (rule: Rule): DefineStepRule => ({
|
||||
|
@ -60,12 +86,11 @@ export const getDefineStepsData = (rule: Rule): DefineStepRule => ({
|
|||
});
|
||||
|
||||
export const getScheduleStepsData = (rule: Rule): ScheduleStepRule => {
|
||||
const { enabled, interval, from } = rule;
|
||||
const { interval, from } = rule;
|
||||
const fromHumanizedValue = getHumanizedDuration(from, interval);
|
||||
|
||||
return {
|
||||
isNew: false,
|
||||
enabled,
|
||||
interval,
|
||||
from: fromHumanizedValue,
|
||||
};
|
||||
|
@ -200,3 +225,46 @@ export const redirectToDetections = (
|
|||
isAuthenticated != null &&
|
||||
hasEncryptionKey != null &&
|
||||
(!isSignalIndexExists || !isAuthenticated || !hasEncryptionKey);
|
||||
|
||||
export const getActionMessageRuleParams = (ruleType: RuleType): string[] => {
|
||||
const commonRuleParamsKeys = [
|
||||
'id',
|
||||
'name',
|
||||
'description',
|
||||
'false_positives',
|
||||
'rule_id',
|
||||
'max_signals',
|
||||
'risk_score',
|
||||
'output_index',
|
||||
'references',
|
||||
'severity',
|
||||
'timeline_id',
|
||||
'timeline_title',
|
||||
'threat',
|
||||
'type',
|
||||
'version',
|
||||
// 'lists',
|
||||
];
|
||||
|
||||
const ruleParamsKeys = [
|
||||
...commonRuleParamsKeys,
|
||||
...(isMlRule(ruleType)
|
||||
? ['anomaly_threshold', 'machine_learning_job_id']
|
||||
: ['index', 'filters', 'language', 'query', 'saved_id']),
|
||||
].sort();
|
||||
|
||||
return ruleParamsKeys;
|
||||
};
|
||||
|
||||
export const getActionMessageParams = memoizeOne((ruleType: RuleType | undefined): string[] => {
|
||||
if (!ruleType) {
|
||||
return [];
|
||||
}
|
||||
const actionMessageRuleParams = getActionMessageRuleParams(ruleType);
|
||||
|
||||
return [
|
||||
'state.signals_count',
|
||||
'{context.results_link}',
|
||||
...actionMessageRuleParams.map(param => `context.rule.${param}`),
|
||||
];
|
||||
});
|
||||
|
|
|
@ -306,6 +306,10 @@ export const SCHEDULE_RULE = i18n.translate('xpack.siem.detectionEngine.rules.sc
|
|||
defaultMessage: 'Schedule rule',
|
||||
});
|
||||
|
||||
export const RULE_ACTIONS = i18n.translate('xpack.siem.detectionEngine.rules.ruleActionsTitle', {
|
||||
defaultMessage: 'Rule actions',
|
||||
});
|
||||
|
||||
export const DEFINITION = i18n.translate('xpack.siem.detectionEngine.rules.stepDefinitionTitle', {
|
||||
defaultMessage: 'Definition',
|
||||
});
|
||||
|
@ -318,6 +322,10 @@ export const SCHEDULE = i18n.translate('xpack.siem.detectionEngine.rules.stepSch
|
|||
defaultMessage: 'Schedule',
|
||||
});
|
||||
|
||||
export const ACTIONS = i18n.translate('xpack.siem.detectionEngine.rules.stepActionsTitle', {
|
||||
defaultMessage: 'Actions',
|
||||
});
|
||||
|
||||
export const OPTIONAL_FIELD = i18n.translate(
|
||||
'xpack.siem.detectionEngine.rules.optionalFieldDescription',
|
||||
{
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { AlertAction } from '../../../../../../../plugins/alerting/common';
|
||||
import { RuleAlertAction } from '../../../../common/detection_engine/types';
|
||||
import { Filter } from '../../../../../../../../src/plugins/data/common';
|
||||
import { RuleType } from '../../../containers/detection_engine/rules/types';
|
||||
import { FieldValueQueryBar } from './components/query_bar';
|
||||
|
@ -27,6 +29,7 @@ export enum RuleStep {
|
|||
defineRule = 'define-rule',
|
||||
aboutRule = 'about-rule',
|
||||
scheduleRule = 'schedule-rule',
|
||||
ruleActions = 'rule-actions',
|
||||
}
|
||||
export type RuleStatusType = 'passive' | 'active' | 'valid';
|
||||
|
||||
|
@ -76,12 +79,18 @@ export interface DefineStepRule extends StepRuleData {
|
|||
}
|
||||
|
||||
export interface ScheduleStepRule extends StepRuleData {
|
||||
enabled: boolean;
|
||||
interval: string;
|
||||
from: string;
|
||||
to?: string;
|
||||
}
|
||||
|
||||
export interface ActionsStepRule extends StepRuleData {
|
||||
actions: AlertAction[];
|
||||
enabled: boolean;
|
||||
kibanaSiemAppUrl?: string;
|
||||
throttle?: string | null;
|
||||
}
|
||||
|
||||
export interface DefineStepRuleJson {
|
||||
anomaly_threshold?: number;
|
||||
index?: string[];
|
||||
|
@ -108,16 +117,18 @@ export interface AboutStepRuleJson {
|
|||
}
|
||||
|
||||
export interface ScheduleStepRuleJson {
|
||||
enabled: boolean;
|
||||
interval: string;
|
||||
from: string;
|
||||
to?: string;
|
||||
meta?: unknown;
|
||||
}
|
||||
|
||||
export type MyRule = Omit<DefineStepRule & ScheduleStepRule & AboutStepRule, 'isNew'> & {
|
||||
immutable: boolean;
|
||||
};
|
||||
export interface ActionsStepRuleJson {
|
||||
actions: RuleAlertAction[];
|
||||
enabled: boolean;
|
||||
throttle?: string | null;
|
||||
meta?: unknown;
|
||||
}
|
||||
|
||||
export interface IMitreAttack {
|
||||
id: string;
|
||||
|
|
|
@ -18,6 +18,9 @@ export {
|
|||
useForm,
|
||||
ValidationFunc,
|
||||
} from '../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib';
|
||||
export { Field } from '../../../../../src/plugins/es_ui_shared/static/forms/components';
|
||||
export {
|
||||
Field,
|
||||
SelectField,
|
||||
} from '../../../../../src/plugins/es_ui_shared/static/forms/components';
|
||||
export { fieldValidators } from '../../../../../src/plugins/es_ui_shared/static/forms/helpers';
|
||||
export { ERROR_CODE } from '../../../../../src/plugins/es_ui_shared/static/forms/helpers/field_validators/types';
|
||||
|
|
|
@ -39,7 +39,7 @@ describe('buildSignalsSearchQuery', () => {
|
|||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: from,
|
||||
gt: from,
|
||||
lte: to,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -30,7 +30,7 @@ export const buildSignalsSearchQuery = ({ ruleId, index, from, to }: BuildSignal
|
|||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: from,
|
||||
gt: from,
|
||||
lte: to,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -42,7 +42,7 @@ describe('createNotifications', () => {
|
|||
const action = {
|
||||
group: 'default',
|
||||
id: '99403909-ca9b-49ba-9d7a-7e5320e68d05',
|
||||
params: { message: 'Rule generated {{state.signalsCount}} signals' },
|
||||
params: { message: 'Rule generated {{state.signals_count}} signals' },
|
||||
action_type_id: '.slack',
|
||||
};
|
||||
await createNotifications({
|
||||
|
|
|
@ -21,7 +21,7 @@ interface GetSignalsCount {
|
|||
ruleAlertId: string;
|
||||
ruleId: string;
|
||||
index: string;
|
||||
kibanaUrl: string | undefined;
|
||||
kibanaSiemAppUrl: string | undefined;
|
||||
callCluster: NotificationExecutorOptions['services']['callCluster'];
|
||||
}
|
||||
|
||||
|
@ -32,7 +32,7 @@ export const getSignalsCount = async ({
|
|||
ruleId,
|
||||
index,
|
||||
callCluster,
|
||||
kibanaUrl = '',
|
||||
kibanaSiemAppUrl = '',
|
||||
}: GetSignalsCount): Promise<SignalsCountResults> => {
|
||||
const fromMoment = moment.isDate(from) ? moment(from) : parseScheduleDates(from);
|
||||
const toMoment = moment.isDate(to) ? moment(to) : parseScheduleDates(to);
|
||||
|
@ -53,7 +53,7 @@ export const getSignalsCount = async ({
|
|||
|
||||
const result = await callCluster('count', query);
|
||||
const resultsLink = getNotificationResultsLink({
|
||||
baseUrl: kibanaUrl,
|
||||
kibanaSiemAppUrl: `${kibanaSiemAppUrl}`,
|
||||
id: ruleAlertId,
|
||||
from: fromInMs,
|
||||
to: toInMs,
|
||||
|
|
|
@ -127,7 +127,7 @@ describe('rules_notification_alert_type', () => {
|
|||
|
||||
expect(alertInstanceFactoryMock).toHaveBeenCalled();
|
||||
expect(alertInstanceMock.replaceState).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ signalsCount: 10 })
|
||||
expect.objectContaining({ signals_count: 10 })
|
||||
);
|
||||
expect(alertInstanceMock.scheduleActions).toHaveBeenCalledWith(
|
||||
'default',
|
||||
|
|
|
@ -40,14 +40,14 @@ export const rulesNotificationAlertType = ({
|
|||
}
|
||||
|
||||
const { params: ruleAlertParams, name: ruleName } = ruleAlertSavedObject.attributes;
|
||||
const ruleParams = { ...ruleAlertParams, name: ruleName };
|
||||
const ruleParams = { ...ruleAlertParams, name: ruleName, id: ruleAlertSavedObject.id };
|
||||
|
||||
const { signalsCount, resultsLink } = await getSignalsCount({
|
||||
from: previousStartedAt ?? `now-${ruleParams.interval}`,
|
||||
to: startedAt,
|
||||
index: ruleParams.outputIndex,
|
||||
ruleId: ruleParams.ruleId!,
|
||||
kibanaUrl: ruleAlertParams.meta?.kibanaUrl as string,
|
||||
kibanaSiemAppUrl: ruleAlertParams.meta?.kibanaSiemAppUrl as string,
|
||||
ruleAlertId: ruleAlertSavedObject.id,
|
||||
callCluster: services.callCluster,
|
||||
});
|
||||
|
|
|
@ -4,11 +4,13 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { mapKeys, snakeCase } from 'lodash/fp';
|
||||
import { AlertInstance } from '../../../../../../../plugins/alerting/server';
|
||||
import { RuleTypeParams } from '../types';
|
||||
|
||||
type NotificationRuleTypeParams = RuleTypeParams & {
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
interface ScheduleNotificationActions {
|
||||
|
@ -26,9 +28,9 @@ export const scheduleNotificationActions = ({
|
|||
}: ScheduleNotificationActions): AlertInstance =>
|
||||
alertInstance
|
||||
.replaceState({
|
||||
signalsCount,
|
||||
signals_count: signalsCount,
|
||||
})
|
||||
.scheduleActions('default', {
|
||||
resultsLink,
|
||||
rule: ruleParams,
|
||||
results_link: resultsLink,
|
||||
rule: mapKeys(snakeCase, ruleParams),
|
||||
});
|
||||
|
|
|
@ -88,7 +88,7 @@ describe('updateNotifications', () => {
|
|||
const action = {
|
||||
group: 'default',
|
||||
id: '99403909-ca9b-49ba-9d7a-7e5320e68d05',
|
||||
params: { message: 'Rule generated {{state.signalsCount}} signals' },
|
||||
params: { message: 'Rule generated {{state.signals_count}} signals' },
|
||||
action_type_id: '.slack',
|
||||
};
|
||||
await updateNotifications({
|
||||
|
|
|
@ -9,7 +9,7 @@ import { getNotificationResultsLink } from './utils';
|
|||
describe('utils', () => {
|
||||
it('getNotificationResultsLink', () => {
|
||||
const resultLink = getNotificationResultsLink({
|
||||
baseUrl: 'http://localhost:5601',
|
||||
kibanaSiemAppUrl: 'http://localhost:5601/app/siem',
|
||||
id: 'notification-id',
|
||||
from: '00000',
|
||||
to: '1111',
|
||||
|
|
|
@ -5,14 +5,14 @@
|
|||
*/
|
||||
|
||||
export const getNotificationResultsLink = ({
|
||||
baseUrl,
|
||||
kibanaSiemAppUrl,
|
||||
id,
|
||||
from,
|
||||
to,
|
||||
}: {
|
||||
baseUrl: string;
|
||||
kibanaSiemAppUrl: string;
|
||||
id: string;
|
||||
from: string;
|
||||
to: string;
|
||||
}) =>
|
||||
`${baseUrl}/app/siem#/detections/rules/id/${id}?timerange=(global:(linkTo:!(timeline),timerange:(from:${from},kind:absolute,to:${to})),timeline:(linkTo:!(global),timerange:(from:${from},kind:absolute,to:${to})))`;
|
||||
`${kibanaSiemAppUrl}#/detections/rules/id/${id}?timerange=(global:(linkTo:!(timeline),timerange:(from:${from},kind:absolute,to:${to})),timeline:(linkTo:!(global),timerange:(from:${from},kind:absolute,to:${to})))`;
|
||||
|
|
|
@ -335,7 +335,7 @@ export const createRuleWithActionsRequest = () => {
|
|||
{
|
||||
group: 'default',
|
||||
id: '99403909-ca9b-49ba-9d7a-7e5320e68d05',
|
||||
params: { message: 'Rule generated {{state.signalsCount}} signals' },
|
||||
params: { message: 'Rule generated {{state.signals_count}} signals' },
|
||||
action_type_id: '.slack',
|
||||
},
|
||||
],
|
||||
|
@ -668,7 +668,8 @@ export const getNotificationResult = (): RuleNotificationAlertType => ({
|
|||
{
|
||||
actionTypeId: '.slack',
|
||||
params: {
|
||||
message: 'Rule generated {{state.signalsCount}} signals\n\n{{rule.name}}\n{{resultsLink}}',
|
||||
message:
|
||||
'Rule generated {{state.signals_count}} signals\n\n{{context.rule.name}}\n{{{context.results_link}}}',
|
||||
},
|
||||
group: 'default',
|
||||
id: '99403909-ca9b-49ba-9d7a-7e5320e68d05',
|
||||
|
|
|
@ -214,13 +214,14 @@ export const signalRulesAlertType = ({
|
|||
const notificationRuleParams = {
|
||||
...ruleParams,
|
||||
name,
|
||||
id: savedObject.id,
|
||||
};
|
||||
const { signalsCount, resultsLink } = await getSignalsCount({
|
||||
from: `now-${interval}`,
|
||||
to: 'now',
|
||||
index: ruleParams.outputIndex,
|
||||
ruleId: ruleParams.ruleId!,
|
||||
kibanaUrl: meta?.kibanaUrl as string,
|
||||
kibanaSiemAppUrl: meta.kibanaSiemAppUrl as string,
|
||||
ruleAlertId: savedObject.id,
|
||||
callCluster: services.callCluster,
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue