[Security Solution] Don't mute rules when bulk editing rule actions (#140626)

## Intro

This PR modifies the logic of bulk updating rule actions, in preparation for https://github.com/elastic/kibana/pull/137430

## Summary

- Removes the mute logic for bulk updating rule actions
- Remove option for “Perform no actions” from the bulk update rule actions dropdown options ONLY (option still available when creating or editing rules individually)
- Also corrects bulk update rule actions flyout, so that:
    - available actions are always displayed
    - copy referring to using "Perform No Actions" to mute all selected rules is no longer displayed.

## Screenshots

**Removed unwanted copy and "On each rule execution" selected as default**
![image](https://user-images.githubusercontent.com/5354282/191498419-10299ee5-4a9e-474e-b00a-657dc90816fa.png)

**"Perform No Action" option no longer available**
![image](https://user-images.githubusercontent.com/5354282/191498500-3965edad-8142-4834-808e-c210e72e17cb.png)


### Checklist

Delete any items that are not applicable to this PR.

- [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials
- [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios
- [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] 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](https://www.elastic.co/support/matrix#matrix_browsers)

### For maintainers

- [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
This commit is contained in:
Juan Pablo Djeredjian 2022-09-27 18:29:34 +02:00 committed by GitHub
parent bde62fd5d8
commit 7aa5428597
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 414 additions and 287 deletions

View file

@ -18,7 +18,6 @@ export * from './src/default_per_page';
export * from './src/default_risk_score_mapping_array';
export * from './src/default_severity_mapping_array';
export * from './src/default_threat_array';
export * from './src/default_throttle_null';
export * from './src/default_to_string';
export * from './src/default_uuid';
export * from './src/from';

View file

@ -1,44 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { Throttle } from '../throttle';
import { DefaultThrottleNull } from '.';
import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils';
describe('default_throttle_null', () => {
test('it should validate a throttle string', () => {
const payload: Throttle = 'some string';
const decoded = DefaultThrottleNull.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should not validate an array with a number', () => {
const payload = 5;
const decoded = DefaultThrottleNull.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "5" supplied to "DefaultThreatNull"',
]);
expect(message.schema).toEqual({});
});
test('it should return a default "null" if not provided a value', () => {
const payload = undefined;
const decoded = DefaultThrottleNull.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(null);
});
});

View file

@ -1,23 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
import { throttle, ThrottleOrNull } from '../throttle';
/**
* Types the DefaultThrottleNull as:
* - If null or undefined, then a null will be set
*/
export const DefaultThrottleNull = new t.Type<ThrottleOrNull, ThrottleOrNull | undefined, unknown>(
'DefaultThreatNull',
throttle.is,
(input, context): Either<t.Errors, ThrottleOrNull> =>
input == null ? t.success(null) : throttle.validate(input, context),
t.identity
);

View file

@ -6,13 +6,15 @@
* Side Public License, v 1.
*/
import { TimeDuration } from '@kbn/securitysolution-io-ts-types';
import * as t from 'io-ts';
export const throttle = t.string;
export const throttle = t.union([
t.literal('no_actions'),
t.literal('rule'),
TimeDuration({ allowedUnits: ['h', 'd'] }),
]);
export type Throttle = t.TypeOf<typeof throttle>;
export const throttleOrNull = t.union([throttle, t.null]);
export type ThrottleOrNull = t.TypeOf<typeof throttleOrNull>;
export const throttleOrNullOrUndefined = t.union([throttle, t.null, t.undefined]);
export type ThrottleOrUndefinedOrNull = t.TypeOf<typeof throttleOrNullOrUndefined>;

View file

@ -6,122 +6,315 @@
* Side Public License, v 1.
*/
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { TimeDuration } from '.';
import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils';
describe('TimeDuration', () => {
test('it should validate a correctly formed TimeDuration with time unit of seconds', () => {
const payload = '1s';
const decoded = TimeDuration.decode(payload);
const message = pipe(decoded, foldLeftRight);
describe('with allowedDurations', () => {
test('it should validate a correctly formed TimeDuration with an allowed duration of 1s', () => {
const payload = '1s';
const decoded = TimeDuration({
allowedDurations: [
[1, 's'],
[2, 'h'],
[7, 'd'],
],
}).decode(payload);
const message = foldLeftRight(decoded);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate a correctly formed TimeDuration with an allowed duration of 7d', () => {
const payload = '1s';
const decoded = TimeDuration({
allowedDurations: [
[1, 's'],
[2, 'h'],
[7, 'd'],
],
}).decode(payload);
const message = foldLeftRight(decoded);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should NOT validate a time duration if the allowed durations does not include it', () => {
const payload = '24h';
const decoded = TimeDuration({
allowedDurations: [
[1, 's'],
[2, 'h'],
[7, 'd'],
],
}).decode(payload);
const message = foldLeftRight(decoded);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "24h" supplied to "TimeDuration"',
]);
expect(message.schema).toEqual({});
});
test('it should NOT validate a an allowed duration with a negative number', () => {
const payload = '10s';
const decoded = TimeDuration({
allowedDurations: [
[1, 's'],
[-7, 'd'],
],
}).decode(payload);
const message = foldLeftRight(decoded);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "[[1,"s"],[-7,"d"]]" supplied to "TimeDuration"',
]);
expect(message.schema).toEqual({});
});
test('it should NOT validate an allowed duration with a fractional number', () => {
const payload = '1.5s';
const decoded = TimeDuration({
allowedDurations: [
[1, 's'],
[-7, 'd'],
],
}).decode(payload);
const message = foldLeftRight(decoded);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "1.5s" supplied to "TimeDuration"',
]);
expect(message.schema).toEqual({});
});
test('it should NOT validate a an allowed duration with a duration of 0', () => {
const payload = '10s';
const decoded = TimeDuration({
allowedDurations: [
[0, 's'],
[7, 'd'],
],
}).decode(payload);
const message = foldLeftRight(decoded);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "[[0,"s"],[7,"d"]]" supplied to "TimeDuration"',
]);
expect(message.schema).toEqual({});
});
test('it should NOT validate a TimeDuration with an invalid time unit', () => {
const payload = '10000000days';
const decoded = TimeDuration({
allowedDurations: [
[1, 'h'],
[1, 'd'],
[7, 'd'],
],
}).decode(payload);
const message = foldLeftRight(decoded);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "10000000days" supplied to "TimeDuration"',
]);
expect(message.schema).toEqual({});
});
test('it should NOT validate a TimeDuration with a time interval with incorrect format', () => {
const payload = '100ff0000w';
const decoded = TimeDuration({
allowedDurations: [
[1, 'h'],
[1, 'd'],
[7, 'd'],
],
}).decode(payload);
const message = foldLeftRight(decoded);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "100ff0000w" supplied to "TimeDuration"',
]);
expect(message.schema).toEqual({});
});
test('it should NOT validate an empty string', () => {
const payload = '';
const decoded = TimeDuration({
allowedDurations: [
[1, 'h'],
[1, 'd'],
[7, 'd'],
],
}).decode(payload);
const message = foldLeftRight(decoded);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "" supplied to "TimeDuration"',
]);
expect(message.schema).toEqual({});
});
test('it should NOT validate an number', () => {
const payload = 100;
const decoded = TimeDuration({
allowedDurations: [
[1, 'h'],
[1, 'd'],
[7, 'd'],
],
}).decode(payload);
const message = foldLeftRight(decoded);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "100" supplied to "TimeDuration"',
]);
expect(message.schema).toEqual({});
});
test('it should NOT validate an TimeDuration with a valid time unit but unsafe integer', () => {
const payload = `${Math.pow(2, 53)}h`;
const decoded = TimeDuration({
allowedDurations: [
[1, 'h'],
[1, 'd'],
[7, 'd'],
],
}).decode(payload);
const message = foldLeftRight(decoded);
expect(getPaths(left(message.errors))).toEqual([
`Invalid value "${Math.pow(2, 53)}h" supplied to "TimeDuration"`,
]);
expect(message.schema).toEqual({});
});
});
describe('with allowedUnits', () => {
test('it should validate a correctly formed TimeDuration with time unit of seconds', () => {
const payload = '1s';
const decoded = TimeDuration({ allowedUnits: ['s'] }).decode(payload);
const message = foldLeftRight(decoded);
test('it should validate a correctly formed TimeDuration with time unit of minutes', () => {
const payload = '100m';
const decoded = TimeDuration.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate a correctly formed TimeDuration with time unit of minutes', () => {
const payload = '100m';
const decoded = TimeDuration({ allowedUnits: ['s', 'm'] }).decode(payload);
const message = foldLeftRight(decoded);
test('it should validate a correctly formed TimeDuration with time unit of hours', () => {
const payload = '10000000h';
const decoded = TimeDuration.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate a correctly formed TimeDuration with time unit of hours', () => {
const payload = '10000000h';
const decoded = TimeDuration({ allowedUnits: ['s', 'm', 'h'] }).decode(payload);
const message = foldLeftRight(decoded);
test('it should NOT validate a TimeDuration of 0 length', () => {
const payload = '0s';
const decoded = TimeDuration.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "0s" supplied to "TimeDuration"',
]);
expect(message.schema).toEqual({});
});
test('it should validate a correctly formed TimeDuration with time unit of days', () => {
const payload = '7d';
const decoded = TimeDuration({ allowedUnits: ['s', 'm', 'h', 'd'] }).decode(payload);
const message = foldLeftRight(decoded);
test('it should NOT validate a negative TimeDuration', () => {
const payload = '-10s';
const decoded = TimeDuration.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "-10s" supplied to "TimeDuration"',
]);
expect(message.schema).toEqual({});
});
test('it should NOT validate a correctly formed TimeDuration with time unit of seconds if it is not an allowed unit', () => {
const payload = '30s';
const decoded = TimeDuration({ allowedUnits: ['m', 'h', 'd'] }).decode(payload);
const message = foldLeftRight(decoded);
test('it should NOT validate a decimal TimeDuration', () => {
const payload = '1.5s';
const decoded = TimeDuration.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "30s" supplied to "TimeDuration"',
]);
expect(message.schema).toEqual({});
});
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "1.5s" supplied to "TimeDuration"',
]);
expect(message.schema).toEqual({});
});
test('it should NOT validate a negative TimeDuration', () => {
const payload = '-10s';
const decoded = TimeDuration({ allowedUnits: ['s', 'm', 'h', 'd'] }).decode(payload);
const message = foldLeftRight(decoded);
test('it should NOT validate a TimeDuration with some other time unit', () => {
const payload = '10000000w';
const decoded = TimeDuration.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "-10s" supplied to "TimeDuration"',
]);
expect(message.schema).toEqual({});
});
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "10000000w" supplied to "TimeDuration"',
]);
expect(message.schema).toEqual({});
});
test('it should NOT validate a fractional number', () => {
const payload = '1.5s';
const decoded = TimeDuration({ allowedUnits: ['s', 'm', 'h', 'd'] }).decode(payload);
const message = foldLeftRight(decoded);
test('it should NOT validate a TimeDuration with a time interval with incorrect format', () => {
const payload = '100ff0000w';
const decoded = TimeDuration.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "1.5s" supplied to "TimeDuration"',
]);
expect(message.schema).toEqual({});
});
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "100ff0000w" supplied to "TimeDuration"',
]);
expect(message.schema).toEqual({});
});
test('it should NOT validate a TimeDuration with an invalid time unit', () => {
const payload = '10000000days';
const decoded = TimeDuration({ allowedUnits: ['s', 'm', 'h', 'd'] }).decode(payload);
const message = foldLeftRight(decoded);
test('it should NOT validate an empty string', () => {
const payload = '';
const decoded = TimeDuration.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "10000000days" supplied to "TimeDuration"',
]);
expect(message.schema).toEqual({});
});
expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "TimeDuration"']);
expect(message.schema).toEqual({});
});
test('it should NOT validate a TimeDuration with a time interval with incorrect format', () => {
const payload = '100ff0000w';
const decoded = TimeDuration({ allowedUnits: ['s', 'm', 'h'] }).decode(payload);
const message = foldLeftRight(decoded);
test('it should NOT validate an number', () => {
const payload = 100;
const decoded = TimeDuration.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "100ff0000w" supplied to "TimeDuration"',
]);
expect(message.schema).toEqual({});
});
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "100" supplied to "TimeDuration"',
]);
expect(message.schema).toEqual({});
});
test('it should NOT validate an empty string', () => {
const payload = '';
const decoded = TimeDuration({ allowedUnits: ['s', 'm', 'h'] }).decode(payload);
const message = foldLeftRight(decoded);
test('it should NOT validate an TimeDuration with a valid time unit but unsafe integer', () => {
const payload = `${Math.pow(2, 53)}h`;
const decoded = TimeDuration.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "" supplied to "TimeDuration"',
]);
expect(message.schema).toEqual({});
});
expect(getPaths(left(message.errors))).toEqual([
`Invalid value "${Math.pow(2, 53)}h" supplied to "TimeDuration"`,
]);
expect(message.schema).toEqual({});
test('it should NOT validate an number', () => {
const payload = 100;
const decoded = TimeDuration({ allowedUnits: ['s', 'm', 'h'] }).decode(payload);
const message = foldLeftRight(decoded);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "100" supplied to "TimeDuration"',
]);
expect(message.schema).toEqual({});
});
test('it should NOT validate an TimeDuration with a valid time unit but unsafe integer', () => {
const payload = `${Math.pow(2, 53)}h`;
const decoded = TimeDuration({ allowedUnits: ['s', 'm', 'h'] }).decode(payload);
const message = foldLeftRight(decoded);
expect(getPaths(left(message.errors))).toEqual([
`Invalid value "${Math.pow(2, 53)}h" supplied to "TimeDuration"`,
]);
expect(message.schema).toEqual({});
});
});
});

View file

@ -12,38 +12,60 @@ import { Either } from 'fp-ts/lib/Either';
/**
* Types the TimeDuration as:
* - A string that is not empty, and composed of a positive integer greater than 0 followed by a unit of time
* - in the format {safe_integer}{timeUnit}, e.g. "30s", "1m", "2h"
* - in the format {safe_integer}{timeUnit}, e.g. "30s", "1m", "2h", "7d"
*/
export const TimeDuration = new t.Type<string, string, unknown>(
'TimeDuration',
t.string.is,
(input, context): Either<t.Errors, string> => {
if (typeof input === 'string' && input.trim() !== '') {
try {
const inputLength = input.length;
const unit = input.trim().at(-1);
const time = parseFloat(input.trim().substring(0, inputLength - 1));
if (!Number.isInteger(time)) {
type TimeUnits = 's' | 'm' | 'h' | 'd' | 'w' | 'y';
interface TimeDurationWithAllowedDurations {
allowedDurations: Array<[number, TimeUnits]>;
allowedUnits?: never;
}
interface TimeDurationWithAllowedUnits {
allowedUnits: TimeUnits[];
allowedDurations?: never;
}
type TimeDurationType = TimeDurationWithAllowedDurations | TimeDurationWithAllowedUnits;
const isTimeSafe = (time: number) => time >= 1 && Number.isSafeInteger(time);
export const TimeDuration = ({ allowedUnits, allowedDurations }: TimeDurationType) => {
return new t.Type<string, string, unknown>(
'TimeDuration',
t.string.is,
(input, context): Either<t.Errors, string> => {
if (typeof input === 'string' && input.trim() !== '') {
try {
const inputLength = input.length;
const time = Number(input.trim().substring(0, inputLength - 1));
const unit = input.trim().at(-1);
if (!isTimeSafe(time)) {
return t.failure(input, context);
}
if (allowedDurations) {
for (const [allowedTime, allowedUnit] of allowedDurations) {
if (!isTimeSafe(allowedTime)) {
return t.failure(allowedDurations, context);
}
if (allowedTime === time && allowedUnit === unit) {
return t.success(input);
}
}
return t.failure(input, context);
} else if (allowedUnits.includes(unit as TimeUnits)) {
return t.success(input);
} else {
return t.failure(input, context);
}
} catch (error) {
return t.failure(input, context);
}
if (
time >= 1 &&
Number.isSafeInteger(time) &&
(unit === 's' || unit === 'm' || unit === 'h')
) {
return t.success(input);
} else {
return t.failure(input, context);
}
} catch (error) {
} else {
return t.failure(input, context);
}
} else {
return t.failure(input, context);
}
},
t.identity
);
},
t.identity
);
};
export type TimeDurationC = typeof TimeDuration;

View file

@ -9,7 +9,6 @@ import * as t from 'io-ts';
import { NonEmptyArray, TimeDuration, enumeration } from '@kbn/securitysolution-io-ts-types';
import {
throttle,
action_group as actionGroup,
action_params as actionParams,
action_id as actionId,
@ -41,6 +40,18 @@ export enum BulkActionEditType {
'set_schedule' = 'set_schedule',
}
export const throttleForBulkActions = t.union([
t.literal('rule'),
TimeDuration({
allowedDurations: [
[1, 'h'],
[1, 'd'],
[7, 'd'],
],
}),
]);
export type ThrottleForBulkActions = t.TypeOf<typeof throttleForBulkActions>;
const bulkActionEditPayloadTags = t.type({
type: t.union([
t.literal(BulkActionEditType.add_tags),
@ -96,7 +107,7 @@ const bulkActionEditPayloadRuleActions = t.type({
t.literal(BulkActionEditType.set_rule_actions),
]),
value: t.type({
throttle,
throttle: throttleForBulkActions,
actions: t.array(normalizedRuleAction),
}),
});
@ -106,8 +117,8 @@ export type BulkActionEditPayloadRuleActions = t.TypeOf<typeof bulkActionEditPay
const bulkActionEditPayloadSchedule = t.type({
type: t.literal(BulkActionEditType.set_schedule),
value: t.type({
interval: TimeDuration,
lookback: TimeDuration,
interval: TimeDuration({ allowedUnits: ['s', 'm', 'h'] }),
lookback: TimeDuration({ allowedUnits: ['s', 'm', 'h'] }),
}),
});
export type BulkActionEditPayloadSchedule = t.TypeOf<typeof bulkActionEditPayloadSchedule>;

View file

@ -6,10 +6,13 @@
*/
import { find } from 'lodash/fp';
import { THROTTLE_OPTIONS, DEFAULT_THROTTLE_OPTION } from '../throttle_select_field';
import {
THROTTLE_OPTIONS_FOR_RULE_CREATION_AND_EDITING,
DEFAULT_THROTTLE_OPTION,
} from '../throttle_select_field';
export const buildThrottleDescription = (value = DEFAULT_THROTTLE_OPTION.value, title: string) => {
const throttleOption = find(['value', value], THROTTLE_OPTIONS);
const throttleOption = find(['value', value], THROTTLE_OPTIONS_FOR_RULE_CREATION_AND_EDITING);
return {
title,

View file

@ -33,7 +33,7 @@ import { Form, UseField, useForm, useFormData } from '../../../../shared_imports
import { StepContentWrapper } from '../step_content_wrapper';
import {
ThrottleSelectField,
THROTTLE_OPTIONS,
THROTTLE_OPTIONS_FOR_RULE_CREATION_AND_EDITING,
DEFAULT_THROTTLE_OPTION,
} from '../throttle_select_field';
import { RuleActionsField } from '../rule_actions_field';
@ -62,11 +62,14 @@ 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 }];
if (
throttle &&
findIndex(['value', throttle], THROTTLE_OPTIONS_FOR_RULE_CREATION_AND_EDITING) < 0
) {
return [...THROTTLE_OPTIONS_FOR_RULE_CREATION_AND_EDITING, { value: throttle, text: throttle }];
}
return THROTTLE_OPTIONS;
return THROTTLE_OPTIONS_FOR_RULE_CREATION_AND_EDITING;
};
const DisplayActionsHeader = () => {

View file

@ -13,15 +13,19 @@ import {
} from '../../../../../common/constants';
import { SelectField } from '../../../../shared_imports';
export const THROTTLE_OPTIONS = [
{ value: NOTIFICATION_THROTTLE_NO_ACTIONS, text: 'Perform no actions' },
export const THROTTLE_OPTIONS_FOR_BULK_RULE_ACTIONS = [
{ value: NOTIFICATION_THROTTLE_RULE, text: 'On each rule execution' },
{ value: '1h', text: 'Hourly' },
{ value: '1d', text: 'Daily' },
{ value: '7d', text: 'Weekly' },
];
export const DEFAULT_THROTTLE_OPTION = THROTTLE_OPTIONS[0];
export const THROTTLE_OPTIONS_FOR_RULE_CREATION_AND_EDITING = [
{ value: NOTIFICATION_THROTTLE_NO_ACTIONS, text: 'Perform no actions' },
...THROTTLE_OPTIONS_FOR_BULK_RULE_ACTIONS,
];
export const DEFAULT_THROTTLE_OPTION = THROTTLE_OPTIONS_FOR_RULE_CREATION_AND_EDITING[0];
type ThrottleSelectField = typeof SelectField;

View file

@ -12,6 +12,7 @@ import type {
RuleAction,
ActionTypeRegistryContract,
} from '@kbn/triggers-actions-ui-plugin/public';
import type { FormSchema } from '../../../../../../../shared_imports';
import {
useForm,
@ -21,9 +22,12 @@ import {
getUseField,
Field,
} from '../../../../../../../shared_imports';
import type { BulkActionEditPayload } from '../../../../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema';
import { BulkActionEditType } from '../../../../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema';
import { NOTIFICATION_THROTTLE_NO_ACTIONS } from '../../../../../../../../common/constants';
import type {
BulkActionEditPayload,
ThrottleForBulkActions,
} from '../../../../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema';
import { NOTIFICATION_THROTTLE_RULE } from '../../../../../../../../common/constants';
import { BulkEditFormWrapper } from './bulk_edit_form_wrapper';
import { bulkAddRuleActions as i18n } from '../translations';
@ -32,7 +36,7 @@ import { useKibana } from '../../../../../../../common/lib/kibana';
import {
ThrottleSelectField,
THROTTLE_OPTIONS,
THROTTLE_OPTIONS_FOR_BULK_RULE_ACTIONS,
} from '../../../../../../components/rules/throttle_select_field';
import { getAllActionMessageParams } from '../../../helpers';
@ -42,7 +46,7 @@ import { validateRuleActionsField } from '../../../../../../containers/detection
const CommonUseField = getUseField({ component: Field });
export interface RuleActionsFormData {
throttle: string;
throttle: ThrottleForBulkActions;
actions: RuleAction[];
overwrite: boolean;
}
@ -68,7 +72,7 @@ const getFormSchema = (
});
const defaultFormData: RuleActionsFormData = {
throttle: NOTIFICATION_THROTTLE_NO_ACTIONS,
throttle: NOTIFICATION_THROTTLE_RULE,
actions: [],
overwrite: false,
};
@ -93,7 +97,7 @@ const RuleActionsFormComponent = ({ rulesCount, onClose, onConfirm }: RuleAction
defaultValue: defaultFormData,
});
const [{ overwrite, throttle }] = useFormData({ form, watch: ['overwrite', 'throttle'] });
const [{ overwrite }] = useFormData({ form, watch: ['overwrite', 'throttle'] });
const handleSubmit = useCallback(async () => {
const { data, isValid } = await form.submit();
@ -121,7 +125,7 @@ const RuleActionsFormComponent = ({ rulesCount, onClose, onConfirm }: RuleAction
dataTestSubj: 'bulkEditRulesRuleActionThrottle',
hasNoInitialSelection: false,
euiFieldProps: {
options: THROTTLE_OPTIONS,
options: THROTTLE_OPTIONS_FOR_BULK_RULE_ACTIONS,
},
}),
[]
@ -129,8 +133,6 @@ const RuleActionsFormComponent = ({ rulesCount, onClose, onConfirm }: RuleAction
const messageVariables = useMemo(() => getAllActionMessageParams(), []);
const showActionsSelect = throttle !== NOTIFICATION_THROTTLE_NO_ACTIONS;
return (
<BulkEditFormWrapper
form={form}
@ -157,30 +159,6 @@ const RuleActionsFormComponent = ({ rulesCount, onClose, onConfirm }: RuleAction
defaultMessage="The actions frequency you select below is applied to all actions (both new and existing) for all selected rules."
/>
</li>
<li>
<FormattedMessage
id="xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.deleteActionsDetail"
defaultMessage="To delete actions for all selected rules, select {noActionsOption} in the menu and check {overwriteActionsCheckbox}."
values={{
noActionsOption: (
<strong>
<FormattedMessage
id="xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.deleteActionsDetail.menuOptionLabel"
defaultMessage="Perform no actions"
/>
</strong>
),
overwriteActionsCheckbox: (
<strong>
<FormattedMessage
id="xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.deleteActionsDetail.overwriteActionsCheckboxLabel"
defaultMessage="Overwrite all selected rule actions"
/>
</strong>
),
}}
/>
</li>
<li>{i18n.RULE_VARIABLES_DETAIL}</li>
</ul>
</EuiCallOut>
@ -193,18 +171,14 @@ const RuleActionsFormComponent = ({ rulesCount, onClose, onConfirm }: RuleAction
/>
<EuiSpacer size="m" />
{showActionsSelect && (
<>
<UseField
path="actions"
component={RuleActionsField}
componentProps={{
messageVariables,
}}
/>
<EuiSpacer size="m" />
</>
)}
<UseField
path="actions"
component={RuleActionsField}
componentProps={{
messageVariables,
}}
/>
<EuiSpacer size="m" />
<CommonUseField
path="overwrite"

View file

@ -5,26 +5,26 @@
* 2.0.
*/
import type { BulkEditError, RulesClient } from '@kbn/alerting-plugin/server';
import pMap from 'p-map';
import type { RulesClient, BulkEditError } from '@kbn/alerting-plugin/server';
import type {
BulkActionEditPayload,
BulkActionEditPayloadRuleActions,
} from '../../../../common/detection_engine/schemas/request/perform_bulk_action_schema';
import { BulkActionEditType } from '../../../../common/detection_engine/schemas/request/perform_bulk_action_schema';
import { enrichFilterWithRuleTypeMapping } from './enrich_filter_with_rule_type_mappings';
import type { MlAuthz } from '../../machine_learning/authz';
import { ruleParamsModifier } from './bulk_actions/rule_params_modifier';
import { splitBulkEditActions } from './bulk_actions/split_bulk_edit_actions';
import { validateBulkEditRule } from './bulk_actions/validations';
import { bulkEditActionToRulesClientOperation } from './bulk_actions/action_to_rules_client_operation';
import {
NOTIFICATION_THROTTLE_NO_ACTIONS,
MAX_RULES_TO_UPDATE_IN_PARALLEL,
} from '../../../../common/constants';
import { BulkActionEditType } from '../../../../common/detection_engine/schemas/request/perform_bulk_action_schema';
import { readRules } from './read_rules';
import type { RuleAlertType } from './types';
import {
MAX_RULES_TO_UPDATE_IN_PARALLEL,
NOTIFICATION_THROTTLE_NO_ACTIONS,
} from '../../../../common/constants';
import { readRules } from './read_rules';
export interface BulkEditRulesArguments {
rulesClient: RulesClient;
@ -67,39 +67,30 @@ export const bulkEditRules = async ({
// rulesClient bulkEdit currently doesn't support bulk mute/unmute.
// this is a workaround to mitigate this,
// until https://github.com/elastic/kibana/issues/139084 is resolved
// if rule actions has been applied:
// - we go through each rule
// - mute/unmute if needed, refetch rule
// calling mute for rule needed only when rule was unmuted before and throttle value is NOTIFICATION_THROTTLE_NO_ACTIONS
// if rule actions has been applied, we go through each rule, unmute it if necessary and refetch it
// calling unmute needed only if rule was muted and throttle value is not NOTIFICATION_THROTTLE_NO_ACTIONS
const ruleActions = attributesActions.filter((rule): rule is BulkActionEditPayloadRuleActions =>
[BulkActionEditType.set_rule_actions, BulkActionEditType.add_rule_actions].includes(rule.type)
);
// bulk edit actions are applying in a historical order.
// bulk edit actions are applied in historical order.
// So, we need to find a rule action that will be applied the last, to be able to check if rule should be muted/unmuted
const rulesAction = ruleActions.pop();
if (rulesAction) {
const muteOrUnmuteErrors: BulkEditError[] = [];
const rulesToMuteOrUnmute = await pMap(
const unmuteErrors: BulkEditError[] = [];
const rulesToUnmute = await pMap(
result.rules,
async (rule) => {
try {
if (rule.muteAll && rulesAction.value.throttle !== NOTIFICATION_THROTTLE_NO_ACTIONS) {
await rulesClient.unmuteAll({ id: rule.id });
return (await readRules({ rulesClient, id: rule.id, ruleId: undefined })) ?? rule;
} else if (
!rule.muteAll &&
rulesAction.value.throttle === NOTIFICATION_THROTTLE_NO_ACTIONS
) {
await rulesClient.muteAll({ id: rule.id });
return (await readRules({ rulesClient, id: rule.id, ruleId: undefined })) ?? rule;
}
return rule;
} catch (err) {
muteOrUnmuteErrors.push({
unmuteErrors.push({
message: err.message,
rule: {
id: rule.id,
@ -115,8 +106,8 @@ export const bulkEditRules = async ({
return {
...result,
rules: rulesToMuteOrUnmute.filter((rule): rule is RuleAlertType => rule != null),
errors: [...result.errors, ...muteOrUnmuteErrors],
rules: rulesToUnmute.filter((rule): rule is RuleAlertType => rule != null),
errors: [...result.errors, ...unmuteErrors],
};
}

View file

@ -1513,10 +1513,9 @@ export default ({ getService }: FtrProviderContext): void => {
});
describe('throttle', () => {
// For bulk editing of rule actions, NOTIFICATION_THROTTLE_NO_ACTIONS
// is not available as payload, because "Perform No Actions" is not a valid option
const casesForEmptyActions = [
{
payloadThrottle: NOTIFICATION_THROTTLE_NO_ACTIONS,
},
{
payloadThrottle: NOTIFICATION_THROTTLE_RULE,
},
@ -1561,10 +1560,6 @@ export default ({ getService }: FtrProviderContext): void => {
});
const casesForNonEmptyActions = [
{
payloadThrottle: NOTIFICATION_THROTTLE_NO_ACTIONS,
expectedThrottle: NOTIFICATION_THROTTLE_NO_ACTIONS,
},
{
payloadThrottle: NOTIFICATION_THROTTLE_RULE,
expectedThrottle: NOTIFICATION_THROTTLE_RULE,
@ -1616,12 +1611,9 @@ export default ({ getService }: FtrProviderContext): void => {
});
describe('notifyWhen', () => {
// For bulk editing of rule actions, NOTIFICATION_THROTTLE_NO_ACTIONS
// is not available as payload, because "Perform No Actions" is not a valid option
const cases = [
{
payload: { throttle: NOTIFICATION_THROTTLE_NO_ACTIONS },
// keeps existing default value which is onActiveAlert
expected: { notifyWhen: 'onActiveAlert' },
},
{
payload: { throttle: '1d' },
expected: { notifyWhen: 'onThrottleInterval' },

View file

@ -220,7 +220,7 @@ export default ({ getService }: FtrProviderContext) => {
updatedRule.rule_id = createRuleBody.rule_id;
updatedRule.name = 'some other name';
updatedRule.actions = [action1];
updatedRule.throttle = '1m';
updatedRule.throttle = '1d';
delete updatedRule.id;
const { body } = await supertest
@ -243,7 +243,7 @@ export default ({ getService }: FtrProviderContext) => {
},
},
];
outputRule.throttle = '1m';
outputRule.throttle = '1d';
const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body);
expect(bodyToCompare).to.eql(outputRule);
});

View file

@ -150,12 +150,12 @@ export default ({ getService }: FtrProviderContext) => {
const updatedRule1 = getSimpleRuleUpdate('rule-1');
updatedRule1.name = 'some other name';
updatedRule1.actions = [action1];
updatedRule1.throttle = '1m';
updatedRule1.throttle = '1d';
const updatedRule2 = getSimpleRuleUpdate('rule-2');
updatedRule2.name = 'some other name';
updatedRule2.actions = [action1];
updatedRule2.throttle = '1m';
updatedRule2.throttle = '1d';
// update both rule names
const { body }: { body: FullResponseSchema[] } = await supertest
@ -179,7 +179,7 @@ export default ({ getService }: FtrProviderContext) => {
},
},
];
outputRule.throttle = '1m';
outputRule.throttle = '1d';
const bodyToCompare = removeServerGeneratedProperties(response);
expect(bodyToCompare).to.eql(outputRule);
});