From 68719bdb07d5307d567489b2b65a3aa8d59e630c Mon Sep 17 00:00:00 2001 From: Ievgen Sorokopud Date: Sun, 23 Apr 2023 10:53:48 +0200 Subject: [PATCH] [RAM][SECURITYSOLUTION][ALERTS] - Integrate Alert summary inside of security solution rule page (#154990) ## Summary [Main ticket](https://github.com/elastic/kibana/issues/151916) This PR dependant on [these changes](https://github.com/elastic/kibana/pull/153101) These changes cover next two tickets: - [RAM][SECURITYSOLUTION][ALERTS] - Integrate per-action frequency field in security solution APIs #154532 - [RAM][SECURITYSOLUTION][ALERTS] - Integrate per-action frequency UI in security solution #154534 With this PR we will integrate per-action `frequency` field which already works within alert framework and will update security solution UI to incorporate the possibility to select "summary" vs "for each alert" type of actions. ![](https://user-images.githubusercontent.com/616158/227377473-f34a330e-81ce-42b4-af1b-e6e302c6319d.png) ## NOTES: - There will be no more "Perform no actions" option which mutes all the actions of the rule. For back compatibility, we need to show that rule is muted in the UI cc @peluja1012 @ARWNightingale - The ability to generate per-alert action will be done as part of https://github.com/elastic/kibana/issues/153611 ## Technical Notes: Here are the overview of the conversions and transformations that we are going to do as part of these changes for devs who are going to review. On rule **create**/**read**/**update**/**patch**: - We always gonna set rule level `throttle` to `undefined` from now on - If each action has `frequency` attribute set, then we just use those values - If actions do not have `frequency` attribute set (or for some reason there is a mix of actions with some of them having `frequency` attribute and some not), then we transform rule level `throttle` into `frequency` and set it for each action in the rule On rule **bulk editing**: - We always gonna set rule level `throttle` to `undefined` - If each action has `frequency` attribute set, then we just use those values - If actions do not have `frequency` attribute set, then we transform rule level `throttle` into `frequency` and set it for each action in the rule - If user passed only `throttle` attribute with empty actions (`actions = []`), this will only remove all actions from the rule This will bring breaking changes which we agreed on with the Advanced Correlation Group cc @XavierM @vitaliidm @peluja1012 ### Checklist Delete any items that are not applicable to this PR. - [x] 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/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [x] [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 --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Maxim Palenov --- .../index.ts | 1 + .../src/actions/index.ts | 13 +- .../src/frequency/index.ts | 37 ++++ .../src/throttle/index.ts | 2 +- .../alerting/server/routes/bulk_edit_rules.ts | 11 + .../format_legacy_actions.ts | 5 +- .../transform_legacy_actions.test.ts | 2 +- .../transform_legacy_actions.ts | 5 +- .../transform_to_alert_throttle.test.ts | 21 ++ .../transform_to_alert_throttle.ts | 20 ++ .../server/rules_client/methods/bulk_edit.ts | 15 +- .../server/rules_client/methods/create.ts | 13 -- .../server/rules_client/methods/update.ts | 14 +- .../security_solution/common/constants.ts | 11 + .../rules/bulk_actions/request_schema.test.ts | 22 -- .../api/rules/bulk_actions/request_schema.ts | 24 ++- .../rule_schema/model/rule_schemas.ts | 3 +- .../detection_engine/transform_actions.ts | 4 + .../bulk_edit_rules_actions.cy.ts | 47 +++-- .../detection_rules/custom_query_rule.cy.ts | 12 +- .../e2e/detection_rules/rule_actions.cy.ts | 2 +- .../security_solution/cypress/objects/rule.ts | 1 - .../cypress/objects/types.ts | 3 - .../cypress/screens/common/rule_actions.ts | 17 ++ .../cypress/screens/create_new_rule.ts | 3 - .../cypress/screens/rules_bulk_edit.ts | 3 - .../cypress/tasks/common/rule_actions.ts | 85 ++++++++ .../cypress/tasks/create_new_rule.ts | 2 - .../cypress/tasks/edit_rule.ts | 5 - .../cypress/tasks/rules_bulk_edit.ts | 5 - .../pages/rule_creation/helpers.test.ts | 86 -------- .../pages/rule_creation/helpers.ts | 10 +- .../components/rules_table/__mocks__/mock.ts | 1 - .../bulk_actions/forms/rule_actions_form.tsx | 48 +---- .../rules_table/bulk_actions/translations.tsx | 15 -- .../bulk_actions/use_bulk_actions.tsx | 11 - .../utils/compute_dry_run_edit_payload.ts | 2 +- .../rules/rule_actions_field/index.tsx | 31 ++- .../rules/step_rule_actions/get_schema.ts | 10 - .../rules/step_rule_actions/index.tsx | 93 ++------- .../rules/step_rule_actions/translations.tsx | 16 -- .../use_manage_case_action.tsx | 66 ------ .../detection_engine/rules/helpers.test.tsx | 30 ++- .../pages/detection_engine/rules/helpers.tsx | 3 +- .../pages/detection_engine/rules/types.ts | 1 - .../routes/__mocks__/utils.ts | 2 +- .../logic/actions/duplicate_rule.test.ts | 2 - .../logic/actions/duplicate_rule.ts | 6 +- .../action_to_rules_client_operation.ts | 25 +-- .../logic/crud/patch_rules.test.ts | 2 + .../logic/crud/update_rules.ts | 9 +- .../logic/export/get_export_all.test.ts | 4 +- .../export/get_export_by_object_ids.test.ts | 6 +- .../normalization/rule_actions.test.ts | 151 ++++++++++++++ .../normalization/rule_actions.ts | 34 ++- .../normalization/rule_converters.ts | 39 ++-- .../rule_management/utils/validate.test.ts | 2 +- .../rule_schema/model/rule_schemas.ts | 12 +- .../translations/translations/fr-FR.json | 6 - .../translations/translations/ja-JP.json | 6 - .../translations/translations/zh-CN.json | 6 - .../basic/tests/create_rules.ts | 1 - .../group1/create_rules.ts | 149 ++++++++++++- .../group1/create_rules_bulk.ts | 145 +++++++++++++ .../group1/delete_rules.ts | 1 + .../group1/delete_rules_bulk.ts | 3 + .../group1/export_rules.ts | 47 +++-- .../security_and_spaces/group1/find_rules.ts | 20 +- .../group10/legacy_actions_migrations.ts | 26 ++- .../group10/patch_rules.ts | 180 +++++++++++++++- .../group10/patch_rules_bulk.ts | 2 +- .../group10/perform_bulk_action.ts | 83 +++++--- .../security_and_spaces/group10/read_rules.ts | 20 +- .../security_and_spaces/group10/throttle.ts | 66 +++--- .../group10/update_rules.ts | 195 +++++++++++++++++- .../group10/update_rules_bulk.ts | 195 +++++++++++++++++- .../utils/get_complex_rule_output.ts | 1 - .../utils/get_rule_actions.ts | 96 +++++++++ .../utils/get_simple_rule_output.ts | 2 +- ...simple_rule_output_with_web_hook_action.ts | 3 +- .../utils/remove_uuid_from_actions.ts | 14 ++ 81 files changed, 1720 insertions(+), 672 deletions(-) create mode 100644 packages/kbn-securitysolution-io-ts-alerting-types/src/frequency/index.ts create mode 100644 x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_to_alert_throttle.test.ts create mode 100644 x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_to_alert_throttle.ts delete mode 100644 x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/use_manage_case_action.tsx create mode 100644 x-pack/test/detection_engine_api_integration/utils/get_rule_actions.ts create mode 100644 x-pack/test/detection_engine_api_integration/utils/remove_uuid_from_actions.ts diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/index.ts index 7a200e4f4c8f9..a1d34a9f94451 100644 --- a/packages/kbn-securitysolution-io-ts-alerting-types/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/index.ts @@ -20,6 +20,7 @@ export * from './src/default_severity_mapping_array'; export * from './src/default_threat_array'; export * from './src/default_to_string'; export * from './src/default_uuid'; +export * from './src/frequency'; export * from './src/language'; export * from './src/machine_learning_job_id'; export * from './src/max_signals'; diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/actions/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/actions/index.ts index ab6df3bdacc2a..1ca2f7fceba40 100644 --- a/packages/kbn-securitysolution-io-ts-alerting-types/src/actions/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/actions/index.ts @@ -9,6 +9,7 @@ import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; import * as t from 'io-ts'; import { saved_object_attributes } from '../saved_object_attributes'; +import { RuleActionFrequency } from '../frequency'; export type RuleActionGroup = t.TypeOf; export const RuleActionGroup = t.string; @@ -94,7 +95,11 @@ export const RuleAction = t.exact( action_type_id: RuleActionTypeId, params: RuleActionParams, }), - t.partial({ uuid: RuleActionUuid, alerts_filter: RuleActionAlertsFilter }), + t.partial({ + uuid: RuleActionUuid, + alerts_filter: RuleActionAlertsFilter, + frequency: RuleActionFrequency, + }), ]) ); @@ -110,7 +115,11 @@ export const RuleActionCamel = t.exact( actionTypeId: RuleActionTypeId, params: RuleActionParams, }), - t.partial({ uuid: RuleActionUuid, alertsFilter: RuleActionAlertsFilter }), + t.partial({ + uuid: RuleActionUuid, + alertsFilter: RuleActionAlertsFilter, + frequency: RuleActionFrequency, + }), ]) ); diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/frequency/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/frequency/index.ts new file mode 100644 index 0000000000000..ba6709d1ff495 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/frequency/index.ts @@ -0,0 +1,37 @@ +/* + * 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 { RuleActionThrottle } from '../throttle'; + +/** + * Action summary indicates whether we will send a summary notification about all the generate alerts or notification per individual alert + */ +export type RuleActionSummary = t.TypeOf; +export const RuleActionSummary = t.boolean; + +/** + * The condition for throttling the notification: `onActionGroupChange`, `onActiveAlert`, or `onThrottleInterval` + */ +export type RuleActionNotifyWhen = t.TypeOf; +export const RuleActionNotifyWhen = t.union([ + t.literal('onActionGroupChange'), + t.literal('onActiveAlert'), + t.literal('onThrottleInterval'), +]); + +/** + * The action frequency defines when the action runs (for example, only on rule execution or at specific time intervals). + */ +export type RuleActionFrequency = t.TypeOf; +export const RuleActionFrequency = t.type({ + summary: RuleActionSummary, + notifyWhen: RuleActionNotifyWhen, + throttle: t.union([RuleActionThrottle, t.null]), +}); diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/throttle/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/throttle/index.ts index fbc75ca67693e..c0fb60482a91c 100644 --- a/packages/kbn-securitysolution-io-ts-alerting-types/src/throttle/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/throttle/index.ts @@ -13,5 +13,5 @@ export type RuleActionThrottle = t.TypeOf; export const RuleActionThrottle = t.union([ t.literal('no_actions'), t.literal('rule'), - TimeDuration({ allowedUnits: ['h', 'd'] }), + TimeDuration({ allowedUnits: ['s', 'm', 'h', 'd'] }), ]); diff --git a/x-pack/plugins/alerting/server/routes/bulk_edit_rules.ts b/x-pack/plugins/alerting/server/routes/bulk_edit_rules.ts index fa30b0ff8d2ed..f22d7d2055b09 100644 --- a/x-pack/plugins/alerting/server/routes/bulk_edit_rules.ts +++ b/x-pack/plugins/alerting/server/routes/bulk_edit_rules.ts @@ -19,6 +19,17 @@ const ruleActionSchema = schema.object({ id: schema.string(), params: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), uuid: schema.maybe(schema.string()), + frequency: schema.maybe( + schema.object({ + summary: schema.boolean(), + throttle: schema.nullable(schema.string()), + notifyWhen: schema.oneOf([ + schema.literal('onActionGroupChange'), + schema.literal('onActiveAlert'), + schema.literal('onThrottleInterval'), + ]), + }) + ), }); const operationsSchema = schema.arrayOf( diff --git a/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/format_legacy_actions.ts b/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/format_legacy_actions.ts index 8689113ca7907..a0aa3286f1f6d 100644 --- a/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/format_legacy_actions.ts +++ b/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/format_legacy_actions.ts @@ -14,6 +14,7 @@ import { injectReferencesIntoActions } from '../../common'; import { transformToNotifyWhen } from './transform_to_notify_when'; import { transformFromLegacyActions } from './transform_legacy_actions'; import { LegacyIRuleActionsAttributes, legacyRuleActionsSavedObjectType } from './types'; +import { transformToAlertThrottle } from './transform_to_alert_throttle'; /** * @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function @@ -136,7 +137,9 @@ export const formatLegacyActions = async ( return { ...rule, actions: [...rule.actions, ...legacyRuleActions], - throttle: (legacyRuleActions.length ? ruleThrottle : rule.throttle) ?? 'no_actions', + throttle: transformToAlertThrottle( + (legacyRuleActions.length ? ruleThrottle : rule.throttle) ?? 'no_actions' + ), notifyWhen: transformToNotifyWhen(ruleThrottle), // muteAll property is disregarded in further rule processing in Security Solution when legacy actions are present. // So it should be safe to set it as false, so it won't be displayed to user as w/o actions see transformFromAlertThrottle method diff --git a/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_legacy_actions.test.ts b/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_legacy_actions.test.ts index b23798294b300..cea32ce3e1913 100644 --- a/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_legacy_actions.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_legacy_actions.test.ts @@ -67,7 +67,7 @@ describe('transformFromLegacyActions', () => { (transformToNotifyWhen as jest.Mock).mockReturnValueOnce(null); const actions = transformFromLegacyActions(legacyActionsAttr, references); - expect(actions[0].frequency?.notifyWhen).toBe('onThrottleInterval'); + expect(actions[0].frequency?.notifyWhen).toBe('onActiveAlert'); }); it('should return transformed legacy actions', () => { diff --git a/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_legacy_actions.ts b/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_legacy_actions.ts index 485a32f781695..1218e96a8dfd7 100644 --- a/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_legacy_actions.ts +++ b/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_legacy_actions.ts @@ -11,6 +11,7 @@ import type { SavedObjectReference } from '@kbn/core/server'; import { RawRuleAction } from '../../../types'; import { transformToNotifyWhen } from './transform_to_notify_when'; import { LegacyIRuleActionsAttributes } from './types'; +import { transformToAlertThrottle } from './transform_to_alert_throttle'; /** * @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function @@ -50,8 +51,8 @@ export const transformFromLegacyActions = ( actionTypeId, frequency: { summary: true, - notifyWhen: transformToNotifyWhen(legacyActionsAttr.ruleThrottle) ?? 'onThrottleInterval', - throttle: legacyActionsAttr.ruleThrottle, + notifyWhen: transformToNotifyWhen(legacyActionsAttr.ruleThrottle) ?? 'onActiveAlert', + throttle: transformToAlertThrottle(legacyActionsAttr.ruleThrottle), }, }, ]; diff --git a/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_to_alert_throttle.test.ts b/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_to_alert_throttle.test.ts new file mode 100644 index 0000000000000..f52742009503a --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_to_alert_throttle.test.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { transformToAlertThrottle } from './transform_to_alert_throttle'; + +describe('transformToAlertThrottle', () => { + it('should return null when throttle is null OR no_actions', () => { + expect(transformToAlertThrottle(null)).toBeNull(); + expect(transformToAlertThrottle('rule')).toBeNull(); + expect(transformToAlertThrottle('no_actions')).toBeNull(); + }); + it('should return same value for other throttle values', () => { + expect(transformToAlertThrottle('1h')).toBe('1h'); + expect(transformToAlertThrottle('1m')).toBe('1m'); + expect(transformToAlertThrottle('1d')).toBe('1d'); + }); +}); diff --git a/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_to_alert_throttle.ts b/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_to_alert_throttle.ts new file mode 100644 index 0000000000000..7f6ecee900e2f --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_to_alert_throttle.ts @@ -0,0 +1,20 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** + * Given a throttle from a "security_solution" rule this will transform it into an "alerting" "throttle" + * on their saved object. + * @params throttle The throttle from a "security_solution" rule + * @returns The "alerting" throttle + */ +export const transformToAlertThrottle = (throttle: string | null | undefined): string | null => { + if (throttle == null || throttle === 'rule' || throttle === 'no_actions') { + return null; + } else { + return throttle; + } +}; diff --git a/x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts b/x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts index ce11a0cc017e3..640f3c3f324b3 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts +++ b/x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts @@ -7,7 +7,7 @@ import pMap from 'p-map'; import Boom from '@hapi/boom'; -import { cloneDeep, omit } from 'lodash'; +import { cloneDeep } from 'lodash'; import { AlertConsumers } from '@kbn/rule-data-utils'; import { KueryNode, nodeBuilder } from '@kbn/es-query'; import { @@ -638,19 +638,6 @@ async function getUpdatedAttributesFromOperations( isAttributesUpdateSkipped = false; } - // TODO https://github.com/elastic/kibana/issues/148414 - // If any action-level frequencies get pushed into a SIEM rule, strip their frequencies - const firstFrequency = updatedOperation.value.find( - (action) => action?.frequency - )?.frequency; - if (rule.attributes.consumer === AlertConsumers.SIEM && firstFrequency) { - ruleActions.actions = ruleActions.actions.map((action) => omit(action, 'frequency')); - if (!attributes.notifyWhen) { - attributes.notifyWhen = firstFrequency.notifyWhen; - attributes.throttle = firstFrequency.throttle; - } - } - break; } case 'snoozeSchedule': { diff --git a/x-pack/plugins/alerting/server/rules_client/methods/create.ts b/x-pack/plugins/alerting/server/rules_client/methods/create.ts index 90d6a4c49f90a..fd83a7b9b92e1 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/create.ts +++ b/x-pack/plugins/alerting/server/rules_client/methods/create.ts @@ -6,8 +6,6 @@ */ import Semver from 'semver'; import Boom from '@hapi/boom'; -import { omit } from 'lodash'; -import { AlertConsumers } from '@kbn/rule-data-utils'; import { SavedObjectsUtils } from '@kbn/core/server'; import { withSpan } from '@kbn/apm-utils'; import { parseDuration } from '../../../common/parse_duration'; @@ -111,17 +109,6 @@ export async function create( throw Boom.badRequest(`Error creating rule: could not create API key - ${error.message}`); } - // TODO https://github.com/elastic/kibana/issues/148414 - // If any action-level frequencies get pushed into a SIEM rule, strip their frequencies - const firstFrequency = data.actions.find((action) => action?.frequency)?.frequency; - if (data.consumer === AlertConsumers.SIEM && firstFrequency) { - data.actions = data.actions.map((action) => omit(action, 'frequency')); - if (!data.notifyWhen) { - data.notifyWhen = firstFrequency.notifyWhen; - data.throttle = firstFrequency.throttle; - } - } - await withSpan({ name: 'validateActions', type: 'rules' }, () => validateActions(context, ruleType, data, allowMissingConnectorSecrets) ); diff --git a/x-pack/plugins/alerting/server/rules_client/methods/update.ts b/x-pack/plugins/alerting/server/rules_client/methods/update.ts index 7c1fbedff37ce..f68b41067ddae 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/update.ts +++ b/x-pack/plugins/alerting/server/rules_client/methods/update.ts @@ -6,9 +6,8 @@ */ import Boom from '@hapi/boom'; -import { isEqual, omit } from 'lodash'; +import { isEqual } from 'lodash'; import { SavedObject } from '@kbn/core/server'; -import { AlertConsumers } from '@kbn/rule-data-utils'; import type { ShouldIncrementRevision } from './bulk_edit'; import { PartialRule, @@ -186,17 +185,6 @@ async function updateAlert( const ruleType = context.ruleTypeRegistry.get(attributes.alertTypeId); - // TODO https://github.com/elastic/kibana/issues/148414 - // If any action-level frequencies get pushed into a SIEM rule, strip their frequencies - const firstFrequency = data.actions.find((action) => action?.frequency)?.frequency; - if (attributes.consumer === AlertConsumers.SIEM && firstFrequency) { - data.actions = data.actions.map((action) => omit(action, 'frequency')); - if (!attributes.notifyWhen) { - attributes.notifyWhen = firstFrequency.notifyWhen; - attributes.throttle = firstFrequency.throttle; - } - } - // Validate const validatedAlertTypeParams = validateRuleTypeParams(data.params, ruleType.validate.params); await validateActions(context, ruleType, data, allowMissingConnectorSecrets); diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index aba27d9b617d5..2551bc34f7fa4 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { RuleNotifyWhen } from '@kbn/alerting-plugin/common'; + /** * as const * @@ -377,9 +379,18 @@ export const ML_GROUP_ID = 'security' as const; export const LEGACY_ML_GROUP_ID = 'siem' as const; export const ML_GROUP_IDS = [ML_GROUP_ID, LEGACY_ML_GROUP_ID] as const; +/** + * Rule Actions + */ export const NOTIFICATION_THROTTLE_NO_ACTIONS = 'no_actions' as const; export const NOTIFICATION_THROTTLE_RULE = 'rule' as const; +export const NOTIFICATION_DEFAULT_FREQUENCY = { + notifyWhen: RuleNotifyWhen.ACTIVE, + throttle: null, + summary: true, +}; + export const showAllOthersBucket: string[] = [ 'destination.ip', 'event.action', diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/bulk_actions/request_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/bulk_actions/request_schema.test.ts index 4b95327810fa9..712312c50140d 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/bulk_actions/request_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/bulk_actions/request_schema.test.ts @@ -515,28 +515,6 @@ describe('Perform bulk action request schema', () => { expect(message.schema).toEqual({}); }); - test('invalid request: missing throttle in payload', () => { - const payload = { - query: 'name: test', - action: BulkActionType.edit, - [BulkActionType.edit]: [ - { - type: BulkActionEditType.add_rule_actions, - value: { - actions: [], - }, - }, - ], - }; - - const message = retrieveValidationMessage(payload); - - expect(getPaths(left(message.errors))).toEqual( - expect.arrayContaining(['Invalid value "undefined" supplied to "edit,value,throttle"']) - ); - expect(message.schema).toEqual({}); - }); - test('invalid request: missing actions in payload', () => { const payload = { query: 'name: test', diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/bulk_actions/request_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/bulk_actions/request_schema.ts index 7b2838ab1d657..e0a392885bad9 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/bulk_actions/request_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/bulk_actions/request_schema.ts @@ -9,6 +9,7 @@ import * as t from 'io-ts'; import { NonEmptyArray, TimeDuration } from '@kbn/securitysolution-io-ts-types'; import { + RuleActionFrequency, RuleActionGroup, RuleActionId, RuleActionParams, @@ -96,11 +97,14 @@ const BulkActionEditPayloadTimeline = t.type({ */ type NormalizedRuleAction = t.TypeOf; const NormalizedRuleAction = t.exact( - t.type({ - group: RuleActionGroup, - id: RuleActionId, - params: RuleActionParams, - }) + t.intersection([ + t.type({ + group: RuleActionGroup, + id: RuleActionId, + params: RuleActionParams, + }), + t.partial({ frequency: RuleActionFrequency }), + ]) ); export type BulkActionEditPayloadRuleActions = t.TypeOf; @@ -109,10 +113,12 @@ export const BulkActionEditPayloadRuleActions = t.type({ t.literal(BulkActionEditType.add_rule_actions), t.literal(BulkActionEditType.set_rule_actions), ]), - value: t.type({ - throttle: ThrottleForBulkActions, - actions: t.array(NormalizedRuleAction), - }), + value: t.intersection([ + t.partial({ throttle: ThrottleForBulkActions }), + t.type({ + actions: t.array(NormalizedRuleAction), + }), + ]), }); type BulkActionEditPayloadSchedule = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_schema/model/rule_schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_schema/model/rule_schemas.ts index e0b427cdcefbc..ff4bb72a5eb65 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/rule_schema/model/rule_schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/rule_schema/model/rule_schemas.ts @@ -119,6 +119,8 @@ export const baseSchema = buildRuleSchemas({ output_index: AlertsIndex, namespace: AlertsIndexNamespace, meta: RuleMetadata, + // Throttle + throttle: RuleActionThrottle, }, defaultable: { // Main attributes @@ -134,7 +136,6 @@ export const baseSchema = buildRuleSchemas({ to: RuleIntervalTo, // Rule actions actions: RuleActionArray, - throttle: RuleActionThrottle, // Rule exceptions exceptions_list: ExceptionListArray, // Misc attributes diff --git a/x-pack/plugins/security_solution/common/detection_engine/transform_actions.ts b/x-pack/plugins/security_solution/common/detection_engine/transform_actions.ts index 1b74bcd320aad..3808837dc0df2 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/transform_actions.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/transform_actions.ts @@ -16,6 +16,7 @@ export const transformRuleToAlertAction = ({ action_type_id: actionTypeId, params, uuid, + frequency, alerts_filter: alertsFilter, }: RuleAlertAction): RuleAction => ({ group, @@ -24,6 +25,7 @@ export const transformRuleToAlertAction = ({ actionTypeId, ...(alertsFilter && { alertsFilter }), ...(uuid && { uuid }), + ...(frequency && { frequency }), }); export const transformAlertToRuleAction = ({ @@ -32,6 +34,7 @@ export const transformAlertToRuleAction = ({ actionTypeId, params, uuid, + frequency, alertsFilter, }: RuleAction): RuleAlertAction => ({ group, @@ -40,6 +43,7 @@ export const transformAlertToRuleAction = ({ action_type_id: actionTypeId, ...(alertsFilter && { alerts_filter: alertsFilter }), ...(uuid && { uuid }), + ...(frequency && { frequency }), }); export const transformRuleToAlertResponseAction = ({ diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules_actions.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules_actions.cy.ts index fceecae2b1d5b..624dec7f1c955 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules_actions.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules_actions.cy.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { RuleActionArray } from '@kbn/securitysolution-io-ts-alerting-types'; import { ROLES } from '../../../common/test'; import { @@ -15,11 +16,18 @@ import { import { actionFormSelector } from '../../screens/common/rule_actions'; import { cleanKibana, deleteAlertsAndRules, deleteConnectors } from '../../tasks/common'; +import type { RuleActionCustomFrequency } from '../../tasks/common/rule_actions'; import { addSlackRuleAction, assertSlackRuleAction, addEmailConnectorAndRuleAction, assertEmailRuleAction, + assertSelectedCustomFrequencyOption, + assertSelectedPerRuleRunFrequencyOption, + assertSelectedSummaryOfAlertsOption, + pickCustomFrequencyOption, + pickPerRuleRunFrequencyOption, + pickSummaryOfAlertsOption, } from '../../tasks/common/rule_actions'; import { waitForRulesTableToBeLoaded, @@ -32,10 +40,8 @@ import { submitBulkEditForm, checkOverwriteRuleActionsCheckbox, openBulkEditRuleActionsForm, - pickActionFrequency, openBulkActionsMenu, } from '../../tasks/rules_bulk_edit'; -import { assertSelectedActionFrequency } from '../../tasks/edit_rule'; import { login, visitWithoutDateRange } from '../../tasks/login'; import { esArchiverResetKibana } from '../../tasks/es_archiver'; @@ -75,7 +81,7 @@ describe.skip('Detection rules, bulk edit of rule actions', () => { esArchiverResetKibana(); createSlackConnector().then(({ body }) => { - const actions = [ + const actions: RuleActionArray = [ { id: body.id, action_type_id: '.slack', @@ -83,6 +89,11 @@ describe.skip('Detection rules, bulk edit of rule actions', () => { params: { message: expectedExistingSlackMessage, }, + frequency: { + summary: true, + throttle: null, + notifyWhen: 'onActiveAlert', + }, }, ]; @@ -120,7 +131,10 @@ describe.skip('Detection rules, bulk edit of rule actions', () => { }); it('Add a rule action to rules (existing connector)', () => { - const expectedActionFrequency = 'Daily'; + const expectedActionFrequency: RuleActionCustomFrequency = { + throttle: 1, + throttleUnit: 'd', + }; loadPrebuiltDetectionRulesFromHeaderBtn(); @@ -131,8 +145,9 @@ describe.skip('Detection rules, bulk edit of rule actions', () => { // ensure rule actions info callout displayed on the form cy.get(RULES_BULK_EDIT_ACTIONS_INFO).should('be.visible'); - pickActionFrequency(expectedActionFrequency); addSlackRuleAction(expectedSlackMessage); + pickSummaryOfAlertsOption(); + pickCustomFrequencyOption(expectedActionFrequency); submitBulkEditForm(); waitForBulkEditActionToFinish({ updatedCount: expectedNumberOfRulesToBeEdited }); @@ -140,7 +155,8 @@ describe.skip('Detection rules, bulk edit of rule actions', () => { // check if rule has been updated goToEditRuleActionsSettingsOf(ruleNameToAssert); - assertSelectedActionFrequency(expectedActionFrequency); + assertSelectedSummaryOfAlertsOption(); + assertSelectedCustomFrequencyOption(expectedActionFrequency, 1); assertSlackRuleAction(expectedExistingSlackMessage, 0); assertSlackRuleAction(expectedSlackMessage, 1); // ensure there is no third action @@ -148,16 +164,15 @@ describe.skip('Detection rules, bulk edit of rule actions', () => { }); it('Overwrite rule actions in rules', () => { - const expectedActionFrequency = 'On each rule execution'; - loadPrebuiltDetectionRulesFromHeaderBtn(); // select both custom and prebuilt rules selectNumberOfRules(expectedNumberOfRulesToBeEdited); openBulkEditRuleActionsForm(); - pickActionFrequency(expectedActionFrequency); addSlackRuleAction(expectedSlackMessage); + pickSummaryOfAlertsOption(); + pickPerRuleRunFrequencyOption(); // check overwrite box, ensure warning is displayed checkOverwriteRuleActionsCheckbox(); @@ -171,22 +186,27 @@ describe.skip('Detection rules, bulk edit of rule actions', () => { // check if rule has been updated goToEditRuleActionsSettingsOf(ruleNameToAssert); - assertSelectedActionFrequency(expectedActionFrequency); + assertSelectedSummaryOfAlertsOption(); + assertSelectedPerRuleRunFrequencyOption(); assertSlackRuleAction(expectedSlackMessage); // ensure existing action was overwritten cy.get(actionFormSelector(1)).should('not.exist'); }); it('Add a rule action to rules (new connector)', () => { - const expectedActionFrequency = 'Hourly'; + const expectedActionFrequency: RuleActionCustomFrequency = { + throttle: 2, + throttleUnit: 'h', + }; const expectedEmail = 'test@example.com'; const expectedSubject = 'Subject'; selectNumberOfRules(expectedNumberOfCustomRulesToBeEdited); openBulkEditRuleActionsForm(); - pickActionFrequency(expectedActionFrequency); addEmailConnectorAndRuleAction(expectedEmail, expectedSubject); + pickSummaryOfAlertsOption(); + pickCustomFrequencyOption(expectedActionFrequency); submitBulkEditForm(); waitForBulkEditActionToFinish({ updatedCount: expectedNumberOfCustomRulesToBeEdited }); @@ -194,7 +214,8 @@ describe.skip('Detection rules, bulk edit of rule actions', () => { // check if rule has been updated goToEditRuleActionsSettingsOf(ruleNameToAssert); - assertSelectedActionFrequency(expectedActionFrequency); + assertSelectedSummaryOfAlertsOption(); + assertSelectedCustomFrequencyOption(expectedActionFrequency, 1); assertEmailRuleAction(expectedEmail, expectedSubject); }); }); diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/custom_query_rule.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/custom_query_rule.cy.ts index 8e2ae1b85cce7..4965072e7038d 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/custom_query_rule.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/custom_query_rule.cy.ts @@ -19,10 +19,13 @@ import { RULE_SWITCH, SEVERITY, } from '../../screens/alerts_detection_rules'; +import { + ACTIONS_NOTIFY_WHEN_BUTTON, + ACTIONS_SUMMARY_BUTTON, +} from '../../screens/common/rule_actions'; import { ABOUT_CONTINUE_BTN, ABOUT_EDIT_BUTTON, - ACTIONS_THROTTLE_INPUT, CUSTOM_QUERY_INPUT, DEFINE_CONTINUE_BUTTON, DEFINE_EDIT_BUTTON, @@ -401,12 +404,11 @@ describe('Custom query rules', () => { goToActionsStepTab(); - cy.get(ACTIONS_THROTTLE_INPUT).invoke('val').should('eql', 'no_actions'); - - cy.get(ACTIONS_THROTTLE_INPUT).select('Weekly'); - addEmailConnectorAndRuleAction('test@example.com', 'Subject'); + cy.get(ACTIONS_SUMMARY_BUTTON).should('have.text', 'Summary of alerts'); + cy.get(ACTIONS_NOTIFY_WHEN_BUTTON).should('have.text', 'Per rule run'); + goToAboutStepTab(); cy.get(TAGS_CLEAR_BUTTON).click({ force: true }); fillAboutRule(getEditedRule()); diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/rule_actions.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/rule_actions.cy.ts index 5ed5ef8be059a..ab458e12dca2d 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/rule_actions.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/rule_actions.cy.ts @@ -43,7 +43,7 @@ describe('Rule actions during detection rule creation', () => { }); const rule = getSimpleCustomQueryRule(); - const actions = { throttle: 'rule', connectors: [indexConnector] }; + const actions = { connectors: [indexConnector] }; const index = actions.connectors[0].index; const initialNumberOfDocuments = 0; const expectedJson = JSON.parse(actions.connectors[0].document); diff --git a/x-pack/plugins/security_solution/cypress/objects/rule.ts b/x-pack/plugins/security_solution/cypress/objects/rule.ts index ac630d1ee0fd9..af44aa14aa668 100644 --- a/x-pack/plugins/security_solution/cypress/objects/rule.ts +++ b/x-pack/plugins/security_solution/cypress/objects/rule.ts @@ -578,7 +578,6 @@ export const expectedExportedRule = (ruleResponse: Cypress.Response = Partial>; export interface Actions { - throttle: RuleActionThrottle; connectors: Connectors[]; } diff --git a/x-pack/plugins/security_solution/cypress/screens/common/rule_actions.ts b/x-pack/plugins/security_solution/cypress/screens/common/rule_actions.ts index 2fe606fc6bf64..9a1702b96d63c 100644 --- a/x-pack/plugins/security_solution/cypress/screens/common/rule_actions.ts +++ b/x-pack/plugins/security_solution/cypress/screens/common/rule_actions.ts @@ -41,3 +41,20 @@ export const INDEX_SELECTOR = "[data-test-subj='.index-siem-ActionTypeSelectOpti export const actionFormSelector = (position: number) => `[data-test-subj="alertActionAccordion-${position}"]`; + +export const ACTIONS_SUMMARY_BUTTON = '[data-test-subj="summaryOrPerRuleSelect"]'; + +export const ACTIONS_NOTIFY_WHEN_BUTTON = '[data-test-subj="notifyWhenSelect"]'; + +export const ACTIONS_NOTIFY_PER_RULE_RUN_BUTTON = '[data-test-subj="onActiveAlert"]'; + +export const ACTIONS_NOTIFY_CUSTOM_FREQUENCY_BUTTON = '[data-test-subj="onThrottleInterval"]'; + +export const ACTIONS_THROTTLE_INPUT = '[data-test-subj="throttleInput"]'; + +export const ACTIONS_THROTTLE_UNIT_INPUT = '[data-test-subj="throttleUnitInput"]'; + +export const ACTIONS_SUMMARY_ALERT_BUTTON = '[data-test-subj="actionNotifyWhen-option-summary"]'; + +export const ACTIONS_SUMMARY_FOR_EACH_ALERT_BUTTON = + '[data-test-subj="actionNotifyWhen-option-for_each"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts index a0cccb508ed66..b248cf06e1a0d 100644 --- a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts @@ -13,9 +13,6 @@ export const ABOUT_EDIT_TAB = '[data-test-subj="edit-rule-about-tab"]'; export const ACTIONS_EDIT_TAB = '[data-test-subj="edit-rule-actions-tab"]'; -export const ACTIONS_THROTTLE_INPUT = - '[data-test-subj="stepRuleActions"] [data-test-subj="select"]'; - export const ADD_FALSE_POSITIVE_BTN = '[data-test-subj="detectionEngineStepAboutRuleFalsePositives"] .euiButtonEmpty__text'; diff --git a/x-pack/plugins/security_solution/cypress/screens/rules_bulk_edit.ts b/x-pack/plugins/security_solution/cypress/screens/rules_bulk_edit.ts index 9546b5da8ad8d..7a36c7fd7c74e 100644 --- a/x-pack/plugins/security_solution/cypress/screens/rules_bulk_edit.ts +++ b/x-pack/plugins/security_solution/cypress/screens/rules_bulk_edit.ts @@ -66,9 +66,6 @@ export const UPDATE_SCHEDULE_LOOKBACK_INPUT = export const UPDATE_SCHEDULE_TIME_UNIT_SELECT = '[data-test-subj="timeType"]'; -export const RULES_BULK_EDIT_ACTIONS_THROTTLE_INPUT = - '[data-test-subj="bulkEditRulesRuleActionThrottle"] [data-test-subj="select"]'; - export const RULES_BULK_EDIT_ACTIONS_INFO = '[data-test-subj="bulkEditRulesRuleActionInfo"]'; export const RULES_BULK_EDIT_ACTIONS_WARNING = '[data-test-subj="bulkEditRulesRuleActionsWarning"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/common/rule_actions.ts b/x-pack/plugins/security_solution/cypress/tasks/common/rule_actions.ts index 2c289eea0f736..a9a45780567ae 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/common/rule_actions.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/common/rule_actions.ts @@ -22,6 +22,15 @@ import { EMAIL_CONNECTOR_PASSWORD_INPUT, FORM_VALIDATION_ERROR, JSON_EDITOR, + ACTIONS_SUMMARY_BUTTON, + ACTIONS_NOTIFY_WHEN_BUTTON, + ACTIONS_THROTTLE_INPUT, + ACTIONS_THROTTLE_UNIT_INPUT, + ACTIONS_SUMMARY_ALERT_BUTTON, + ACTIONS_SUMMARY_FOR_EACH_ALERT_BUTTON, + ACTIONS_NOTIFY_CUSTOM_FREQUENCY_BUTTON, + actionFormSelector, + ACTIONS_NOTIFY_PER_RULE_RUN_BUTTON, } from '../../screens/common/rule_actions'; import { COMBO_BOX_INPUT, COMBO_BOX_SELECTION } from '../../screens/common/controls'; import type { EmailConnector, IndexConnector } from '../../objects/connector'; @@ -84,3 +93,79 @@ export const fillIndexConnectorForm = (connector: IndexConnector = getIndexConne parseSpecialCharSequences: false, }); }; + +export interface RuleActionCustomFrequency { + throttle?: number; + throttleUnit?: 's' | 'm' | 'h' | 'd'; +} + +export const pickSummaryOfAlertsOption = (index = 0) => { + const form = cy.get(actionFormSelector(index)); + form.within(() => { + cy.get(ACTIONS_SUMMARY_BUTTON).click(); + }); + cy.get(ACTIONS_SUMMARY_ALERT_BUTTON).click(); +}; +export const pickForEachAlertOption = (index = 0) => { + const form = cy.get(actionFormSelector(index)); + form.within(() => { + cy.get(ACTIONS_SUMMARY_BUTTON).click(); + }); + cy.get(ACTIONS_SUMMARY_FOR_EACH_ALERT_BUTTON).click(); +}; + +export const pickCustomFrequencyOption = ( + { throttle = 1, throttleUnit = 'h' }: RuleActionCustomFrequency, + index = 0 +) => { + const form = cy.get(actionFormSelector(index)); + form.within(() => { + cy.get(ACTIONS_NOTIFY_WHEN_BUTTON).click(); + }); + cy.get(ACTIONS_NOTIFY_CUSTOM_FREQUENCY_BUTTON).click(); + form.within(() => { + cy.get(ACTIONS_THROTTLE_INPUT).type(`{selectAll}${throttle}`); + cy.get(ACTIONS_THROTTLE_UNIT_INPUT).select(throttleUnit); + }); +}; + +export const pickPerRuleRunFrequencyOption = (index = 0) => { + const form = cy.get(actionFormSelector(index)); + form.within(() => { + cy.get(ACTIONS_NOTIFY_WHEN_BUTTON).click(); + }); + cy.get(ACTIONS_NOTIFY_PER_RULE_RUN_BUTTON).click(); +}; + +export const assertSelectedSummaryOfAlertsOption = (index = 0) => { + const form = cy.get(actionFormSelector(index)); + form.within(() => { + cy.get(ACTIONS_SUMMARY_BUTTON).should('have.text', 'Summary of alerts'); + }); +}; + +export const assertSelectedForEachAlertOption = (index = 0) => { + const form = cy.get(actionFormSelector(index)); + form.within(() => { + cy.get(ACTIONS_SUMMARY_BUTTON).should('have.text', 'For each alert'); + }); +}; + +export const assertSelectedCustomFrequencyOption = ( + { throttle = 1, throttleUnit = 'h' }: RuleActionCustomFrequency, + index = 0 +) => { + const form = cy.get(actionFormSelector(index)); + form.within(() => { + cy.get(ACTIONS_NOTIFY_WHEN_BUTTON).should('have.text', 'Custom frequency'); + cy.get(ACTIONS_THROTTLE_INPUT).should('have.value', throttle); + cy.get(ACTIONS_THROTTLE_UNIT_INPUT).should('have.value', throttleUnit); + }); +}; + +export const assertSelectedPerRuleRunFrequencyOption = (index = 0) => { + const form = cy.get(actionFormSelector(index)); + form.within(() => { + cy.get(ACTIONS_NOTIFY_WHEN_BUTTON).should('have.text', 'Per rule run'); + }); +}; diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts index b8274ed33c120..1f3f051c6f474 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts @@ -99,7 +99,6 @@ import { NEW_TERMS_HISTORY_SIZE, NEW_TERMS_HISTORY_TIME_TYPE, NEW_TERMS_INPUT_AREA, - ACTIONS_THROTTLE_INPUT, CONTINUE_BUTTON, CREATE_WITHOUT_ENABLING_BTN, RULE_INDICES, @@ -407,7 +406,6 @@ export const fillFrom = (from: RuleIntervalFrom = ruleFields.ruleIntervalFrom) = }; export const fillRuleAction = (actions: Actions) => { - cy.get(ACTIONS_THROTTLE_INPUT).select(actions.throttle); actions.connectors.forEach((connector) => { switch (connector.type) { case 'index': diff --git a/x-pack/plugins/security_solution/cypress/tasks/edit_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/edit_rule.ts index a016691328ffd..42d5619c28a67 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/edit_rule.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/edit_rule.ts @@ -6,7 +6,6 @@ */ import { BACK_TO_RULE_DETAILS, EDIT_SUBMIT_BUTTON } from '../screens/edit_rule'; -import { ACTIONS_THROTTLE_INPUT } from '../screens/create_new_rule'; export const saveEditedRule = () => { cy.get(EDIT_SUBMIT_BUTTON).should('exist').click({ force: true }); @@ -17,7 +16,3 @@ export const goBackToRuleDetails = () => { cy.get(BACK_TO_RULE_DETAILS).should('exist').click(); cy.get(BACK_TO_RULE_DETAILS).should('not.exist'); }; - -export const assertSelectedActionFrequency = (frequency: string) => { - cy.get(ACTIONS_THROTTLE_INPUT).find('option:selected').should('have.text', frequency); -}; diff --git a/x-pack/plugins/security_solution/cypress/tasks/rules_bulk_edit.ts b/x-pack/plugins/security_solution/cypress/tasks/rules_bulk_edit.ts index b2203d1b1202a..b4bf088bafd6a 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/rules_bulk_edit.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/rules_bulk_edit.ts @@ -39,7 +39,6 @@ import { UPDATE_SCHEDULE_LOOKBACK_INPUT, RULES_BULK_EDIT_SCHEDULES_WARNING, RULES_BULK_EDIT_OVERWRITE_ACTIONS_CHECKBOX, - RULES_BULK_EDIT_ACTIONS_THROTTLE_INPUT, } from '../screens/rules_bulk_edit'; import { SCHEDULE_DETAILS } from '../screens/rule_details'; @@ -292,7 +291,3 @@ export const assertRuleScheduleValues = ({ interval, lookback }: RuleSchedule) = cy.get('dd').eq(1).should('contain.text', lookback); }); }; - -export const pickActionFrequency = (frequency: string) => { - cy.get(RULES_BULK_EDIT_ACTIONS_THROTTLE_INPUT).select(frequency); -}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.test.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.test.ts index af6a82af1a9d5..8b7a9c62b1541 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.test.ts @@ -797,91 +797,6 @@ describe('helpers', () => { meta: { kibana_siem_app_url: 'http://localhost:5601/app/siem', }, - throttle: 'no_actions', - }; - - expect(result).toEqual(expected); - }); - - test('returns proper throttle value for no_actions', () => { - const mockStepData: ActionsStepRule = { - ...mockData, - throttle: 'no_actions', - }; - const result = formatActionsStepData(mockStepData); - const expected: ActionsStepRuleJson = { - actions: [], - enabled: false, - meta: { - kibana_siem_app_url: mockStepData.kibanaSiemAppUrl, - }, - throttle: 'no_actions', - }; - - expect(result).toEqual(expected); - }); - - test('returns proper throttle value for rule', () => { - const mockStepData: ActionsStepRule = { - ...mockData, - throttle: 'rule', - actions: [ - { - group: 'default', - id: 'id', - actionTypeId: 'actionTypeId', - params: {}, - }, - ], - }; - const result = formatActionsStepData(mockStepData); - const expected: ActionsStepRuleJson = { - 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: { - kibana_siem_app_url: mockStepData.kibanaSiemAppUrl, - }, - throttle: 'rule', - }; - - expect(result).toEqual(expected); - }); - - test('returns proper throttle value for interval', () => { - const mockStepData: ActionsStepRule = { - ...mockData, - throttle: '1d', - actions: [ - { - group: 'default', - id: 'id', - actionTypeId: 'actionTypeId', - params: {}, - }, - ], - }; - const result = formatActionsStepData(mockStepData); - const expected: ActionsStepRuleJson = { - 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: { - kibana_siem_app_url: mockStepData.kibanaSiemAppUrl, - }, - throttle: mockStepData.throttle, }; expect(result).toEqual(expected); @@ -913,7 +828,6 @@ describe('helpers', () => { meta: { kibana_siem_app_url: mockStepData.kibanaSiemAppUrl, }, - throttle: 'no_actions', }; expect(result).toEqual(expected); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.ts index 5442727561ce1..e61f55dbce4ec 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.ts @@ -25,7 +25,6 @@ import type { Type, } from '@kbn/securitysolution-io-ts-alerting-types'; import { ENDPOINT_LIST_ID } from '@kbn/securitysolution-list-constants'; -import { NOTIFICATION_THROTTLE_NO_ACTIONS } from '../../../../../common/constants'; import { assertUnreachable } from '../../../../../common/utility_types'; import { transformAlertToRuleAction, @@ -563,19 +562,12 @@ export const formatAboutStepData = ( }; export const formatActionsStepData = (actionsStepData: ActionsStepRule): ActionsStepRuleJson => { - const { - actions = [], - responseActions, - enabled, - kibanaSiemAppUrl, - throttle = NOTIFICATION_THROTTLE_NO_ACTIONS, - } = actionsStepData; + const { actions = [], responseActions, enabled, kibanaSiemAppUrl } = actionsStepData; return { actions: actions.map(transformAlertToRuleAction), response_actions: responseActions?.map(transformAlertToRuleResponseAction), enabled, - throttle: actions.length ? throttle : NOTIFICATION_THROTTLE_NO_ACTIONS, meta: { kibana_siem_app_url: kibanaSiemAppUrl, }, diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/__mocks__/mock.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/__mocks__/mock.ts index 40707a4307f27..487052fcbf2ef 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/__mocks__/mock.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/__mocks__/mock.ts @@ -200,7 +200,6 @@ export const mockActionsStepRule = (enabled = false): ActionsStepRule => ({ actions: [], kibanaSiemAppUrl: 'http://localhost:5601/app/siem', enabled, - throttle: 'no_actions', }); export const mockDefineStepRule = (): DefineStepRule => ({ diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/forms/rule_actions_form.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/forms/rule_actions_form.tsx index 8b791ee2aece1..ff41017ac1771 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/forms/rule_actions_form.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/forms/rule_actions_form.tsx @@ -23,21 +23,13 @@ import { Field, } from '../../../../../../shared_imports'; import { BulkActionEditType } from '../../../../../../../common/detection_engine/rule_management/api/rules/bulk_actions/request_schema'; -import type { - BulkActionEditPayload, - ThrottleForBulkActions, -} from '../../../../../../../common/detection_engine/rule_management/api/rules/bulk_actions/request_schema'; -import { NOTIFICATION_THROTTLE_RULE } from '../../../../../../../common/constants'; +import type { BulkActionEditPayload } from '../../../../../../../common/detection_engine/rule_management/api/rules/bulk_actions/request_schema'; import { BulkEditFormWrapper } from './bulk_edit_form_wrapper'; import { bulkAddRuleActions as i18n } from '../translations'; import { useKibana } from '../../../../../../common/lib/kibana'; -import { - ThrottleSelectField, - THROTTLE_OPTIONS_FOR_BULK_RULE_ACTIONS, -} from '../../../../../../detections/components/rules/throttle_select_field'; import { getAllActionMessageParams } from '../../../../../../detections/pages/detection_engine/rules/helpers'; import { RuleActionsField } from '../../../../../../detections/components/rules/rule_actions_field'; @@ -45,19 +37,16 @@ import { debouncedValidateRuleActionsField } from '../../../../../../detections/ const CommonUseField = getUseField({ component: Field }); +type BulkActionsRuleAction = RuleAction & Required>; + export interface RuleActionsFormData { - throttle: ThrottleForBulkActions; - actions: RuleAction[]; + actions: BulkActionsRuleAction[]; overwrite: boolean; } const getFormSchema = ( actionTypeRegistry: ActionTypeRegistryContract ): FormSchema => ({ - throttle: { - label: i18n.THROTTLE_LABEL, - helpText: i18n.THROTTLE_HELP_TEXT, - }, actions: { validations: [ { @@ -75,7 +64,6 @@ const getFormSchema = ( }); const defaultFormData: RuleActionsFormData = { - throttle: NOTIFICATION_THROTTLE_RULE, actions: [], overwrite: false, }; @@ -108,7 +96,7 @@ const RuleActionsFormComponent = ({ rulesCount, onClose, onConfirm }: RuleAction return; } - const { actions = [], throttle: throttleToSubmit, overwrite: overwriteValue } = data; + const { actions = [], overwrite: overwriteValue } = data; const editAction = overwriteValue ? BulkActionEditType.set_rule_actions : BulkActionEditType.add_rule_actions; @@ -117,23 +105,10 @@ const RuleActionsFormComponent = ({ rulesCount, onClose, onConfirm }: RuleAction type: editAction, value: { actions: actions.map(({ actionTypeId, ...action }) => action), - throttle: throttleToSubmit, }, }); }, [form, onConfirm]); - const throttleFieldComponentProps = useMemo( - () => ({ - idAria: 'bulkEditRulesRuleActionThrottle', - 'data-test-subj': 'bulkEditRulesRuleActionThrottle', - hasNoInitialSelection: false, - euiFieldProps: { - options: THROTTLE_OPTIONS_FOR_BULK_RULE_ACTIONS, - }, - }), - [] - ); - const messageVariables = useMemo(() => getAllActionMessageParams(), []); return ( @@ -156,24 +131,11 @@ const RuleActionsFormComponent = ({ rulesCount, onClose, onConfirm }: RuleAction } >
    -
  • - -
  • {i18n.RULE_VARIABLES_DETAIL}
- - - omit(a, 'frequency')); - } - startTransaction({ name: BULK_RULE_ACTIONS.EDIT }); const hideWarningToast = () => { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/utils/compute_dry_run_edit_payload.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/utils/compute_dry_run_edit_payload.ts index c8f49ebe4a6c6..f25b1331d766a 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/utils/compute_dry_run_edit_payload.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/utils/compute_dry_run_edit_payload.ts @@ -50,7 +50,7 @@ export function computeDryRunEditPayload(editAction: BulkActionEditType): BulkAc return [ { type: editAction, - value: { throttle: '1h', actions: [] }, + value: { actions: [] }, }, ]; case BulkActionEditType.set_schedule: diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx index 019e20b25db1a..084be38391a45 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx @@ -16,7 +16,6 @@ import type { ActionVariables, NotifyWhenSelectOptions, } from '@kbn/triggers-actions-ui-plugin/public'; -import { RuleNotifyWhen } from '@kbn/alerting-plugin/common'; import type { RuleAction, RuleActionAlertsFilterProperty, @@ -24,6 +23,7 @@ import type { } from '@kbn/alerting-plugin/common'; import { SecurityConnectorFeatureId } from '@kbn/actions-plugin/common'; import { FormattedMessage } from '@kbn/i18n-react'; +import { NOTIFICATION_DEFAULT_FREQUENCY } from '../../../../../common/constants'; import type { FieldHook } from '../../../../shared_imports'; import { useFormContext } from '../../../../shared_imports'; import { useKibana } from '../../../../common/lib/kibana'; @@ -33,12 +33,6 @@ import { FORM_ON_ACTIVE_ALERT_OPTION, } from './translations'; -const DEFAULT_FREQUENCY = { - notifyWhen: RuleNotifyWhen.ACTIVE, - throttle: null, - summary: true, -}; - const NOTIFY_WHEN_OPTIONS: NotifyWhenSelectOptions[] = [ { isSummaryOption: true, @@ -214,6 +208,23 @@ export const RuleActionsField: React.FC = ({ field, messageVariables }) = [field] ); + const setActionFrequency = useCallback( + (key: string, value: RuleActionParam, index: number) => { + field.setValue((prevValue: RuleAction[]) => { + const updatedActions = [...prevValue]; + updatedActions[index] = { + ...updatedActions[index], + frequency: { + ...(updatedActions[index].frequency ?? NOTIFICATION_DEFAULT_FREQUENCY), + [key]: value, + }, + }; + return updatedActions; + }); + }, + [field] + ); + const actionForm = useMemo( () => getActionForm({ @@ -223,22 +234,22 @@ export const RuleActionsField: React.FC = ({ field, messageVariables }) = setActionIdByIndex, setActions: setAlertActionsProperty, setActionParamsProperty, - setActionFrequencyProperty: () => {}, + setActionFrequencyProperty: setActionFrequency, setActionAlertsFilterProperty, featureId: SecurityConnectorFeatureId, defaultActionMessage: DEFAULT_ACTION_MESSAGE, defaultSummaryMessage: DEFAULT_ACTION_MESSAGE, hideActionHeader: true, - hideNotifyWhen: true, hasSummary: true, notifyWhenSelectOptions: NOTIFY_WHEN_OPTIONS, - defaultRuleFrequency: DEFAULT_FREQUENCY, + defaultRuleFrequency: NOTIFICATION_DEFAULT_FREQUENCY, showActionAlertsFilter: true, }), [ actions, getActionForm, messageVariables, + setActionFrequency, setActionIdByIndex, setActionParamsProperty, setAlertActionsProperty, diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/get_schema.ts b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/get_schema.ts index 858578f8a5d38..f16cac0eb923a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/get_schema.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/get_schema.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { i18n } from '@kbn/i18n'; - import type { ActionTypeRegistryContract } from '@kbn/triggers-actions-ui-plugin/public'; import { debouncedValidateRuleActionsField } from '../../../containers/detection_engine/rules/validate_rule_actions_field'; @@ -30,12 +28,4 @@ export const getSchema = ({ responseActions: {}, enabled: {}, kibanaSiemAppUrl: {}, - throttle: { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepRuleActions.fieldThrottleLabel', - { - defaultMessage: 'Actions frequency', - } - ), - }, }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx index 58dd95bcac0dd..86bbb7604add2 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx @@ -15,7 +15,6 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; -import { findIndex } from 'lodash/fp'; import type { FC } from 'react'; import React, { memo, useCallback, useEffect, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -29,20 +28,13 @@ import { ResponseActionsForm } from '../../../../detection_engine/rule_response_ import type { RuleStepProps, ActionsStepRule } from '../../../pages/detection_engine/rules/types'; import { RuleStep } from '../../../pages/detection_engine/rules/types'; import { StepRuleDescription } from '../description_step'; -import { Form, UseField, useForm, useFormData } from '../../../../shared_imports'; +import { Form, UseField, useForm } from '../../../../shared_imports'; import { StepContentWrapper } from '../step_content_wrapper'; -import { - ThrottleSelectField, - THROTTLE_OPTIONS_FOR_RULE_CREATION_AND_EDITING, - DEFAULT_THROTTLE_OPTION, -} from '../throttle_select_field'; import { RuleActionsField } from '../rule_actions_field'; import { useKibana } from '../../../../common/lib/kibana'; import { getSchema } from './get_schema'; import * as I18n from './translations'; import { APP_UI_ID } from '../../../../../common/constants'; -import { useManageCaseAction } from './use_manage_case_action'; -import { THROTTLE_FIELD_HELP_TEXT, THROTTLE_FIELD_HELP_TEXT_WHEN_QUERY } from './translations'; interface StepRuleActionsProps extends RuleStepProps { defaultValues?: ActionsStepRule | null; @@ -55,23 +47,10 @@ export const stepActionsDefaultValue: ActionsStepRule = { actions: [], responseActions: [], kibanaSiemAppUrl: '', - throttle: DEFAULT_THROTTLE_OPTION.value, }; const GhostFormField = () => <>; -const getThrottleOptions = (throttle?: string | null) => { - // Add support for throttle options set by the API - 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_FOR_RULE_CREATION_AND_EDITING; -}; - const DisplayActionsHeader = () => { return ( <> @@ -99,7 +78,6 @@ const StepRuleActionsComponent: FC = ({ actionMessageParams, ruleType, }) => { - const [isLoadingCaseAction] = useManageCaseAction(); const { services: { application, @@ -127,11 +105,6 @@ const StepRuleActionsComponent: FC = ({ schema, }); const { getFields, getFormData, submit } = form; - const [{ throttle: formThrottle }] = useFormData({ - form, - watch: ['throttle'], - }); - const throttle = formThrottle || initialState.throttle; const handleSubmit = useCallback( (enabled: boolean) => { @@ -163,44 +136,20 @@ const StepRuleActionsComponent: FC = ({ }; }, [getData, setForm]); - const throttleOptions = useMemo(() => { - return getThrottleOptions(throttle); - }, [throttle]); - - const throttleFieldComponentProps = useMemo( - () => ({ - idAria: 'detectionEngineStepRuleActionsThrottle', - isDisabled: isLoading, - isLoading: isLoadingCaseAction, - dataTestSubj: 'detectionEngineStepRuleActionsThrottle', - hasNoInitialSelection: false, - helpText: isQueryRule(ruleType) - ? THROTTLE_FIELD_HELP_TEXT_WHEN_QUERY - : THROTTLE_FIELD_HELP_TEXT, - euiFieldProps: { - options: throttleOptions, - }, - }), - [isLoading, isLoadingCaseAction, ruleType, throttleOptions] - ); - const displayActionsOptions = useMemo( - () => - throttle !== stepActionsDefaultValue.throttle ? ( - <> - - - - ) : ( - - ), - [throttle, actionMessageParams] + () => ( + <> + + + + ), + [actionMessageParams] ); const displayResponseActionsOptions = useMemo(() => { if (isQueryRule(ruleType)) { @@ -217,11 +166,6 @@ const StepRuleActionsComponent: FC = ({ return application.capabilities.actions.show ? ( <> - {displayActionsOptions} {responseActionsEnabled && displayResponseActionsOptions} @@ -231,14 +175,6 @@ const StepRuleActionsComponent: FC = ({ ) : ( <> {I18n.NO_ACTIONS_READ_PERMISSIONS} - - - - ); }, [ @@ -246,7 +182,6 @@ const StepRuleActionsComponent: FC = ({ displayActionsOptions, displayResponseActionsOptions, responseActionsEnabled, - throttleFieldComponentProps, ]); if (isReadOnlyView) { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/translations.tsx index 15937883fb409..d467c3af05f8f 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/translations.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/translations.tsx @@ -28,19 +28,3 @@ export const NO_ACTIONS_READ_PERMISSIONS = i18n.translate( 'Cannot create rule actions. You do not have "Read" permissions for the "Actions" plugin.', } ); - -export const THROTTLE_FIELD_HELP_TEXT = i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepRuleActions.fieldThrottleHelpText', - { - defaultMessage: - 'Select when automated actions should be performed if a rule evaluates as true.', - } -); - -export const THROTTLE_FIELD_HELP_TEXT_WHEN_QUERY = i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepRuleActions.fieldThrottleHelpTextWhenQuery', - { - defaultMessage: - 'Select when automated actions should be performed if a rule evaluates as true. This frequency does not apply to Response Actions.', - } -); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/use_manage_case_action.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/use_manage_case_action.tsx deleted file mode 100644 index 57dd5670dba80..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/use_manage_case_action.tsx +++ /dev/null @@ -1,66 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useEffect, useRef, useState } from 'react'; -import { getAllConnectorsUrl, getCreateConnectorUrl } from '@kbn/cases-plugin/common'; -import { convertArrayToCamelCase, KibanaServices } from '../../../../common/lib/kibana'; - -interface CaseAction { - connectorTypeId: string; - id: string; - isPreconfigured: boolean; - name: string; - referencedByCount: number; -} - -const CASE_ACTION_NAME = 'Cases'; - -export const useManageCaseAction = () => { - const hasInit = useRef(true); - const [loading, setLoading] = useState(true); - const [hasError, setHasError] = useState(false); - - useEffect(() => { - const abortCtrl = new AbortController(); - const fetchActions = async () => { - try { - const actions = convertArrayToCamelCase( - await KibanaServices.get().http.fetch(getAllConnectorsUrl(), { - method: 'GET', - signal: abortCtrl.signal, - }) - ) as CaseAction[]; - - if (!actions.some((a) => a.connectorTypeId === '.case' && a.name === CASE_ACTION_NAME)) { - await KibanaServices.get().http.post(getCreateConnectorUrl(), { - method: 'POST', - body: JSON.stringify({ - connector_type_id: '.case', - config: {}, - name: CASE_ACTION_NAME, - secrets: {}, - }), - signal: abortCtrl.signal, - }); - } - setLoading(false); - } catch { - setLoading(false); - setHasError(true); - } - }; - if (hasInit.current) { - hasInit.current = false; - fetchActions(); - } - - return () => { - abortCtrl.abort(); - }; - }, []); - return [loading, hasError]; -}; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx index 22ba4e03dbf38..58d6e4c7c8470 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx @@ -35,6 +35,7 @@ import type { ActionsStepRule, } from './types'; import { getThreatMock } from '../../../../../common/detection_engine/schemas/types/threat.mock'; +import type { RuleAlertAction } from '../../../../../common/detection_engine/types'; describe('rule helpers', () => { moment.suppressDeprecationWarnings = true; @@ -146,7 +147,6 @@ describe('rule helpers', () => { const scheduleRuleStepData = { from: '0s', interval: '5m' }; const ruleActionsStepData = { enabled: true, - throttle: 'no_actions', actions: [], responseActions: undefined, }; @@ -410,16 +410,22 @@ describe('rule helpers', () => { describe('getActionsStepsData', () => { test('returns expected ActionsStepRule rule object', () => { + const actions: RuleAlertAction[] = [ + { + id: 'id', + group: 'group', + params: {}, + action_type_id: 'action_type_id', + frequency: { + summary: true, + throttle: null, + notifyWhen: 'onActiveAlert', + }, + }, + ]; const mockedRule = { ...mockRule('test-id'), - actions: [ - { - id: 'id', - group: 'group', - params: {}, - action_type_id: 'action_type_id', - }, - ], + actions, }; const result: ActionsStepRule = getActionsStepsData(mockedRule); const expected = { @@ -429,11 +435,15 @@ describe('rule helpers', () => { group: 'group', params: {}, actionTypeId: 'action_type_id', + frequency: { + summary: true, + throttle: null, + notifyWhen: 'onActiveAlert', + }, }, ], responseActions: undefined, enabled: mockedRule.enabled, - throttle: 'no_actions', }; expect(result).toEqual(expected); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx index a290ac92f6a66..d0cfdc8b707f3 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx @@ -76,12 +76,11 @@ export const getActionsStepsData = ( response_actions?: ResponseAction[]; } ): ActionsStepRule => { - const { enabled, throttle, meta, actions = [], response_actions: responseActions } = rule; + const { enabled, meta, actions = [], response_actions: responseActions } = rule; return { actions: actions?.map(transformRuleToAlertAction), responseActions: responseActions?.map(transformRuleToAlertResponseAction), - throttle, kibanaSiemAppUrl: meta?.kibana_siem_app_url, enabled, }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts index edbb3b4ecbf97..2d2c7580ed051 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts @@ -194,7 +194,6 @@ export interface ActionsStepRule { responseActions?: RuleResponseAction[]; enabled: boolean; kibanaSiemAppUrl?: string; - throttle?: string | null; } export interface DefineStepRuleJson { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts index 7df109deb6f3b..848a0933da542 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts @@ -65,7 +65,7 @@ export const getOutputRuleAlertForRest = (): RuleResponse => ({ severity_mapping: [], updated_by: 'elastic', tags: [], - throttle: 'no_actions', + throttle: undefined, threat: getThreatMock(), exceptions_list: getListArrayMock(), filters: [ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.test.ts index b441d071c21c1..08a53c007dc06 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.test.ts @@ -109,8 +109,6 @@ describe('duplicateRule', () => { consumer: rule.consumer, schedule: rule.schedule, actions: rule.actions, - throttle: null, // TODO: fix? - notifyWhen: null, // TODO: fix? enabled: false, // covered in a separate test }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.ts index 4a99085123b41..315517504def4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.ts @@ -11,6 +11,7 @@ import { ruleTypeMappings } from '@kbn/securitysolution-rules'; import type { SanitizedRule } from '@kbn/alerting-plugin/common'; import { SERVER_APP_ID } from '../../../../../../common/constants'; import type { InternalRuleCreate, RuleParams } from '../../../rule_schema'; +import { transformToActionFrequency } from '../../normalization/rule_actions'; const DUPLICATE_TITLE = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.cloneRule.duplicateTitle', @@ -33,6 +34,7 @@ export const duplicateRule = async ({ rule }: DuplicateRuleParams): Promise - ({ - field: 'throttle', - operation: 'set', - value: transformToAlertThrottle(throttle), - } as const); - -const getNotifyWhenOperation = (throttle: string) => - ({ - field: 'notifyWhen', - operation: 'set', - value: transformToNotifyWhen(throttle), - } as const); +import { transformToActionFrequency } from '../../normalization/rule_actions'; /** * converts bulk edit action to format of rulesClient.bulkEdit operation @@ -70,10 +55,8 @@ export const bulkEditActionToRulesClientOperation = ( { field: 'actions', operation: 'add', - value: action.value.actions, + value: transformToActionFrequency(action.value.actions, action.value.throttle), }, - getThrottleOperation(action.value.throttle), - getNotifyWhenOperation(action.value.throttle), ]; case BulkActionEditType.set_rule_actions: @@ -81,10 +64,8 @@ export const bulkEditActionToRulesClientOperation = ( { field: 'actions', operation: 'set', - value: action.value.actions, + value: transformToActionFrequency(action.value.actions, action.value.throttle), }, - getThrottleOperation(action.value.throttle), - getNotifyWhenOperation(action.value.throttle), ]; // schedule actions diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/crud/patch_rules.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/crud/patch_rules.test.ts index cfb051273255a..6e4d573a880e0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/crud/patch_rules.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/crud/patch_rules.test.ts @@ -123,6 +123,7 @@ describe('patchRules', () => { message: 'Rule {{context.rule.name}} generated {{state.signals_count}} signals', }, group: 'default', + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, }, ], }), @@ -158,6 +159,7 @@ describe('patchRules', () => { message: 'Rule {{context.rule.name}} generated {{state.signals_count}} signals', }, group: 'default', + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, }, ], }), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/crud/update_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/crud/update_rules.ts index 1996cb1652ab6..070a81692f3d5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/crud/update_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/crud/update_rules.ts @@ -12,7 +12,7 @@ import type { RuleUpdateProps } from '../../../../../../common/detection_engine/ import { transformRuleToAlertAction } from '../../../../../../common/detection_engine/transform_actions'; import type { InternalRuleUpdate, RuleParams, RuleAlertType } from '../../../rule_schema'; -import { transformToAlertThrottle, transformToNotifyWhen } from '../../normalization/rule_actions'; +import { transformToActionFrequency } from '../../normalization/rule_actions'; import { typeSpecificSnakeToCamel } from '../../normalization/rule_converters'; export interface UpdateRulesOptions { @@ -30,6 +30,9 @@ export const updateRules = async ({ return null; } + const alertActions = ruleUpdate.actions?.map(transformRuleToAlertAction) ?? []; + const actions = transformToActionFrequency(alertActions, ruleUpdate.throttle); + const typeSpecificParams = typeSpecificSnakeToCamel(ruleUpdate); const enabled = ruleUpdate.enabled ?? true; const newInternalRule: InternalRuleUpdate = { @@ -70,9 +73,7 @@ export const updateRules = async ({ ...typeSpecificParams, }, schedule: { interval: ruleUpdate.interval ?? '5m' }, - actions: ruleUpdate.actions != null ? ruleUpdate.actions.map(transformRuleToAlertAction) : [], - throttle: transformToAlertThrottle(ruleUpdate.throttle), - notifyWhen: transformToNotifyWhen(ruleUpdate.throttle), + actions, }; const update = await rulesClient.update({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_all.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_all.test.ts index 038dfbc9152d6..c86387f26f50a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_all.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_all.test.ts @@ -131,7 +131,6 @@ describe('getExportAll', () => { to: 'now', type: 'query', threat: getThreatMock(), - throttle: 'no_actions', note: '# Investigative notes', version: 1, exceptions_list: getListArrayMock(), @@ -275,6 +274,7 @@ describe('getExportAll', () => { message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', }, action_type_id: '.slack', + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, }, ], building_block_type: 'default', @@ -313,7 +313,6 @@ describe('getExportAll', () => { to: 'now', type: 'query', threat: getThreatMock(), - throttle: 'rule', note: '# Investigative notes', version: 1, revision: 0, @@ -416,6 +415,7 @@ describe('getExportAll', () => { message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', }, action_type_id: '.email', + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, }, ], }) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.test.ts index de76ef3b23d8b..35151f1b4b1d6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.test.ts @@ -128,7 +128,6 @@ describe('get_export_by_object_ids', () => { to: 'now', type: 'query', threat: getThreatMock(), - throttle: 'no_actions', note: '# Investigative notes', version: 1, exceptions_list: getListArrayMock(), @@ -284,6 +283,7 @@ describe('get_export_by_object_ids', () => { message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', }, action_type_id: '.slack', + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, }, ], building_block_type: 'default', @@ -322,7 +322,6 @@ describe('get_export_by_object_ids', () => { to: 'now', type: 'query', threat: getThreatMock(), - throttle: 'rule', note: '# Investigative notes', version: 1, revision: 0, @@ -426,6 +425,7 @@ describe('get_export_by_object_ids', () => { message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', }, action_type_id: '.email', + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, }, ], }) @@ -508,7 +508,7 @@ describe('get_export_by_object_ids', () => { to: 'now', type: 'query', threat: getThreatMock(), - throttle: 'no_actions', + throttle: undefined, note: '# Investigative notes', version: 1, revision: 0, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_actions.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_actions.test.ts index d0d5edad970f0..33fcf0bd29c20 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_actions.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_actions.test.ts @@ -5,7 +5,9 @@ * 2.0. */ +import type { SanitizedRuleAction } from '@kbn/alerting-plugin/common'; import { + NOTIFICATION_DEFAULT_FREQUENCY, NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE, } from '../../../../../common/constants'; @@ -14,6 +16,7 @@ import type { RuleAlertType } from '../../rule_schema'; import { transformFromAlertThrottle, + transformToActionFrequency, transformToAlertThrottle, transformToNotifyWhen, } from './rule_actions'; @@ -151,4 +154,152 @@ describe('Rule actions normalization', () => { ).toEqual(NOTIFICATION_THROTTLE_RULE); }); }); + + describe('transformToActionFrequency', () => { + describe('actions without frequencies', () => { + const actionsWithoutFrequencies: SanitizedRuleAction[] = [ + { + group: 'group', + id: 'id-123', + actionTypeId: 'id-456', + params: {}, + }, + { + group: 'group', + id: 'id-789', + actionTypeId: 'id-012', + params: {}, + }, + ]; + + test.each([undefined, null, NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE])( + `it sets each action's frequency attribute to default value when 'throttle' is '%s'`, + (throttle) => { + expect(transformToActionFrequency(actionsWithoutFrequencies, throttle)).toEqual( + actionsWithoutFrequencies.map((action) => ({ + ...action, + frequency: NOTIFICATION_DEFAULT_FREQUENCY, + })) + ); + } + ); + + test.each(['47s', '10m', '3h', '101d'])( + `it correctly transforms 'throttle = %s' and sets it as a frequency of each action`, + (throttle) => { + expect(transformToActionFrequency(actionsWithoutFrequencies, throttle)).toEqual( + actionsWithoutFrequencies.map((action) => ({ + ...action, + frequency: { + summary: true, + throttle, + notifyWhen: 'onThrottleInterval', + }, + })) + ); + } + ); + }); + + describe('actions with frequencies', () => { + const actionsWithFrequencies: SanitizedRuleAction[] = [ + { + group: 'group', + id: 'id-123', + actionTypeId: 'id-456', + params: {}, + frequency: { + summary: true, + throttle: null, + notifyWhen: 'onActiveAlert', + }, + }, + { + group: 'group', + id: 'id-789', + actionTypeId: 'id-012', + params: {}, + frequency: { + summary: false, + throttle: '1s', + notifyWhen: 'onThrottleInterval', + }, + }, + ]; + + test.each([ + undefined, + null, + NOTIFICATION_THROTTLE_NO_ACTIONS, + NOTIFICATION_THROTTLE_RULE, + '1h', + '1d', + ])(`it does not change actions frequency attributes when 'throttle' is '%s'`, (throttle) => { + expect(transformToActionFrequency(actionsWithFrequencies, throttle)).toEqual( + actionsWithFrequencies + ); + }); + }); + + describe('some actions with frequencies', () => { + const someActionsWithFrequencies: SanitizedRuleAction[] = [ + { + group: 'group', + id: 'id-123', + actionTypeId: 'id-456', + params: {}, + frequency: { + summary: true, + throttle: null, + notifyWhen: 'onActiveAlert', + }, + }, + { + group: 'group', + id: 'id-789', + actionTypeId: 'id-012', + params: {}, + frequency: { + summary: false, + throttle: '1s', + notifyWhen: 'onThrottleInterval', + }, + }, + { + group: 'group', + id: 'id-345', + actionTypeId: 'id-678', + params: {}, + }, + ]; + + test.each([undefined, null, NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE])( + `it overrides each action's frequency attribute to default value when 'throttle' is '%s'`, + (throttle) => { + expect(transformToActionFrequency(someActionsWithFrequencies, throttle)).toEqual( + someActionsWithFrequencies.map((action) => ({ + ...action, + frequency: action.frequency ?? NOTIFICATION_DEFAULT_FREQUENCY, + })) + ); + } + ); + + test.each(['47s', '10m', '3h', '101d'])( + `it correctly transforms 'throttle = %s' and overrides frequency attribute of each action`, + (throttle) => { + expect(transformToActionFrequency(someActionsWithFrequencies, throttle)).toEqual( + someActionsWithFrequencies.map((action) => ({ + ...action, + frequency: action.frequency ?? { + summary: true, + throttle, + notifyWhen: 'onThrottleInterval', + }, + })) + ); + } + ); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_actions.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_actions.ts index 51dbe9d80f986..d0e909cc5f329 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_actions.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_actions.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { RuleNotifyWhenType } from '@kbn/alerting-plugin/common'; +import type { RuleActionFrequency, RuleNotifyWhenType } from '@kbn/alerting-plugin/common'; import { NOTIFICATION_THROTTLE_NO_ACTIONS, @@ -14,6 +14,38 @@ import { import type { RuleAlertType } from '../../rule_schema'; +export const transformToFrequency = (throttle: string | null | undefined): RuleActionFrequency => { + return { + summary: true, + notifyWhen: transformToNotifyWhen(throttle) ?? 'onActiveAlert', + throttle: transformToAlertThrottle(throttle), + }; +}; + +interface ActionWithFrequency { + frequency?: RuleActionFrequency; +} + +/** + * The action level `frequency` attribute should always take precedence over the rule level `throttle` + * Frequency's default value is `{ summary: true, throttle: null, notifyWhen: 'onActiveAlert' }` + * + * The transformation follows the next rules: + * - Both rule level `throttle` and all actions have `frequency` are set: we will ignore rule level `throttle` + * - Rule level `throttle` set and actions don't have `frequency` set: we will transform rule level `throttle` in action level `frequency` + * - All actions have `frequency` set: do nothing + * - Neither of them is set: we will set action level `frequency` to default value + * - Rule level `throttle` and some of the actions have `frequency` set: we will transform rule level `throttle` and set it to actions without the frequency attribute + * - Only some actions have `frequency` set and there is no rule level `throttle`: we will set default `frequency` to actions without frequency attribute + */ +export const transformToActionFrequency = ( + actions: T[], + throttle: string | null | undefined +): T[] => { + const defaultFrequency = transformToFrequency(throttle); + return actions.map((action) => ({ ...action, frequency: action.frequency ?? defaultFrequency })); +}; + /** * Given a throttle from a "security_solution" rule this will transform it into an "alerting" notifyWhen * on their saved object. diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_converters.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_converters.ts index 7296e90bf73e7..66d873c2c0935 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_converters.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_converters.ts @@ -39,10 +39,10 @@ import { } from '../../../../../common/detection_engine/rule_schema'; import { + transformAlertToRuleAction, transformAlertToRuleResponseAction, transformRuleToAlertAction, transformRuleToAlertResponseAction, - transformAlertToRuleAction, } from '../../../../../common/detection_engine/transform_actions'; import { @@ -73,11 +73,7 @@ import type { NewTermsRuleParams, NewTermsSpecificRuleParams, } from '../../rule_schema'; -import { - transformFromAlertThrottle, - transformToAlertThrottle, - transformToNotifyWhen, -} from './rule_actions'; +import { transformFromAlertThrottle, transformToActionFrequency } from './rule_actions'; import { convertAlertSuppressionToCamel, convertAlertSuppressionToSnake } from '../utils/utils'; import { createRuleExecutionSummary } from '../../rule_monitoring'; @@ -399,6 +395,11 @@ export const convertPatchAPIToInternalSchema = ( ): InternalRuleUpdate => { const typeSpecificParams = patchTypeSpecificSnakeToCamel(nextParams, existingRule.params); const existingParams = existingRule.params; + + const alertActions = nextParams.actions?.map(transformRuleToAlertAction) ?? existingRule.actions; + const throttle = nextParams.throttle ?? transformFromAlertThrottle(existingRule); + const actions = transformToActionFrequency(alertActions, throttle); + return { name: nextParams.name ?? existingRule.name, tags: nextParams.tags ?? existingRule.tags, @@ -438,15 +439,7 @@ export const convertPatchAPIToInternalSchema = ( ...typeSpecificParams, }, schedule: { interval: nextParams.interval ?? existingRule.schedule.interval }, - actions: nextParams.actions - ? nextParams.actions.map(transformRuleToAlertAction) - : existingRule.actions, - throttle: nextParams.throttle - ? transformToAlertThrottle(nextParams.throttle) - : existingRule.throttle ?? null, - notifyWhen: nextParams.throttle - ? transformToNotifyWhen(nextParams.throttle) - : existingRule.notifyWhen ?? null, + actions, }; }; @@ -462,6 +455,10 @@ export const convertCreateAPIToInternalSchema = ( ): InternalRuleCreate => { const typeSpecificParams = typeSpecificSnakeToCamel(input); const newRuleId = input.rule_id ?? uuidv4(); + + const alertActions = input.actions?.map(transformRuleToAlertAction) ?? []; + const actions = transformToActionFrequency(alertActions, input.throttle); + return { name: input.name, tags: input.tags ?? [], @@ -502,9 +499,7 @@ export const convertCreateAPIToInternalSchema = ( }, schedule: { interval: input.interval ?? '5m' }, enabled: input.enabled ?? defaultEnabled, - actions: input.actions?.map(transformRuleToAlertAction) ?? [], - throttle: transformToAlertThrottle(input.throttle), - notifyWhen: transformToNotifyWhen(input.throttle), + actions, }; }; @@ -651,6 +646,10 @@ export const internalRuleToAPIResponse = ( const isResolvedRule = (obj: unknown): obj is ResolvedSanitizedRule => (obj as ResolvedSanitizedRule).outcome != null; + const alertActions = rule.actions.map(transformAlertToRuleAction); + const throttle = transformFromAlertThrottle(rule); + const actions = transformToActionFrequency(alertActions, throttle); + return { // saved object properties outcome: isResolvedRule(rule) ? rule.outcome : undefined, @@ -672,8 +671,8 @@ export const internalRuleToAPIResponse = ( // Type specific security solution rule params ...typeSpecificCamelToSnake(rule.params), // Actions - throttle: transformFromAlertThrottle(rule), - actions: rule.actions.map(transformAlertToRuleAction), + throttle: undefined, + actions, // Execution summary execution_summary: executionSummary ?? undefined, }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.test.ts index bae75363ff49a..373842fe31860 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.test.ts @@ -43,7 +43,7 @@ export const ruleOutput = (): RuleResponse => ({ tags: [], to: 'now', type: 'query', - throttle: 'no_actions', + throttle: undefined, threat: getThreatMock(), version: 1, revision: 0, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.ts index e204aeb7bc50f..0fbddc2a2236c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.ts @@ -14,6 +14,7 @@ import { RiskScore, RiskScoreMapping, RuleActionArrayCamel, + RuleActionNotifyWhen, RuleActionThrottle, RuleIntervalFrom, RuleIntervalTo, @@ -259,13 +260,6 @@ export interface CompleteRule { ruleConfig: SanitizedRuleConfig; } -export const notifyWhen = t.union([ - t.literal('onActionGroupChange'), - t.literal('onActiveAlert'), - t.literal('onThrottleInterval'), - t.null, -]); - export const allRuleTypes = t.union([ t.literal(SIGNALS_ID), t.literal(EQL_RULE_TYPE_ID), @@ -291,7 +285,7 @@ const internalRuleCreateRequired = t.type({ }); const internalRuleCreateOptional = t.partial({ throttle: t.union([RuleActionThrottle, t.null]), - notifyWhen, + notifyWhen: t.union([RuleActionNotifyWhen, t.null]), }); export const internalRuleCreate = t.intersection([ internalRuleCreateOptional, @@ -310,7 +304,7 @@ const internalRuleUpdateRequired = t.type({ }); const internalRuleUpdateOptional = t.partial({ throttle: t.union([RuleActionThrottle, t.null]), - notifyWhen, + notifyWhen: t.union([RuleActionNotifyWhen, t.null]), }); export const internalRuleUpdate = t.intersection([ internalRuleUpdateOptional, diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index d3c4ae3938077..290368a66b4ed 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -28999,9 +28999,6 @@ "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.threatMatchingIcesHelperDescription": "Sélectionner des index de menaces", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.threatMatchoutputIndiceNameFieldRequiredError": "Au minimum un modèle d'indexation est requis.", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.thresholdField.thresholdFieldPlaceholderText": "Tous les résultats", - "xpack.securitySolution.detectionEngine.createRule.stepRuleActions.fieldThrottleHelpText": "Sélectionnez le moment auquel les actions automatiques doivent être effectuées si une règle est évaluée comme vraie.", - "xpack.securitySolution.detectionEngine.createRule.stepRuleActions.fieldThrottleHelpTextWhenQuery": "Sélectionnez le moment auquel les actions automatiques doivent être effectuées si une règle est évaluée comme vraie. Cette fréquence ne s'applique pas aux actions de réponse.", - "xpack.securitySolution.detectionEngine.createRule.stepRuleActions.fieldThrottleLabel": "Fréquence des actions", "xpack.securitySolution.detectionEngine.createRule.stepRuleActions.noReadActionsPrivileges": "Impossible de créer des actions de règle. Vous ne disposez pas des autorisations \"Lire\" pour le plug-in \"Actions\".", "xpack.securitySolution.detectionEngine.createRule.stepScheduleRule.completeWithEnablingTitle": "Créer et activer la règle", "xpack.securitySolution.detectionEngine.createRule.stepScheduleRule.completeWithoutEnablingTitle": "Créer la règle sans l’activer", @@ -29959,12 +29956,9 @@ "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.exceptionsConfirmation.tooltip": " Si vous dupliquez les exceptions, la liste des exceptions partagée sera dupliquée par référence et l'exception de la règle par défaut sera copiée et créée comme une nouvelle exception", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.successToastTitle": "Règles dupliquées", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicateTitle": "Dupliquer", - "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.actionFrequencyDetail": "La fréquence des actions que vous sélectionnez ci-dessous est appliquée à toutes les actions (nouvelles et existantes) pour toutes les règles sélectionnées.", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.formTitle": "Ajouter des actions sur les règles", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.overwriteCheckboxLabel": "Écraser toutes les actions sur les règles sélectionnées", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.ruleVariablesDetail": "Les variables de règle peuvent affecter uniquement certaines règles sélectionnées, en fonction des types de règle (par exemple, \\u007b\\u007bcontext.rule.threshold\\u007d\\u007d affichera uniquement les valeurs des règles de seuil).", - "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.throttleHelpText": "Sélectionnez le moment auquel les actions automatiques doivent être effectuées si une règle est évaluée comme vraie.", - "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.throttleLabel": "Fréquence des actions", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.warningCalloutMessage.buttonLabel": "Enregistrer", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.applyTimelineTemplate.formTitle": "Appliquer le modèle de chronologie", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.applyTimelineTemplate.templateSelectorDefaultValue": "Aucun", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 060f5364f947f..c1c8d3f14fe4e 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -28978,9 +28978,6 @@ "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.threatMatchingIcesHelperDescription": "脅威インデックスを選択", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.threatMatchoutputIndiceNameFieldRequiredError": "インデックスパターンが最低1つ必要です。", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.thresholdField.thresholdFieldPlaceholderText": "すべての結果", - "xpack.securitySolution.detectionEngine.createRule.stepRuleActions.fieldThrottleHelpText": "ルールが true であると評価された場合に自動アクションを実行するタイミングを選択します。", - "xpack.securitySolution.detectionEngine.createRule.stepRuleActions.fieldThrottleHelpTextWhenQuery": "ルールが true であると評価された場合に自動アクションを実行するタイミングを選択します。この頻度は対応アクションには適用されません。", - "xpack.securitySolution.detectionEngine.createRule.stepRuleActions.fieldThrottleLabel": "アクション頻度", "xpack.securitySolution.detectionEngine.createRule.stepRuleActions.noReadActionsPrivileges": "ルールアクションを作成できません。「Actions」プラグインの「読み取り」アクセス権がありません。", "xpack.securitySolution.detectionEngine.createRule.stepScheduleRule.completeWithEnablingTitle": "ルールを作成して有効にする", "xpack.securitySolution.detectionEngine.createRule.stepScheduleRule.completeWithoutEnablingTitle": "有効にせずにルールを作成", @@ -29938,12 +29935,9 @@ "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.exceptionsConfirmation.tooltip": " 例外を複製する場合は、参照によって共有例外リストが複製されます。それから、デフォルトルール例外がコピーされ、新しい例外が作成されます", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.successToastTitle": "ルールが複製されました", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicateTitle": "複製", - "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.actionFrequencyDetail": "以下で選択したアクション頻度は、すべての選択したルールのすべてのアクション(新規と既存のアクション)に適用されます。", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.formTitle": "ルールアクションを追加", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.overwriteCheckboxLabel": "すべての選択したルールアクションを上書き", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.ruleVariablesDetail": "ルールタイプによっては、ルール変数が選択舌一部のルールにのみ影響する場合があります(例:\\u007b\\u007bcontext.rule.threshold\\u007d\\u007dはしきい値ルールの値のみを表示します)。", - "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.throttleHelpText": "ルールが true であると評価された場合に自動アクションを実行するタイミングを選択します。", - "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.throttleLabel": "アクション頻度", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.warningCalloutMessage.buttonLabel": "保存", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.applyTimelineTemplate.formTitle": "タイムラインテンプレートを適用", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.applyTimelineTemplate.templateSelectorDefaultValue": "なし", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index e577ba4753e8f..40989bc638cc5 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -28994,9 +28994,6 @@ "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.threatMatchingIcesHelperDescription": "选择威胁索引", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.threatMatchoutputIndiceNameFieldRequiredError": "至少需要一种索引模式。", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.thresholdField.thresholdFieldPlaceholderText": "所有结果", - "xpack.securitySolution.detectionEngine.createRule.stepRuleActions.fieldThrottleHelpText": "选择在规则评估为 true 时应执行自动操作的时间。", - "xpack.securitySolution.detectionEngine.createRule.stepRuleActions.fieldThrottleHelpTextWhenQuery": "选择在规则评估为 true 时应执行自动操作的时间。此频率不适用于响应操作。", - "xpack.securitySolution.detectionEngine.createRule.stepRuleActions.fieldThrottleLabel": "操作频率", "xpack.securitySolution.detectionEngine.createRule.stepRuleActions.noReadActionsPrivileges": "无法创建规则操作。您对“操作”插件没有“读”权限。", "xpack.securitySolution.detectionEngine.createRule.stepScheduleRule.completeWithEnablingTitle": "创建并启用规则", "xpack.securitySolution.detectionEngine.createRule.stepScheduleRule.completeWithoutEnablingTitle": "创建规则但不启用", @@ -29954,12 +29951,9 @@ "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.exceptionsConfirmation.tooltip": " 如果您复制例外,则会通过引用复制共享例外列表,然后复制默认规则例外,并将其创建为新例外", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.successToastTitle": "规则已复制", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicateTitle": "复制", - "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.actionFrequencyDetail": "您在下面选择的操作频率将应用于所有选定规则的所有操作(新操作和现有操作)。", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.formTitle": "添加规则操作", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.overwriteCheckboxLabel": "覆盖所有选定规则操作", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.ruleVariablesDetail": "基于规则类型,规则变量可能仅影响您选择的某些规则(例如,\\u007b\\u007bcontext.rule.threshold\\u007d\\u007d 将仅显示阈值规则的值)。", - "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.throttleHelpText": "选择在规则评估为 true 时应执行自动操作的时间。", - "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.throttleLabel": "操作频率", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.warningCalloutMessage.buttonLabel": "保存", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.applyTimelineTemplate.formTitle": "应用时间线模板", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.applyTimelineTemplate.templateSelectorDefaultValue": "无", diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts index 62fa4d3786db6..2c6ae644d911c 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts @@ -99,7 +99,6 @@ export default ({ getService }: FtrProviderContext) => { to: 'now', type: 'query', threat: [], - throttle: 'no_actions', exceptions_list: [], version: 1, revision: 0, diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules.ts index 4346087684fd4..e6adaa069b497 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules.ts @@ -7,7 +7,12 @@ import expect from '@kbn/expect'; -import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; +import { + DETECTION_ENGINE_RULES_URL, + NOTIFICATION_DEFAULT_FREQUENCY, + NOTIFICATION_THROTTLE_NO_ACTIONS, + NOTIFICATION_THROTTLE_RULE, +} from '@kbn/security-solution-plugin/common/constants'; import { RuleCreateProps } from '@kbn/security-solution-plugin/common/detection_engine/rule_schema'; import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { ROLES } from '@kbn/security-solution-plugin/common/test'; @@ -32,8 +37,15 @@ import { waitForSignalsToBePresent, getThresholdRuleForSignalTesting, waitForRulePartialFailure, + createRule, } from '../../utils'; import { createUserAndRole, deleteUserAndRole } from '../../../common/services/security_solution'; +import { + getActionsWithFrequencies, + getActionsWithoutFrequencies, + getSomeActionsWithFrequencies, +} from '../../utils/get_rule_actions'; +import { removeUUIDFromActions } from '../../utils/remove_uuid_from_actions'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { @@ -200,7 +212,6 @@ export default ({ getService }: FtrProviderContext) => { to: 'now', type: 'query', threat: [], - throttle: 'no_actions', exceptions_list: [], version: 1, }; @@ -566,5 +577,139 @@ export default ({ getService }: FtrProviderContext) => { expect(rule?.execution_summary?.last_execution.status).to.eql('partial failure'); }); }); + + describe('per-action frequencies', () => { + const createSingleRule = async (rule: RuleCreateProps) => { + const createdRule = await createRule(supertest, log, rule); + createdRule.actions = removeUUIDFromActions(createdRule.actions); + return createdRule; + }; + + describe('actions without frequencies', () => { + [undefined, NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE].forEach( + (throttle) => { + it(`it sets each action's frequency attribute to default value when 'throttle' is ${throttle}`, async () => { + const actionsWithoutFrequencies = await getActionsWithoutFrequencies(supertest); + + const simpleRule = getSimpleRule(); + simpleRule.throttle = throttle; + simpleRule.actions = actionsWithoutFrequencies; + + const createdRule = await createSingleRule(simpleRule); + + const expectedRule = getSimpleRuleOutput(); + expectedRule.actions = actionsWithoutFrequencies.map((action) => ({ + ...action, + frequency: NOTIFICATION_DEFAULT_FREQUENCY, + })); + + const rule = removeServerGeneratedProperties(createdRule); + expect(rule).to.eql(expectedRule); + }); + } + ); + + // Action throttle cannot be shorter than the schedule interval which is by default is 5m + ['300s', '5m', '3h', '4d'].forEach((throttle) => { + it(`it correctly transforms 'throttle = ${throttle}' and sets it as a frequency of each action`, async () => { + const actionsWithoutFrequencies = await getActionsWithoutFrequencies(supertest); + + const simpleRule = getSimpleRule(); + simpleRule.throttle = throttle; + simpleRule.actions = actionsWithoutFrequencies; + + const createdRule = await createSingleRule(simpleRule); + + const expectedRule = getSimpleRuleOutput(); + expectedRule.actions = actionsWithoutFrequencies.map((action) => ({ + ...action, + frequency: { summary: true, throttle, notifyWhen: 'onThrottleInterval' }, + })); + + const rule = removeServerGeneratedProperties(createdRule); + expect(rule).to.eql(expectedRule); + }); + }); + }); + + describe('actions with frequencies', () => { + [ + undefined, + NOTIFICATION_THROTTLE_NO_ACTIONS, + NOTIFICATION_THROTTLE_RULE, + '321s', + '6m', + '10h', + '2d', + ].forEach((throttle) => { + it(`it does not change actions frequency attributes when 'throttle' is '${throttle}'`, async () => { + const actionsWithFrequencies = await getActionsWithFrequencies(supertest); + + const simpleRule = getSimpleRule(); + simpleRule.throttle = throttle; + simpleRule.actions = actionsWithFrequencies; + + const createdRule = await createSingleRule(simpleRule); + + const expectedRule = getSimpleRuleOutput(); + expectedRule.actions = actionsWithFrequencies; + + const rule = removeServerGeneratedProperties(createdRule); + expect(rule).to.eql(expectedRule); + }); + }); + }); + + describe('some actions with frequencies', () => { + [undefined, NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE].forEach( + (throttle) => { + it(`it overrides each action's frequency attribute to default value when 'throttle' is ${throttle}`, async () => { + const someActionsWithFrequencies = await getSomeActionsWithFrequencies(supertest); + + const simpleRule = getSimpleRule(); + simpleRule.throttle = throttle; + simpleRule.actions = someActionsWithFrequencies; + + const createdRule = await createSingleRule(simpleRule); + + const expectedRule = getSimpleRuleOutput(); + expectedRule.actions = someActionsWithFrequencies.map((action) => ({ + ...action, + frequency: action.frequency ?? NOTIFICATION_DEFAULT_FREQUENCY, + })); + + const rule = removeServerGeneratedProperties(createdRule); + expect(rule).to.eql(expectedRule); + }); + } + ); + + // Action throttle cannot be shorter than the schedule interval which is by default is 5m + ['430s', '7m', '1h', '8d'].forEach((throttle) => { + it(`it correctly transforms 'throttle = ${throttle}' and overrides frequency attribute of each action`, async () => { + const someActionsWithFrequencies = await getSomeActionsWithFrequencies(supertest); + + const simpleRule = getSimpleRule(); + simpleRule.throttle = throttle; + simpleRule.actions = someActionsWithFrequencies; + + const createdRule = await createSingleRule(simpleRule); + + const expectedRule = getSimpleRuleOutput(); + expectedRule.actions = someActionsWithFrequencies.map((action) => ({ + ...action, + frequency: action.frequency ?? { + summary: true, + throttle, + notifyWhen: 'onThrottleInterval', + }, + })); + + const rule = removeServerGeneratedProperties(createdRule); + expect(rule).to.eql(expectedRule); + }); + }); + }); + }); }); }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules_bulk.ts index 6d0e79975bfd6..63d7e18367dc6 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules_bulk.ts @@ -10,7 +10,11 @@ import expect from '@kbn/expect'; import { DETECTION_ENGINE_RULES_BULK_CREATE, DETECTION_ENGINE_RULES_URL, + NOTIFICATION_DEFAULT_FREQUENCY, + NOTIFICATION_THROTTLE_NO_ACTIONS, + NOTIFICATION_THROTTLE_RULE, } from '@kbn/security-solution-plugin/common/constants'; +import { RuleCreateProps } from '@kbn/security-solution-plugin/common/detection_engine/rule_schema'; import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; @@ -27,6 +31,12 @@ import { removeServerGeneratedPropertiesIncludingRuleId, waitForRuleSuccess, } from '../../utils'; +import { + getActionsWithFrequencies, + getActionsWithoutFrequencies, + getSomeActionsWithFrequencies, +} from '../../utils/get_rule_actions'; +import { removeUUIDFromActions } from '../../utils/remove_uuid_from_actions'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -276,6 +286,141 @@ export default ({ getService }: FtrProviderContext): void => { }, ]); }); + + describe('per-action frequencies', () => { + const bulkCreateSingleRule = async (rule: RuleCreateProps) => { + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_BULK_CREATE) + .set('kbn-xsrf', 'true') + .send([rule]) + .expect(200); + + const createdRule = body[0]; + createdRule.actions = removeUUIDFromActions(createdRule.actions); + return removeServerGeneratedPropertiesIncludingRuleId(createdRule); + }; + + describe('actions without frequencies', () => { + [undefined, NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE].forEach( + (throttle) => { + it(`it sets each action's frequency attribute to default value when 'throttle' is ${throttle}`, async () => { + const actionsWithoutFrequencies = await getActionsWithoutFrequencies(supertest); + + const simpleRule = getSimpleRuleWithoutRuleId(); + simpleRule.throttle = throttle; + simpleRule.actions = actionsWithoutFrequencies; + + const createdRule = await bulkCreateSingleRule(simpleRule); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.actions = actionsWithoutFrequencies.map((action) => ({ + ...action, + frequency: NOTIFICATION_DEFAULT_FREQUENCY, + })); + + expect(createdRule).to.eql(expectedRule); + }); + } + ); + + // Action throttle cannot be shorter than the schedule interval which is by default is 5m + ['300s', '5m', '3h', '4d'].forEach((throttle) => { + it(`it correctly transforms 'throttle = ${throttle}' and sets it as a frequency of each action`, async () => { + const actionsWithoutFrequencies = await getActionsWithoutFrequencies(supertest); + + const simpleRule = getSimpleRuleWithoutRuleId(); + simpleRule.throttle = throttle; + simpleRule.actions = actionsWithoutFrequencies; + + const createdRule = await bulkCreateSingleRule(simpleRule); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.actions = actionsWithoutFrequencies.map((action) => ({ + ...action, + frequency: { summary: true, throttle, notifyWhen: 'onThrottleInterval' }, + })); + + expect(createdRule).to.eql(expectedRule); + }); + }); + }); + + describe('actions with frequencies', () => { + [ + undefined, + NOTIFICATION_THROTTLE_NO_ACTIONS, + NOTIFICATION_THROTTLE_RULE, + '321s', + '6m', + '10h', + '2d', + ].forEach((throttle) => { + it(`it does not change actions frequency attributes when 'throttle' is '${throttle}'`, async () => { + const actionsWithFrequencies = await getActionsWithFrequencies(supertest); + + const simpleRule = getSimpleRuleWithoutRuleId(); + simpleRule.throttle = throttle; + simpleRule.actions = actionsWithFrequencies; + + const createdRule = await bulkCreateSingleRule(simpleRule); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.actions = actionsWithFrequencies; + + expect(createdRule).to.eql(expectedRule); + }); + }); + }); + + describe('some actions with frequencies', () => { + [undefined, NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE].forEach( + (throttle) => { + it(`it overrides each action's frequency attribute to default value when 'throttle' is ${throttle}`, async () => { + const someActionsWithFrequencies = await getSomeActionsWithFrequencies(supertest); + + const simpleRule = getSimpleRuleWithoutRuleId(); + simpleRule.throttle = throttle; + simpleRule.actions = someActionsWithFrequencies; + + const createdRule = await bulkCreateSingleRule(simpleRule); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.actions = someActionsWithFrequencies.map((action) => ({ + ...action, + frequency: action.frequency ?? NOTIFICATION_DEFAULT_FREQUENCY, + })); + + expect(createdRule).to.eql(expectedRule); + }); + } + ); + + // Action throttle cannot be shorter than the schedule interval which is by default is 5m + ['430s', '7m', '1h', '8d'].forEach((throttle) => { + it(`it correctly transforms 'throttle = ${throttle}' and overrides frequency attribute of each action`, async () => { + const someActionsWithFrequencies = await getSomeActionsWithFrequencies(supertest); + + const simpleRule = getSimpleRuleWithoutRuleId(); + simpleRule.throttle = throttle; + simpleRule.actions = someActionsWithFrequencies; + + const createdRule = await bulkCreateSingleRule(simpleRule); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.actions = someActionsWithFrequencies.map((action) => ({ + ...action, + frequency: action.frequency ?? { + summary: true, + throttle, + notifyWhen: 'onThrottleInterval', + }, + })); + + expect(createdRule).to.eql(expectedRule); + }); + }); + }); + }); }); }); }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/delete_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/delete_rules.ts index a394bef16cd22..e029c13aff8e5 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/delete_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/delete_rules.ts @@ -176,6 +176,7 @@ export default ({ getService }: FtrProviderContext): void => { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/delete_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/delete_rules_bulk.ts index fa83ceb9a19b1..8e0ad07a782c3 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/delete_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/delete_rules_bulk.ts @@ -309,6 +309,7 @@ export default ({ getService }: FtrProviderContext): void => { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]); }); @@ -357,6 +358,7 @@ export default ({ getService }: FtrProviderContext): void => { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]); expect(body[1].actions).to.eql([ @@ -368,6 +370,7 @@ export default ({ getService }: FtrProviderContext): void => { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/export_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/export_rules.ts index c9cb430e144ba..72fb3631ac0c3 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/export_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/export_rules.ts @@ -8,6 +8,7 @@ import expect from 'expect'; import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; +import { RuleResponse } from '@kbn/security-solution-plugin/common/detection_engine/rule_schema'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { binaryToString, @@ -208,10 +209,17 @@ export default ({ getService }: FtrProviderContext): void => { const outputRule1: ReturnType = { ...getSimpleRuleOutput('rule-1'), actions: [ - { ...action1, uuid: firstRule.actions[0].uuid }, - { ...action2, uuid: firstRule.actions[1].uuid }, + { + ...action1, + uuid: firstRule.actions[0].uuid, + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, + }, + { + ...action2, + uuid: firstRule.actions[1].uuid, + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, + }, ], - throttle: 'rule', }; expect(firstRule).toEqual(outputRule1); }); @@ -258,13 +266,23 @@ export default ({ getService }: FtrProviderContext): void => { const outputRule1: ReturnType = { ...getSimpleRuleOutput('rule-2'), - actions: [{ ...action, uuid: firstRule.actions[0].uuid }], - throttle: 'rule', + actions: [ + { + ...action, + uuid: firstRule.actions[0].uuid, + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, + }, + ], }; const outputRule2: ReturnType = { ...getSimpleRuleOutput('rule-1'), - actions: [{ ...action, uuid: secondRule.actions[0].uuid }], - throttle: 'rule', + actions: [ + { + ...action, + uuid: secondRule.actions[0].uuid, + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, + }, + ], }; expect(firstRule).toEqual(outputRule1); expect(secondRule).toEqual(outputRule2); @@ -437,9 +455,9 @@ export default ({ getService }: FtrProviderContext): void => { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ], - throttle: '1h', }; const firstRuleParsed = JSON.parse(body.toString().split(/\n/)[0]); const firstRule = removeServerGeneratedProperties(firstRuleParsed); @@ -514,6 +532,7 @@ export default ({ getService }: FtrProviderContext): void => { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, { group: 'default', @@ -523,9 +542,9 @@ export default ({ getService }: FtrProviderContext): void => { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ], - throttle: '1h', }; const firstRuleParsed = JSON.parse(body.toString().split(/\n/)[0]); const firstRule = removeServerGeneratedProperties(firstRuleParsed); @@ -631,6 +650,7 @@ export default ({ getService }: FtrProviderContext): void => { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, { group: 'default', @@ -640,9 +660,9 @@ export default ({ getService }: FtrProviderContext): void => { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ], - throttle: '1h', }; const outputRule2: ReturnType = { @@ -656,6 +676,7 @@ export default ({ getService }: FtrProviderContext): void => { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, { group: 'default', @@ -665,9 +686,9 @@ export default ({ getService }: FtrProviderContext): void => { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ], - throttle: '1h', }; const firstRuleParsed = JSON.parse(body.toString().split(/\n/)[0]); const secondRuleParsed = JSON.parse(body.toString().split(/\n/)[1]); @@ -682,7 +703,8 @@ export default ({ getService }: FtrProviderContext): void => { }); }; -function expectToMatchRuleSchema(obj: unknown): void { +function expectToMatchRuleSchema(obj: RuleResponse): void { + expect(obj.throttle).toBeUndefined(); expect(obj).toEqual({ id: expect.any(String), rule_id: expect.any(String), @@ -718,7 +740,6 @@ function expectToMatchRuleSchema(obj: unknown): void { language: expect.any(String), index: expect.arrayContaining([]), query: expect.any(String), - throttle: expect.any(String), actions: expect.arrayContaining([]), }); } diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/find_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/find_rules.ts index 7cd260697be72..348400580edd2 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/find_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/find_rules.ts @@ -126,8 +126,13 @@ export default ({ getService }: FtrProviderContext): void => { const ruleWithActions: ReturnType = { ...getSimpleRuleOutput(), - actions: [{ ...action, uuid: body.data[0].actions[0].uuid }], - throttle: 'rule', + actions: [ + { + ...action, + uuid: body.data[0].actions[0].uuid, + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, + }, + ], }; body.data = [removeServerGeneratedProperties(body.data[0])]; @@ -171,8 +176,13 @@ export default ({ getService }: FtrProviderContext): void => { const ruleWithActions: ReturnType = { ...getSimpleRuleOutput(), - actions: [{ ...action, uuid: body.data[0].actions[0].uuid }], - throttle: '1h', // <-- throttle makes this a scheduled action + actions: [ + { + ...action, + uuid: body.data[0].actions[0].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, + }, + ], }; body.data = [removeServerGeneratedProperties(body.data[0])]; @@ -239,9 +249,9 @@ export default ({ getService }: FtrProviderContext): void => { 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, action_type_id: hookAction.actionTypeId, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ], - throttle: '1h', }; body.data = [removeServerGeneratedProperties(body.data[0])]; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/legacy_actions_migrations.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/legacy_actions_migrations.ts index 6ba7871b1dbf5..9fd2542664f3b 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/legacy_actions_migrations.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/legacy_actions_migrations.ts @@ -75,8 +75,8 @@ export default ({ getService }: FtrProviderContext) => { expect(sidecarActionsSOAfterMigration.hits.hits.length).to.eql(0); expect(ruleSO?.alert.actions).to.eql([]); - expect(ruleSO?.alert.throttle).to.eql('no_actions'); - expect(ruleSO?.alert.notifyWhen).to.eql('onThrottleInterval'); + expect(ruleSO?.alert.throttle).to.eql(null); + expect(ruleSO?.alert.notifyWhen).to.eql(null); }); it('migrates legacy actions for rule with action run on every run', async () => { @@ -122,6 +122,7 @@ export default ({ getService }: FtrProviderContext) => { to: ['test@test.com'], }, uuid: ruleSO?.alert.actions[0].uuid, + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, }, { actionRef: 'action_1', @@ -133,10 +134,11 @@ export default ({ getService }: FtrProviderContext) => { to: ['test@test.com'], }, uuid: ruleSO?.alert.actions[1].uuid, + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, }, ]); - expect(ruleSO?.alert.throttle).to.eql('rule'); - expect(ruleSO?.alert.notifyWhen).to.eql('onActiveAlert'); + expect(ruleSO?.alert.throttle).to.eql(null); + expect(ruleSO?.alert.notifyWhen).to.eql(null); expect(ruleSO?.references).to.eql([ { id: 'c95cb100-b075-11ec-bb3f-1f063f8e06cf', @@ -197,6 +199,7 @@ export default ({ getService }: FtrProviderContext) => { actionRef: 'action_0', group: 'default', uuid: ruleSO?.alert.actions[0].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, { actionTypeId: '.slack', @@ -206,10 +209,11 @@ export default ({ getService }: FtrProviderContext) => { actionRef: 'action_1', group: 'default', uuid: ruleSO?.alert.actions[1].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]); - expect(ruleSO?.alert.throttle).to.eql('1h'); - expect(ruleSO?.alert.notifyWhen).to.eql('onThrottleInterval'); + expect(ruleSO?.alert.throttle).to.eql(undefined); + expect(ruleSO?.alert.notifyWhen).to.eql(null); expect(ruleSO?.references).to.eql([ { id: 'c95cb100-b075-11ec-bb3f-1f063f8e06cf', @@ -269,10 +273,11 @@ export default ({ getService }: FtrProviderContext) => { to: ['test@test.com'], }, uuid: ruleSO?.alert.actions[0].uuid, + frequency: { summary: true, throttle: '1d', notifyWhen: 'onThrottleInterval' }, }, ]); - expect(ruleSO?.alert.throttle).to.eql('1d'); - expect(ruleSO?.alert.notifyWhen).to.eql('onThrottleInterval'); + expect(ruleSO?.alert.throttle).to.eql(undefined); + expect(ruleSO?.alert.notifyWhen).to.eql(null); expect(ruleSO?.references).to.eql([ { id: 'c95cb100-b075-11ec-bb3f-1f063f8e06cf', @@ -327,10 +332,11 @@ export default ({ getService }: FtrProviderContext) => { to: ['test@test.com'], }, uuid: ruleSO?.alert.actions[0].uuid, + frequency: { summary: true, throttle: '7d', notifyWhen: 'onThrottleInterval' }, }, ]); - expect(ruleSO?.alert.throttle).to.eql('7d'); - expect(ruleSO?.alert.notifyWhen).to.eql('onThrottleInterval'); + expect(ruleSO?.alert.throttle).to.eql(undefined); + expect(ruleSO?.alert.notifyWhen).to.eql(null); expect(ruleSO?.references).to.eql([ { id: 'c95cb100-b075-11ec-bb3f-1f063f8e06cf', diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules.ts index 98b64726bbaca..ae434a50df98f 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules.ts @@ -7,7 +7,13 @@ import expect from '@kbn/expect'; -import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; +import { + DETECTION_ENGINE_RULES_URL, + NOTIFICATION_DEFAULT_FREQUENCY, + NOTIFICATION_THROTTLE_NO_ACTIONS, + NOTIFICATION_THROTTLE_RULE, +} from '@kbn/security-solution-plugin/common/constants'; +import { RuleActionArray, RuleActionThrottle } from '@kbn/securitysolution-io-ts-alerting-types'; import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { @@ -24,7 +30,14 @@ import { getSimpleMlRule, createLegacyRuleAction, getLegacyActionSO, + getSimpleRuleWithoutRuleId, } from '../../utils'; +import { + getActionsWithFrequencies, + getActionsWithoutFrequencies, + getSomeActionsWithFrequencies, +} from '../../utils/get_rule_actions'; +import { removeUUIDFromActions } from '../../utils/remove_uuid_from_actions'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { @@ -377,9 +390,9 @@ export default ({ getService }: FtrProviderContext) => { 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, uuid: bodyToCompare.actions[0].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]; - outputRule.throttle = '1h'; outputRule.revision = 1; expect(bodyToCompare).to.eql(outputRule); @@ -414,5 +427,168 @@ export default ({ getService }: FtrProviderContext) => { }); }); }); + + describe('patch per-action frequencies', () => { + const patchSingleRule = async ( + ruleId: string, + throttle: RuleActionThrottle | undefined, + actions: RuleActionArray + ) => { + const { body: patchedRule } = await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ rule_id: ruleId, throttle, actions }) + .expect(200); + + patchedRule.actions = removeUUIDFromActions(patchedRule.actions); + return removeServerGeneratedPropertiesIncludingRuleId(patchedRule); + }; + + describe('actions without frequencies', () => { + [undefined, NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE].forEach( + (throttle) => { + it(`it sets each action's frequency attribute to default value when 'throttle' is ${throttle}`, async () => { + const actionsWithoutFrequencies = await getActionsWithoutFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // patch a simple rule's `throttle` and `actions` + const patchedRule = await patchSingleRule( + createdRule.rule_id, + throttle, + actionsWithoutFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = actionsWithoutFrequencies.map((action) => ({ + ...action, + frequency: NOTIFICATION_DEFAULT_FREQUENCY, + })); + + expect(patchedRule).to.eql(expectedRule); + }); + } + ); + + // Action throttle cannot be shorter than the schedule interval which is by default is 5m + ['300s', '5m', '3h', '4d'].forEach((throttle) => { + it(`it correctly transforms 'throttle = ${throttle}' and sets it as a frequency of each action`, async () => { + const actionsWithoutFrequencies = await getActionsWithoutFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // patch a simple rule's `throttle` and `actions` + const patchedRule = await patchSingleRule( + createdRule.rule_id, + throttle, + actionsWithoutFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = actionsWithoutFrequencies.map((action) => ({ + ...action, + frequency: { summary: true, throttle, notifyWhen: 'onThrottleInterval' }, + })); + + expect(patchedRule).to.eql(expectedRule); + }); + }); + }); + + describe('actions with frequencies', () => { + [ + undefined, + NOTIFICATION_THROTTLE_NO_ACTIONS, + NOTIFICATION_THROTTLE_RULE, + '321s', + '6m', + '10h', + '2d', + ].forEach((throttle) => { + it(`it does not change actions frequency attributes when 'throttle' is '${throttle}'`, async () => { + const actionsWithFrequencies = await getActionsWithFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // patch a simple rule's `throttle` and `actions` + const patchedRule = await patchSingleRule( + createdRule.rule_id, + throttle, + actionsWithFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = actionsWithFrequencies; + + expect(patchedRule).to.eql(expectedRule); + }); + }); + }); + + describe('some actions with frequencies', () => { + [undefined, NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE].forEach( + (throttle) => { + it(`it overrides each action's frequency attribute to default value when 'throttle' is ${throttle}`, async () => { + const someActionsWithFrequencies = await getSomeActionsWithFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // patch a simple rule's `throttle` and `actions` + const patchedRule = await patchSingleRule( + createdRule.rule_id, + throttle, + someActionsWithFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = someActionsWithFrequencies.map((action) => ({ + ...action, + frequency: action.frequency ?? NOTIFICATION_DEFAULT_FREQUENCY, + })); + + expect(patchedRule).to.eql(expectedRule); + }); + } + ); + + // Action throttle cannot be shorter than the schedule interval which is by default is 5m + ['430s', '7m', '1h', '8d'].forEach((throttle) => { + it(`it correctly transforms 'throttle = ${throttle}' and overrides frequency attribute of each action`, async () => { + const someActionsWithFrequencies = await getSomeActionsWithFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // patch a simple rule's `throttle` and `actions` + const patchedRule = await patchSingleRule( + createdRule.rule_id, + throttle, + someActionsWithFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = someActionsWithFrequencies.map((action) => ({ + ...action, + frequency: action.frequency ?? { + summary: true, + throttle, + notifyWhen: 'onThrottleInterval', + }, + })); + + expect(patchedRule).to.eql(expectedRule); + }); + }); + }); + }); }); }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules_bulk.ts index 1a26b17bca42e..2d80d17f9100e 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules_bulk.ts @@ -207,9 +207,9 @@ export default ({ getService }: FtrProviderContext) => { 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, uuid: bodyToCompare.actions[0].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]; - outputRule.throttle = '1h'; outputRule.revision = 1; expect(bodyToCompare).to.eql(outputRule); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action.ts index e23db2120eb7e..185d820351459 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action.ts @@ -9,7 +9,6 @@ import expect from '@kbn/expect'; import { DETECTION_ENGINE_RULES_BULK_ACTION, DETECTION_ENGINE_RULES_URL, - NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE, } from '@kbn/security-solution-plugin/common/constants'; import type { RuleResponse } from '@kbn/security-solution-plugin/common/detection_engine/rule_schema'; @@ -172,7 +171,6 @@ export default ({ getService }: FtrProviderContext): void => { const rule = removeServerGeneratedProperties(JSON.parse(ruleJson)); expect(rule).to.eql({ ...getSimpleRuleOutput(), - throttle: 'rule', actions: [ { action_type_id: '.webhook', @@ -182,6 +180,7 @@ export default ({ getService }: FtrProviderContext): void => { body: '{"test":"a default action"}', }, uuid: rule.actions[0].uuid, + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, }, ], }); @@ -335,6 +334,7 @@ export default ({ getService }: FtrProviderContext): void => { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, uuid: ruleBody.actions[0].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]); // we want to ensure rule is executing successfully, to prevent any AAD issues related to partial update of rule SO @@ -407,6 +407,7 @@ export default ({ getService }: FtrProviderContext): void => { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, uuid: ruleBody.actions[0].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]); }); @@ -705,6 +706,7 @@ export default ({ getService }: FtrProviderContext): void => { 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, ...(uuid ? { uuid } : {}), + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]); }); @@ -1334,6 +1336,7 @@ export default ({ getService }: FtrProviderContext): void => { 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, uuid: setTagsRule.actions[0].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]); }); @@ -1618,6 +1621,7 @@ export default ({ getService }: FtrProviderContext): void => { id: webHookConnector.id, action_type_id: '.webhook', uuid: body.attributes.results.updated[0].actions[0].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]; @@ -1675,6 +1679,7 @@ export default ({ getService }: FtrProviderContext): void => { id: webHookConnector.id, action_type_id: '.webhook', uuid: body.attributes.results.updated[0].actions[0].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]; @@ -1786,6 +1791,7 @@ export default ({ getService }: FtrProviderContext): void => { id: webHookConnector.id, action_type_id: '.webhook', uuid: body.attributes.results.updated[0].actions[0].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]; @@ -1838,6 +1844,7 @@ export default ({ getService }: FtrProviderContext): void => { id: webHookConnector.id, action_type_id: '.webhook', uuid: body.attributes.results.updated[0].actions[0].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]; @@ -1892,12 +1899,17 @@ export default ({ getService }: FtrProviderContext): void => { .expect(200); const expectedRuleActions = [ - { ...defaultRuleAction, uuid: body.attributes.results.updated[0].actions[0].uuid }, + { + ...defaultRuleAction, + uuid: body.attributes.results.updated[0].actions[0].uuid, + frequency: { summary: true, throttle: '1d', notifyWhen: 'onThrottleInterval' }, + }, { ...webHookActionMock, id: webHookConnector.id, action_type_id: '.webhook', uuid: body.attributes.results.updated[0].actions[1].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]; @@ -1960,12 +1972,17 @@ export default ({ getService }: FtrProviderContext): void => { .expect(200); const expectedRuleActions = [ - { ...defaultRuleAction, uuid: body.attributes.results.updated[0].actions[0].uuid }, + { + ...defaultRuleAction, + uuid: body.attributes.results.updated[0].actions[0].uuid, + frequency: { summary: true, throttle: '1d', notifyWhen: 'onThrottleInterval' }, + }, { ...slackConnectorMockProps, id: slackConnector.id, action_type_id: '.slack', uuid: body.attributes.results.updated[0].actions[1].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]; @@ -2014,20 +2031,22 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - // Check that the updated rule is returned with the response - expect(body.attributes.results.updated[0].actions).to.eql([ - { ...defaultRuleAction, uuid: createdRule.actions[0].uuid }, - ]); + // Check that the rule is skipped and was not updated + expect(body.attributes.results.skipped[0].id).to.eql(createdRule.id); // Check that the updates have been persisted const { body: readRule } = await fetchRule(ruleId).expect(200); expect(readRule.actions).to.eql([ - { ...defaultRuleAction, uuid: createdRule.actions[0].uuid }, + { + ...defaultRuleAction, + uuid: createdRule.actions[0].uuid, + frequency: { summary: true, throttle: '1d', notifyWhen: 'onThrottleInterval' }, + }, ]); }); - it('should change throttle if actions list in payload is empty', async () => { + it('should not change throttle if actions list in payload is empty', async () => { // create a new connector const webHookConnector = await createWebHookConnector(); @@ -2063,13 +2082,14 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - // Check that the updated rule is returned with the response - expect(body.attributes.results.updated[0].throttle).to.be('1h'); + // Check that the rule is skipped and was not updated + expect(body.attributes.results.skipped[0].id).to.eql(createdRule.id); // Check that the updates have been persisted const { body: readRule } = await fetchRule(ruleId).expect(200); - expect(readRule.throttle).to.eql('1h'); + expect(readRule.throttle).to.eql(undefined); + expect(readRule.actions).to.eql(createdRule.actions); }); }); @@ -2117,6 +2137,7 @@ export default ({ getService }: FtrProviderContext): void => { id: webHookConnector.id, action_type_id: '.webhook', uuid: editedRule.actions[0].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]); // version of prebuilt rule should not change @@ -2131,6 +2152,7 @@ export default ({ getService }: FtrProviderContext): void => { id: webHookConnector.id, action_type_id: '.webhook', uuid: readRule.actions[0].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]); expect(prebuiltRule.version).to.be(readRule.version); @@ -2207,7 +2229,7 @@ export default ({ getService }: FtrProviderContext): void => { }, ]; casesForEmptyActions.forEach(({ payloadThrottle }) => { - it(`throttle is set to NOTIFICATION_THROTTLE_NO_ACTIONS, if payload throttle="${payloadThrottle}" and actions list is empty`, async () => { + it(`should not update throttle, if payload throttle="${payloadThrottle}" and actions list is empty`, async () => { const ruleId = 'ruleId'; const createdRule = await createRule(supertest, log, { ...getSimpleRule(ruleId), @@ -2230,34 +2252,32 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - // Check that the updated rule is returned with the response - expect(body.attributes.results.updated[0].throttle).to.eql( - NOTIFICATION_THROTTLE_NO_ACTIONS - ); + // Check that the rule is skipped and was not updated + expect(body.attributes.results.skipped[0].id).to.eql(createdRule.id); // Check that the updates have been persisted const { body: rule } = await fetchRule(ruleId).expect(200); - expect(rule.throttle).to.eql(NOTIFICATION_THROTTLE_NO_ACTIONS); + expect(rule.throttle).to.eql(undefined); }); }); const casesForNonEmptyActions = [ { payloadThrottle: NOTIFICATION_THROTTLE_RULE, - expectedThrottle: NOTIFICATION_THROTTLE_RULE, + expectedThrottle: undefined, }, { payloadThrottle: '1h', - expectedThrottle: '1h', + expectedThrottle: undefined, }, { payloadThrottle: '1d', - expectedThrottle: '1d', + expectedThrottle: undefined, }, { payloadThrottle: '7d', - expectedThrottle: '7d', + expectedThrottle: undefined, }, ]; [BulkActionEditType.set_rule_actions, BulkActionEditType.add_rule_actions].forEach( @@ -2295,10 +2315,23 @@ export default ({ getService }: FtrProviderContext): void => { // Check that the updated rule is returned with the response expect(body.attributes.results.updated[0].throttle).to.eql(expectedThrottle); + const expectedActions = body.attributes.results.updated[0].actions.map( + (action: any) => ({ + ...action, + frequency: { + summary: true, + throttle: payloadThrottle !== 'rule' ? payloadThrottle : null, + notifyWhen: + payloadThrottle !== 'rule' ? 'onThrottleInterval' : 'onActiveAlert', + }, + }) + ); + // Check that the updates have been persisted const { body: rule } = await fetchRule(ruleId).expect(200); expect(rule.throttle).to.eql(expectedThrottle); + expect(rule.actions).to.eql(expectedActions); }); }); } @@ -2311,11 +2344,11 @@ export default ({ getService }: FtrProviderContext): void => { const cases = [ { payload: { throttle: '1d' }, - expected: { notifyWhen: 'onThrottleInterval' }, + expected: { notifyWhen: null }, }, { payload: { throttle: NOTIFICATION_THROTTLE_RULE }, - expected: { notifyWhen: 'onActiveAlert' }, + expected: { notifyWhen: null }, }, ]; cases.forEach(({ payload, expected }) => { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/read_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/read_rules.ts index d2162e02d443e..31904342e294f 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/read_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/read_rules.ts @@ -135,8 +135,13 @@ export default ({ getService }: FtrProviderContext) => { const bodyToCompare = removeServerGeneratedProperties(body); const ruleWithActions: ReturnType = { ...getSimpleRuleOutput(), - actions: [{ ...action, uuid: bodyToCompare.actions[0].uuid }], - throttle: 'rule', + actions: [ + { + ...action, + uuid: bodyToCompare.actions[0].uuid, + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, + }, + ], }; expect(bodyToCompare).to.eql(ruleWithActions); }); @@ -174,8 +179,13 @@ export default ({ getService }: FtrProviderContext) => { const bodyToCompare = removeServerGeneratedProperties(body); const ruleWithActions: ReturnType = { ...getSimpleRuleOutput(), - actions: [{ ...action, uuid: bodyToCompare.actions[0].uuid }], - throttle: '1h', // <-- throttle makes this a scheduled action + actions: [ + { + ...action, + uuid: bodyToCompare.actions[0].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, + }, + ], }; expect(bodyToCompare).to.eql(ruleWithActions); }); @@ -236,9 +246,9 @@ export default ({ getService }: FtrProviderContext) => { 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, action_type_id: hookAction.actionTypeId, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ], - throttle: '1h', }; expect(bodyToCompare).to.eql(ruleWithActions); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/throttle.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/throttle.ts index eedfaee3ec531..4b218d07b7613 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/throttle.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/throttle.ts @@ -56,7 +56,7 @@ export default ({ getService }: FtrProviderContext) => { }); describe('creating a rule', () => { - it('When creating a new action and attaching it to a rule, the rule should have its kibana alerting "mute_all" set to "false" and notify_when set to "onActiveAlert"', async () => { + it('When creating a new action and attaching it to a rule, the rule should have its kibana alerting "mute_all" set to "false" and notify_when set to null', async () => { // create a new action const { body: hookAction } = await supertest .post('/api/actions/action') @@ -66,10 +66,16 @@ export default ({ getService }: FtrProviderContext) => { const rule = await createRule(supertest, log, getRuleWithWebHookAction(hookAction.id)); const { - body: { mute_all: muteAll, notify_when: notifyWhen }, + body: { mute_all: muteAll, notify_when: notifyWhen, actions }, } = await supertest.get(`/api/alerting/rule/${rule.id}`); expect(muteAll).to.eql(false); - expect(notifyWhen).to.eql('onActiveAlert'); + expect(actions.length).to.eql(1); + expect(actions[0].frequency).to.eql({ + summary: true, + throttle: null, + notify_when: 'onActiveAlert', + }); + expect(notifyWhen).to.eql(null); }); it('When creating throttle with "NOTIFICATION_THROTTLE_NO_ACTIONS" set and no actions, the rule should have its kibana alerting "mute_all" set to "false" and notify_when set to null', async () => { @@ -105,7 +111,7 @@ export default ({ getService }: FtrProviderContext) => { expect(notifyWhen).to.eql(null); }); - it('When creating throttle with "NOTIFICATION_THROTTLE_RULE" set and no actions, the rule should have its kibana alerting "mute_all" set to "false" and notify_when set to "onActiveAlert"', async () => { + it('When creating throttle with "NOTIFICATION_THROTTLE_RULE" set and no actions, the rule should have its kibana alerting "mute_all" set to "false" and notify_when set to null', async () => { const ruleWithThrottle: RuleCreateProps = { ...getSimpleRule(), throttle: NOTIFICATION_THROTTLE_RULE, @@ -115,7 +121,7 @@ export default ({ getService }: FtrProviderContext) => { body: { mute_all: muteAll, notify_when: notifyWhen }, } = await supertest.get(`/api/alerting/rule/${rule.id}`); expect(muteAll).to.eql(false); - expect(notifyWhen).to.eql('onActiveAlert'); + expect(notifyWhen).to.eql(null); }); // NOTE: This shows A side effect of how we do not set data on side cars anymore where the user is told they have no actions since the array is empty. @@ -125,10 +131,10 @@ export default ({ getService }: FtrProviderContext) => { throttle: NOTIFICATION_THROTTLE_RULE, }; const rule = await createRule(supertest, log, ruleWithThrottle); - expect(rule.throttle).to.eql(NOTIFICATION_THROTTLE_NO_ACTIONS); + expect(rule.throttle).to.eql(undefined); }); - it('When creating throttle with "NOTIFICATION_THROTTLE_RULE" set and actions set, the rule should have its kibana alerting "mute_all" set to "false" and notify_when set to "onActiveAlert"', async () => { + it('When creating throttle with "NOTIFICATION_THROTTLE_RULE" set and actions set, the rule should have its kibana alerting "mute_all" set to "false" and notify_when set to null', async () => { // create a new action const { body: hookAction } = await supertest .post('/api/actions/action') @@ -142,13 +148,19 @@ export default ({ getService }: FtrProviderContext) => { }; const rule = await createRule(supertest, log, ruleWithThrottle); const { - body: { mute_all: muteAll, notify_when: notifyWhen }, + body: { mute_all: muteAll, notify_when: notifyWhen, actions }, } = await supertest.get(`/api/alerting/rule/${rule.id}`); expect(muteAll).to.eql(false); - expect(notifyWhen).to.eql('onActiveAlert'); + expect(actions.length).to.eql(1); + expect(actions[0].frequency).to.eql({ + summary: true, + throttle: null, + notify_when: 'onActiveAlert', + }); + expect(notifyWhen).to.eql(null); }); - it('When creating throttle with "1h" set and no actions, the rule should have its kibana alerting "mute_all" set to "false" and notify_when set to "onThrottleInterval"', async () => { + it('When creating throttle with "1h" set and no actions, the rule should have its kibana alerting "mute_all" set to "false" and notify_when set to null', async () => { const ruleWithThrottle: RuleCreateProps = { ...getSimpleRule(), throttle: '1h', @@ -158,10 +170,10 @@ export default ({ getService }: FtrProviderContext) => { body: { mute_all: muteAll, notify_when: notifyWhen }, } = await supertest.get(`/api/alerting/rule/${rule.id}`); expect(muteAll).to.eql(false); - expect(notifyWhen).to.eql('onThrottleInterval'); + expect(notifyWhen).to.eql(null); }); - it('When creating throttle with "1h" set and actions set, the rule should have its kibana alerting "mute_all" set to "false" and notify_when set to "onThrottleInterval"', async () => { + it('When creating throttle with "1h" set and actions set, the rule should have its kibana alerting "mute_all" set to "false" and notify_when set to null', async () => { // create a new action const { body: hookAction } = await supertest .post('/api/actions/action') @@ -175,10 +187,16 @@ export default ({ getService }: FtrProviderContext) => { }; const rule = await createRule(supertest, log, ruleWithThrottle); const { - body: { mute_all: muteAll, notify_when: notifyWhen }, + body: { mute_all: muteAll, notify_when: notifyWhen, actions }, } = await supertest.get(`/api/alerting/rule/${rule.id}`); expect(muteAll).to.eql(false); - expect(notifyWhen).to.eql('onThrottleInterval'); + expect(actions.length).to.eql(1); + expect(actions[0].frequency).to.eql({ + summary: true, + throttle: '1h', + notify_when: 'onThrottleInterval', + }); + expect(notifyWhen).to.eql(null); }); }); @@ -193,7 +211,7 @@ export default ({ getService }: FtrProviderContext) => { const rule = await createRule(supertest, log, getRuleWithWebHookAction(hookAction.id)); const readRule = await getRule(supertest, log, rule.rule_id); - expect(readRule.throttle).to.eql(NOTIFICATION_THROTTLE_RULE); + expect(readRule.throttle).to.eql(undefined); }); it('When creating throttle with "NOTIFICATION_THROTTLE_NO_ACTIONS" set and no actions, we should return "NOTIFICATION_THROTTLE_NO_ACTIONS" when doing a read', async () => { @@ -203,7 +221,7 @@ export default ({ getService }: FtrProviderContext) => { }; const rule = await createRule(supertest, log, ruleWithThrottle); const readRule = await getRule(supertest, log, rule.rule_id); - expect(readRule.throttle).to.eql(NOTIFICATION_THROTTLE_NO_ACTIONS); + expect(readRule.throttle).to.eql(undefined); }); // NOTE: This shows A side effect of how we do not set data on side cars anymore where the user is told they have no actions since the array is empty. @@ -214,7 +232,7 @@ export default ({ getService }: FtrProviderContext) => { }; const rule = await createRule(supertest, log, ruleWithThrottle); const readRule = await getRule(supertest, log, rule.rule_id); - expect(readRule.throttle).to.eql(NOTIFICATION_THROTTLE_NO_ACTIONS); + expect(readRule.throttle).to.eql(undefined); }); it('When creating a new action and attaching it to a rule, if we change the alert to a "muteAll" through the kibana alerting API, we should get back "NOTIFICATION_THROTTLE_NO_ACTIONS" ', async () => { @@ -232,7 +250,7 @@ export default ({ getService }: FtrProviderContext) => { .send() .expect(204); const readRule = await getRule(supertest, log, rule.rule_id); - expect(readRule.throttle).to.eql(NOTIFICATION_THROTTLE_NO_ACTIONS); + expect(readRule.throttle).to.eql(undefined); }); }); @@ -249,7 +267,7 @@ export default ({ getService }: FtrProviderContext) => { await createRule(supertest, log, ruleWithWebHookAction); ruleWithWebHookAction.name = 'some other name'; const updated = await updateRule(supertest, log, ruleWithWebHookAction); - expect(updated.throttle).to.eql(NOTIFICATION_THROTTLE_RULE); + expect(updated.throttle).to.eql(undefined); }); it('will not change the "muteAll" or "notifyWhen" if we update some part of the rule', async () => { @@ -268,7 +286,7 @@ export default ({ getService }: FtrProviderContext) => { body: { mute_all: muteAll, notify_when: notifyWhen }, } = await supertest.get(`/api/alerting/rule/${updated.id}`); expect(muteAll).to.eql(false); - expect(notifyWhen).to.eql('onActiveAlert'); + expect(notifyWhen).to.eql(null); }); // NOTE: This shows A side effect of how we do not set data on side cars anymore where the user is told they have no actions since the array is empty. @@ -284,7 +302,7 @@ export default ({ getService }: FtrProviderContext) => { await createRule(supertest, log, ruleWithWebHookAction); ruleWithWebHookAction.actions = []; const updated = await updateRule(supertest, log, ruleWithWebHookAction); - expect(updated.throttle).to.eql(NOTIFICATION_THROTTLE_NO_ACTIONS); + expect(updated.throttle).to.eql(undefined); }); }); @@ -306,7 +324,7 @@ export default ({ getService }: FtrProviderContext) => { .send({ rule_id: rule.rule_id, name: 'some other name' }) .expect(200); const readRule = await getRule(supertest, log, rule.rule_id); - expect(readRule.throttle).to.eql(NOTIFICATION_THROTTLE_RULE); + expect(readRule.throttle).to.eql(undefined); }); it('will not change the "muteAll" or "notifyWhen" if we patch part of the rule', async () => { @@ -329,7 +347,7 @@ export default ({ getService }: FtrProviderContext) => { body: { mute_all: muteAll, notify_when: notifyWhen }, } = await supertest.get(`/api/alerting/rule/${rule.id}`); expect(muteAll).to.eql(false); - expect(notifyWhen).to.eql('onActiveAlert'); + expect(notifyWhen).to.eql(null); }); // NOTE: This shows A side effect of how we do not set data on side cars anymore where the user is told they have no actions since the array is empty. @@ -350,7 +368,7 @@ export default ({ getService }: FtrProviderContext) => { .send({ rule_id: rule.rule_id, actions: [] }) .expect(200); const readRule = await getRule(supertest, log, rule.rule_id); - expect(readRule.throttle).to.eql(NOTIFICATION_THROTTLE_NO_ACTIONS); + expect(readRule.throttle).to.eql(undefined); }); }); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules.ts index 8396f72a9bb08..82b23e8b381d1 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules.ts @@ -7,7 +7,13 @@ import expect from '@kbn/expect'; -import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; +import { + DETECTION_ENGINE_RULES_URL, + NOTIFICATION_DEFAULT_FREQUENCY, + NOTIFICATION_THROTTLE_NO_ACTIONS, + NOTIFICATION_THROTTLE_RULE, +} from '@kbn/security-solution-plugin/common/constants'; +import { RuleActionArray, RuleActionThrottle } from '@kbn/securitysolution-io-ts-alerting-types'; import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { @@ -28,7 +34,14 @@ import { createLegacyRuleAction, getThresholdRuleForSignalTesting, getLegacyActionSO, + getSimpleRuleWithoutRuleId, } from '../../utils'; +import { + getActionsWithFrequencies, + getActionsWithoutFrequencies, + getSomeActionsWithFrequencies, +} from '../../utils/get_rule_actions'; +import { removeUUIDFromActions } from '../../utils/remove_uuid_from_actions'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { @@ -185,8 +198,6 @@ export default ({ getService }: FtrProviderContext) => { outputRule.revision = 1; // Expect an empty array outputRule.actions = []; - // Expect "no_actions" - outputRule.throttle = 'no_actions'; const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body); expect(bodyToCompare).to.eql(outputRule); }); @@ -221,7 +232,7 @@ export default ({ getService }: FtrProviderContext) => { id: connector.body.id, action_type_id: connector.body.connector_type_id, params: { - message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', }, }; // update a simple rule's name @@ -229,7 +240,6 @@ export default ({ getService }: FtrProviderContext) => { updatedRule.rule_id = createRuleBody.rule_id; updatedRule.name = 'some other name'; updatedRule.actions = [action1]; - updatedRule.throttle = '1d'; delete updatedRule.id; const { body } = await supertest @@ -249,13 +259,12 @@ export default ({ getService }: FtrProviderContext) => { group: 'default', id: connector.body.id, params: { - message: - 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', }, uuid: bodyToCompare.actions![0].uuid, + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, }, ]; - outputRule.throttle = '1d'; expect(bodyToCompare).to.eql(outputRule); @@ -662,6 +671,176 @@ export default ({ getService }: FtrProviderContext) => { expect(outputRule.saved_id).to.be(undefined); }); }); + + describe('per-action frequencies', () => { + const updateSingleRule = async ( + ruleId: string, + throttle: RuleActionThrottle | undefined, + actions: RuleActionArray + ) => { + // update a simple rule's `throttle` and `actions` + const ruleToUpdate = getSimpleRuleUpdate(); + ruleToUpdate.throttle = throttle; + ruleToUpdate.actions = actions; + ruleToUpdate.id = ruleId; + delete ruleToUpdate.rule_id; + + const { body: updatedRule } = await supertest + .put(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(ruleToUpdate) + .expect(200); + + updatedRule.actions = removeUUIDFromActions(updatedRule.actions); + return removeServerGeneratedPropertiesIncludingRuleId(updatedRule); + }; + + describe('actions without frequencies', () => { + [undefined, NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE].forEach( + (throttle) => { + it(`it sets each action's frequency attribute to default value when 'throttle' is ${throttle}`, async () => { + const actionsWithoutFrequencies = await getActionsWithoutFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // update a simple rule's `throttle` and `actions` + const updatedRule = await updateSingleRule( + createdRule.id, + throttle, + actionsWithoutFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = actionsWithoutFrequencies.map((action) => ({ + ...action, + frequency: NOTIFICATION_DEFAULT_FREQUENCY, + })); + + expect(updatedRule).to.eql(expectedRule); + }); + } + ); + + // Action throttle cannot be shorter than the schedule interval which is by default is 5m + ['300s', '5m', '3h', '4d'].forEach((throttle) => { + it(`it correctly transforms 'throttle = ${throttle}' and sets it as a frequency of each action`, async () => { + const actionsWithoutFrequencies = await getActionsWithoutFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // update a simple rule's `throttle` and `actions` + const updatedRule = await updateSingleRule( + createdRule.id, + throttle, + actionsWithoutFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = actionsWithoutFrequencies.map((action) => ({ + ...action, + frequency: { summary: true, throttle, notifyWhen: 'onThrottleInterval' }, + })); + + expect(updatedRule).to.eql(expectedRule); + }); + }); + }); + + describe('actions with frequencies', () => { + [ + undefined, + NOTIFICATION_THROTTLE_NO_ACTIONS, + NOTIFICATION_THROTTLE_RULE, + '321s', + '6m', + '10h', + '2d', + ].forEach((throttle) => { + it(`it does not change actions frequency attributes when 'throttle' is '${throttle}'`, async () => { + const actionsWithFrequencies = await getActionsWithFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // update a simple rule's `throttle` and `actions` + const updatedRule = await updateSingleRule( + createdRule.id, + throttle, + actionsWithFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = actionsWithFrequencies; + + expect(updatedRule).to.eql(expectedRule); + }); + }); + }); + + describe('some actions with frequencies', () => { + [undefined, NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE].forEach( + (throttle) => { + it(`it overrides each action's frequency attribute to default value when 'throttle' is ${throttle}`, async () => { + const someActionsWithFrequencies = await getSomeActionsWithFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // update a simple rule's `throttle` and `actions` + const updatedRule = await updateSingleRule( + createdRule.id, + throttle, + someActionsWithFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = someActionsWithFrequencies.map((action) => ({ + ...action, + frequency: action.frequency ?? NOTIFICATION_DEFAULT_FREQUENCY, + })); + + expect(updatedRule).to.eql(expectedRule); + }); + } + ); + + // Action throttle cannot be shorter than the schedule interval which is by default is 5m + ['430s', '7m', '1h', '8d'].forEach((throttle) => { + it(`it correctly transforms 'throttle = ${throttle}' and overrides frequency attribute of each action`, async () => { + const someActionsWithFrequencies = await getSomeActionsWithFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // update a simple rule's `throttle` and `actions` + const updatedRule = await updateSingleRule( + createdRule.id, + throttle, + someActionsWithFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = someActionsWithFrequencies.map((action) => ({ + ...action, + frequency: action.frequency ?? { + summary: true, + throttle, + notifyWhen: 'onThrottleInterval', + }, + })); + + expect(updatedRule).to.eql(expectedRule); + }); + }); + }); + }); }); }); }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules_bulk.ts index 0c8dcacd0ebbf..5da2e8f1c10cd 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules_bulk.ts @@ -11,8 +11,12 @@ import { RuleResponse } from '@kbn/security-solution-plugin/common/detection_eng import { DETECTION_ENGINE_RULES_URL, DETECTION_ENGINE_RULES_BULK_UPDATE, + NOTIFICATION_THROTTLE_NO_ACTIONS, + NOTIFICATION_THROTTLE_RULE, + NOTIFICATION_DEFAULT_FREQUENCY, } from '@kbn/security-solution-plugin/common/constants'; import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; +import { RuleActionArray, RuleActionThrottle } from '@kbn/securitysolution-io-ts-alerting-types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { createSignalsIndex, @@ -25,7 +29,16 @@ import { getSimpleRule, createLegacyRuleAction, getLegacyActionSO, + removeServerGeneratedPropertiesIncludingRuleId, + getSimpleRuleWithoutRuleId, + getSimpleRuleOutputWithoutRuleId, } from '../../utils'; +import { removeUUIDFromActions } from '../../utils/remove_uuid_from_actions'; +import { + getActionsWithFrequencies, + getActionsWithoutFrequencies, + getSomeActionsWithFrequencies, +} from '../../utils/get_rule_actions'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { @@ -138,7 +151,7 @@ export default ({ getService }: FtrProviderContext) => { id: connector.body.id, action_type_id: connector.body.connector_type_id, params: { - message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', }, }; const [rule1, rule2] = await Promise.all([ @@ -160,12 +173,10 @@ export default ({ getService }: FtrProviderContext) => { const updatedRule1 = getSimpleRuleUpdate('rule-1'); updatedRule1.name = 'some other name'; updatedRule1.actions = [action1]; - updatedRule1.throttle = '1d'; const updatedRule2 = getSimpleRuleUpdate('rule-2'); updatedRule2.name = 'some other name'; updatedRule2.actions = [action1]; - updatedRule2.throttle = '1d'; // update both rule names const { body }: { body: RuleResponse[] } = await supertest @@ -189,13 +200,12 @@ export default ({ getService }: FtrProviderContext) => { group: 'default', id: connector.body.id, params: { - message: - 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', }, uuid: bodyToCompare.actions[0].uuid, + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, }, ]; - outputRule.throttle = '1d'; expect(bodyToCompare).to.eql(outputRule); }); @@ -247,7 +257,6 @@ export default ({ getService }: FtrProviderContext) => { outputRule.name = 'some other name'; outputRule.revision = 1; outputRule.actions = []; - outputRule.throttle = 'no_actions'; const bodyToCompare = removeServerGeneratedProperties(response); expect(bodyToCompare).to.eql(outputRule); }); @@ -603,5 +612,177 @@ export default ({ getService }: FtrProviderContext) => { ]); }); }); + + describe('bulk per-action frequencies', () => { + const bulkUpdateSingleRule = async ( + ruleId: string, + throttle: RuleActionThrottle | undefined, + actions: RuleActionArray + ) => { + // update a simple rule's `throttle` and `actions` + const ruleToUpdate = getSimpleRuleUpdate(); + ruleToUpdate.throttle = throttle; + ruleToUpdate.actions = actions; + ruleToUpdate.id = ruleId; + delete ruleToUpdate.rule_id; + + const { body } = await supertest + .put(DETECTION_ENGINE_RULES_BULK_UPDATE) + .set('kbn-xsrf', 'true') + .send([ruleToUpdate]) + .expect(200); + + const updatedRule = body[0]; + updatedRule.actions = removeUUIDFromActions(updatedRule.actions); + return removeServerGeneratedPropertiesIncludingRuleId(updatedRule); + }; + + describe('actions without frequencies', () => { + [undefined, NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE].forEach( + (throttle) => { + it(`it sets each action's frequency attribute to default value when 'throttle' is ${throttle}`, async () => { + const actionsWithoutFrequencies = await getActionsWithoutFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // update a simple rule's `throttle` and `actions` + const updatedRule = await bulkUpdateSingleRule( + createdRule.id, + throttle, + actionsWithoutFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = actionsWithoutFrequencies.map((action) => ({ + ...action, + frequency: NOTIFICATION_DEFAULT_FREQUENCY, + })); + + expect(updatedRule).to.eql(expectedRule); + }); + } + ); + + // Action throttle cannot be shorter than the schedule interval which is by default is 5m + ['300s', '5m', '3h', '4d'].forEach((throttle) => { + it(`it correctly transforms 'throttle = ${throttle}' and sets it as a frequency of each action`, async () => { + const actionsWithoutFrequencies = await getActionsWithoutFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // update a simple rule's `throttle` and `actions` + // update a simple rule's `throttle` and `actions` + const updatedRule = await bulkUpdateSingleRule( + createdRule.id, + throttle, + actionsWithoutFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = actionsWithoutFrequencies.map((action) => ({ + ...action, + frequency: { summary: true, throttle, notifyWhen: 'onThrottleInterval' }, + })); + + expect(updatedRule).to.eql(expectedRule); + }); + }); + }); + + describe('actions with frequencies', () => { + [ + undefined, + NOTIFICATION_THROTTLE_NO_ACTIONS, + NOTIFICATION_THROTTLE_RULE, + '321s', + '6m', + '10h', + '2d', + ].forEach((throttle) => { + it(`it does not change actions frequency attributes when 'throttle' is '${throttle}'`, async () => { + const actionsWithFrequencies = await getActionsWithFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // update a simple rule's `throttle` and `actions` + const updatedRule = await bulkUpdateSingleRule( + createdRule.id, + throttle, + actionsWithFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = actionsWithFrequencies; + + expect(updatedRule).to.eql(expectedRule); + }); + }); + }); + + describe('some actions with frequencies', () => { + [undefined, NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE].forEach( + (throttle) => { + it(`it overrides each action's frequency attribute to default value when 'throttle' is ${throttle}`, async () => { + const someActionsWithFrequencies = await getSomeActionsWithFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // update a simple rule's `throttle` and `actions` + const updatedRule = await bulkUpdateSingleRule( + createdRule.id, + throttle, + someActionsWithFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = someActionsWithFrequencies.map((action) => ({ + ...action, + frequency: action.frequency ?? NOTIFICATION_DEFAULT_FREQUENCY, + })); + + expect(updatedRule).to.eql(expectedRule); + }); + } + ); + + // Action throttle cannot be shorter than the schedule interval which is by default is 5m + ['430s', '7m', '1h', '8d'].forEach((throttle) => { + it(`it correctly transforms 'throttle = ${throttle}' and overrides frequency attribute of each action`, async () => { + const someActionsWithFrequencies = await getSomeActionsWithFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // update a simple rule's `throttle` and `actions` + const updatedRule = await bulkUpdateSingleRule( + createdRule.id, + throttle, + someActionsWithFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = someActionsWithFrequencies.map((action) => ({ + ...action, + frequency: action.frequency ?? { + summary: true, + throttle, + notifyWhen: 'onThrottleInterval', + }, + })); + + expect(updatedRule).to.eql(expectedRule); + }); + }); + }); + }); }); }; diff --git a/x-pack/test/detection_engine_api_integration/utils/get_complex_rule_output.ts b/x-pack/test/detection_engine_api_integration/utils/get_complex_rule_output.ts index af66123014383..31b8c4571e2f5 100644 --- a/x-pack/test/detection_engine_api_integration/utils/get_complex_rule_output.ts +++ b/x-pack/test/detection_engine_api_integration/utils/get_complex_rule_output.ts @@ -92,7 +92,6 @@ export const getComplexRuleOutput = (ruleId = 'rule-1'): Partial = 'http://www.example.com/some-article-about-attack', 'Some plain text string here explaining why this is a valid thing to look out for', ], - throttle: 'no_actions', timeline_id: 'timeline_id', timeline_title: 'timeline_title', updated_by: 'elastic', diff --git a/x-pack/test/detection_engine_api_integration/utils/get_rule_actions.ts b/x-pack/test/detection_engine_api_integration/utils/get_rule_actions.ts new file mode 100644 index 0000000000000..519cd9136c604 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/utils/get_rule_actions.ts @@ -0,0 +1,96 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type SuperTest from 'supertest'; + +import { RuleActionArray } from '@kbn/securitysolution-io-ts-alerting-types'; +import { getSlackAction } from './get_slack_action'; +import { getWebHookAction } from './get_web_hook_action'; + +const createConnector = async ( + supertest: SuperTest.SuperTest, + payload: Record +) => + (await supertest.post('/api/actions/action').set('kbn-xsrf', 'true').send(payload).expect(200)) + .body; +const createWebHookConnector = (supertest: SuperTest.SuperTest) => + createConnector(supertest, getWebHookAction()); +const createSlackConnector = (supertest: SuperTest.SuperTest) => + createConnector(supertest, getSlackAction()); + +export const getActionsWithoutFrequencies = async ( + supertest: SuperTest.SuperTest +): Promise => { + const webHookAction = await createWebHookConnector(supertest); + const slackConnector = await createSlackConnector(supertest); + return [ + { + group: 'default', + id: webHookAction.id, + action_type_id: '.webhook', + params: { message: 'Email message' }, + }, + { + group: 'default', + id: slackConnector.id, + action_type_id: '.slack', + params: { message: 'Slack message' }, + }, + ]; +}; + +export const getActionsWithFrequencies = async ( + supertest: SuperTest.SuperTest +): Promise => { + const webHookAction = await createWebHookConnector(supertest); + const slackConnector = await createSlackConnector(supertest); + return [ + { + group: 'default', + id: webHookAction.id, + action_type_id: '.webhook', + params: { message: 'Email message' }, + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, + }, + { + group: 'default', + id: slackConnector.id, + action_type_id: '.slack', + params: { message: 'Slack message' }, + frequency: { summary: false, throttle: '3d', notifyWhen: 'onThrottleInterval' }, + }, + ]; +}; + +export const getSomeActionsWithFrequencies = async ( + supertest: SuperTest.SuperTest +): Promise => { + const webHookAction = await createWebHookConnector(supertest); + const slackConnector = await createSlackConnector(supertest); + return [ + { + group: 'default', + id: webHookAction.id, + action_type_id: '.webhook', + params: { message: 'Email message' }, + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, + }, + { + group: 'default', + id: slackConnector.id, + action_type_id: '.slack', + params: { message: 'Slack message' }, + frequency: { summary: false, throttle: '3d', notifyWhen: 'onThrottleInterval' }, + }, + { + group: 'default', + id: slackConnector.id, + action_type_id: '.slack', + params: { message: 'Slack message' }, + }, + ]; +}; diff --git a/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output.ts b/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output.ts index 9826d9a2b98b4..cdadcce39e0a8 100644 --- a/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output.ts +++ b/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output.ts @@ -40,7 +40,7 @@ export const getMockSharedResponseSchema = ( tags: [], to: 'now', threat: [], - throttle: 'no_actions', + throttle: undefined, exceptions_list: [], version: 1, revision: 0, diff --git a/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output_with_web_hook_action.ts b/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output_with_web_hook_action.ts index 25776fefd5687..7ecee679e50b3 100644 --- a/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output_with_web_hook_action.ts +++ b/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output_with_web_hook_action.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { NOTIFICATION_DEFAULT_FREQUENCY } from '@kbn/security-solution-plugin/common/constants'; import { getSimpleRuleOutput } from './get_simple_rule_output'; import { RuleWithoutServerGeneratedProperties } from './remove_server_generated_properties'; @@ -13,7 +14,6 @@ export const getSimpleRuleOutputWithWebHookAction = ( uuid: string ): RuleWithoutServerGeneratedProperties => ({ ...getSimpleRuleOutput(), - throttle: 'rule', actions: [ { action_type_id: '.webhook', @@ -23,6 +23,7 @@ export const getSimpleRuleOutputWithWebHookAction = ( body: '{}', }, uuid, + frequency: NOTIFICATION_DEFAULT_FREQUENCY, }, ], }); diff --git a/x-pack/test/detection_engine_api_integration/utils/remove_uuid_from_actions.ts b/x-pack/test/detection_engine_api_integration/utils/remove_uuid_from_actions.ts new file mode 100644 index 0000000000000..08d95bc750212 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/utils/remove_uuid_from_actions.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RuleActionArray } from '@kbn/securitysolution-io-ts-alerting-types'; + +export const removeUUIDFromActions = (actions: RuleActionArray): RuleActionArray => { + return actions.map(({ uuid, ...restOfAction }) => ({ + ...restOfAction, + })); +};