diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts index c0975925d480c..aa07dfcbba8b2 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts @@ -8,7 +8,6 @@ /* eslint-disable @typescript-eslint/naming-convention */ import { - enumeration, IsoDateString, NonEmptyString, PositiveInteger, @@ -359,75 +358,3 @@ export const privilege = t.type({ }); export type Privilege = t.TypeOf; - -export enum BulkAction { - 'enable' = 'enable', - 'disable' = 'disable', - 'export' = 'export', - 'delete' = 'delete', - 'duplicate' = 'duplicate', - 'edit' = 'edit', -} - -export const bulkAction = enumeration('BulkAction', BulkAction); - -export enum BulkActionEditType { - 'add_tags' = 'add_tags', - 'delete_tags' = 'delete_tags', - 'set_tags' = 'set_tags', - 'add_index_patterns' = 'add_index_patterns', - 'delete_index_patterns' = 'delete_index_patterns', - 'set_index_patterns' = 'set_index_patterns', - 'set_timeline' = 'set_timeline', -} - -const bulkActionEditPayloadTags = t.type({ - type: t.union([ - t.literal(BulkActionEditType.add_tags), - t.literal(BulkActionEditType.delete_tags), - t.literal(BulkActionEditType.set_tags), - ]), - value: tags, -}); - -export type BulkActionEditPayloadTags = t.TypeOf; - -const bulkActionEditPayloadIndexPatterns = t.intersection([ - t.type({ - type: t.union([ - t.literal(BulkActionEditType.add_index_patterns), - t.literal(BulkActionEditType.delete_index_patterns), - t.literal(BulkActionEditType.set_index_patterns), - ]), - value: index, - }), - t.exact(t.partial({ overwrite_data_views: t.boolean })), -]); - -export type BulkActionEditPayloadIndexPatterns = t.TypeOf< - typeof bulkActionEditPayloadIndexPatterns ->; - -const bulkActionEditPayloadTimeline = t.type({ - type: t.literal(BulkActionEditType.set_timeline), - value: t.type({ - timeline_id, - timeline_title, - }), -}); - -export type BulkActionEditPayloadTimeline = t.TypeOf; - -export const bulkActionEditPayload = t.union([ - bulkActionEditPayloadTags, - bulkActionEditPayloadIndexPatterns, - bulkActionEditPayloadTimeline, -]); - -export type BulkActionEditPayload = t.TypeOf; - -export type BulkActionEditForRuleAttributes = BulkActionEditPayloadTags; - -export type BulkActionEditForRuleParams = - | BulkActionEditPayloadIndexPatterns - | BulkActionEditPayloadTimeline; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/perform_bulk_action_schema.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/perform_bulk_action_schema.mock.ts index b3988568a4765..1768acc8dfbfa 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/perform_bulk_action_schema.mock.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/perform_bulk_action_schema.mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { BulkAction, BulkActionEditType } from '../common/schemas'; +import { BulkAction, BulkActionEditType } from './perform_bulk_action_schema'; import type { PerformBulkActionSchema } from './perform_bulk_action_schema'; export const getPerformBulkActionSchemaMock = (): PerformBulkActionSchema => ({ diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/perform_bulk_action_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/perform_bulk_action_schema.test.ts index a1f6122a2ef35..f6de8a29cc90d 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/perform_bulk_action_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/perform_bulk_action_schema.test.ts @@ -6,10 +6,13 @@ */ import type { PerformBulkActionSchema } from './perform_bulk_action_schema'; -import { performBulkActionSchema } from './perform_bulk_action_schema'; +import { + performBulkActionSchema, + BulkAction, + BulkActionEditType, +} from './perform_bulk_action_schema'; import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { left } from 'fp-ts/lib/Either'; -import { BulkAction, BulkActionEditType } from '../common/schemas'; const retrieveValidationMessage = (payload: unknown) => { const decoded = performBulkActionSchema.decode(payload); @@ -343,12 +346,12 @@ describe('perform_bulk_action_schema', () => { const message = retrieveValidationMessage(payload); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "edit" supplied to "action"', - 'Invalid value "set_timeline" supplied to "edit,type"', - 'Invalid value "{"timeline_title":"Test timeline title"}" supplied to "edit,value"', - 'Invalid value "undefined" supplied to "edit,value,timeline_id"', - ]); + expect(getPaths(left(message.errors))).toEqual( + expect.arrayContaining([ + 'Invalid value "{"timeline_title":"Test timeline title"}" supplied to "edit,value"', + 'Invalid value "undefined" supplied to "edit,value,timeline_id"', + ]) + ); expect(message.schema).toEqual({}); }); @@ -373,5 +376,163 @@ describe('perform_bulk_action_schema', () => { expect(message.schema).toEqual(payload); }); }); + + describe('rule actions', () => { + test('invalid request: invalid rule actions payload', () => { + const payload = { + query: 'name: test', + action: BulkAction.edit, + [BulkAction.edit]: [{ type: BulkActionEditType.add_rule_actions, value: [] }], + }; + + const message = retrieveValidationMessage(payload); + + expect(getPaths(left(message.errors))).toEqual( + expect.arrayContaining(['Invalid value "[]" supplied to "edit,value"']) + ); + expect(message.schema).toEqual({}); + }); + + test('invalid request: missing throttle in payload', () => { + const payload = { + query: 'name: test', + action: BulkAction.edit, + [BulkAction.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', + action: BulkAction.edit, + [BulkAction.edit]: [ + { + type: BulkActionEditType.add_rule_actions, + value: { + throttle: '1h', + }, + }, + ], + }; + + const message = retrieveValidationMessage(payload); + + expect(getPaths(left(message.errors))).toEqual( + expect.arrayContaining(['Invalid value "undefined" supplied to "edit,value,actions"']) + ); + expect(message.schema).toEqual({}); + }); + + test('invalid request: invalid action_type_id property in actions array', () => { + const payload = { + query: 'name: test', + action: BulkAction.edit, + [BulkAction.edit]: [ + { + type: BulkActionEditType.add_rule_actions, + value: { + throttle: '1h', + actions: [ + { + action_type_id: '.webhook', + group: 'default', + id: '458a50e0-1a28-11ed-9098-47fd8e1f3345', + params: { + body: { + rule_id: '{{rule.id}}', + }, + }, + }, + ], + }, + }, + ], + }; + + const message = retrieveValidationMessage(payload); + expect(getPaths(left(message.errors))).toEqual( + expect.arrayContaining(['invalid keys "action_type_id"']) + ); + expect(message.schema).toEqual({}); + }); + + test('valid request: add_rule_actions edit action', () => { + const payload: PerformBulkActionSchema = { + query: 'name: test', + action: BulkAction.edit, + [BulkAction.edit]: [ + { + type: BulkActionEditType.add_rule_actions, + value: { + throttle: '1h', + actions: [ + { + group: 'default', + id: '458a50e0-1a28-11ed-9098-47fd8e1f3345', + params: { + body: { + rule_id: '{{rule.id}}', + }, + }, + }, + ], + }, + }, + ], + }; + + const message = retrieveValidationMessage(payload); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('valid request: set_rule_actions edit action', () => { + const payload: PerformBulkActionSchema = { + query: 'name: test', + action: BulkAction.edit, + [BulkAction.edit]: [ + { + type: BulkActionEditType.set_rule_actions, + value: { + throttle: '1h', + actions: [ + { + group: 'default', + id: '458a50e0-1a28-11ed-9098-47fd8e1f3345', + params: { + documents: [ + { + rule_id: '{{rule.id}}', + }, + ], + }, + }, + ], + }, + }, + ], + }; + + const message = retrieveValidationMessage(payload); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + }); }); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/perform_bulk_action_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/perform_bulk_action_schema.ts index fa33e75e236fd..58675bf3c0d99 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/perform_bulk_action_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/perform_bulk_action_schema.ts @@ -6,8 +6,124 @@ */ import * as t from 'io-ts'; -import { NonEmptyArray } from '@kbn/securitysolution-io-ts-types'; -import { BulkAction, queryOrUndefined, bulkActionEditPayload } from '../common/schemas'; +import { NonEmptyArray, enumeration } from '@kbn/securitysolution-io-ts-types'; + +import { + throttle, + action_group as actionGroup, + action_params as actionParams, + action_id as actionId, +} from '@kbn/securitysolution-io-ts-alerting-types'; + +import { queryOrUndefined, tags, index, timeline_id, timeline_title } from '../common/schemas'; + +export enum BulkAction { + 'enable' = 'enable', + 'disable' = 'disable', + 'export' = 'export', + 'delete' = 'delete', + 'duplicate' = 'duplicate', + 'edit' = 'edit', +} + +export const bulkAction = enumeration('BulkAction', BulkAction); + +export enum BulkActionEditType { + 'add_tags' = 'add_tags', + 'delete_tags' = 'delete_tags', + 'set_tags' = 'set_tags', + 'add_index_patterns' = 'add_index_patterns', + 'delete_index_patterns' = 'delete_index_patterns', + 'set_index_patterns' = 'set_index_patterns', + 'set_timeline' = 'set_timeline', + 'add_rule_actions' = 'add_rule_actions', + 'set_rule_actions' = 'set_rule_actions', +} + +const bulkActionEditPayloadTags = t.type({ + type: t.union([ + t.literal(BulkActionEditType.add_tags), + t.literal(BulkActionEditType.delete_tags), + t.literal(BulkActionEditType.set_tags), + ]), + value: tags, +}); + +export type BulkActionEditPayloadTags = t.TypeOf; + +const bulkActionEditPayloadIndexPatterns = t.intersection([ + t.type({ + type: t.union([ + t.literal(BulkActionEditType.add_index_patterns), + t.literal(BulkActionEditType.delete_index_patterns), + t.literal(BulkActionEditType.set_index_patterns), + ]), + value: index, + }), + t.exact(t.partial({ overwrite_data_views: t.boolean })), +]); + +export type BulkActionEditPayloadIndexPatterns = t.TypeOf< + typeof bulkActionEditPayloadIndexPatterns +>; + +const bulkActionEditPayloadTimeline = t.type({ + type: t.literal(BulkActionEditType.set_timeline), + value: t.type({ + timeline_id, + timeline_title, + }), +}); + +export type BulkActionEditPayloadTimeline = t.TypeOf; + +/** + * per rulesClient.bulkEdit rules actions operation contract (x-pack/plugins/alerting/server/rules_client/rules_client.ts) + * normalized rule action object is expected (NormalizedAlertAction) as value for the edit operation + */ +const normalizedRuleAction = t.exact( + t.type({ + group: actionGroup, + id: actionId, + params: actionParams, + }) +); + +const bulkActionEditPayloadRuleActions = t.type({ + type: t.union([ + t.literal(BulkActionEditType.add_rule_actions), + t.literal(BulkActionEditType.set_rule_actions), + ]), + value: t.type({ + throttle, + actions: t.array(normalizedRuleAction), + }), +}); + +export type BulkActionEditPayloadRuleActions = t.TypeOf; + +export const bulkActionEditPayload = t.union([ + bulkActionEditPayloadTags, + bulkActionEditPayloadIndexPatterns, + bulkActionEditPayloadTimeline, + bulkActionEditPayloadRuleActions, +]); + +export type BulkActionEditPayload = t.TypeOf; + +/** + * actions that modify rules attributes + */ +export type BulkActionEditForRuleAttributes = + | BulkActionEditPayloadTags + | BulkActionEditPayloadRuleActions; + +/** + * actions that modify rules params + */ +export type BulkActionEditForRuleParams = + | BulkActionEditPayloadIndexPatterns + | BulkActionEditPayloadTimeline; export const performBulkActionSchema = t.intersection([ t.exact( diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.tsx index b8e423827edce..beb8e8365d74e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.tsx @@ -16,7 +16,7 @@ import { noop } from 'lodash'; import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; import { APP_UI_ID, SecurityPageName } from '../../../../../common/constants'; -import { BulkAction } from '../../../../../common/detection_engine/schemas/common'; +import { BulkAction } from '../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; import { getRulesUrl } from '../../../../common/components/link_to/redirect_to_detection_engine'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { useBoolState } from '../../../../common/hooks/use_bool_state'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.tsx index 574f4ec166193..6c924a68da4a1 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.tsx @@ -10,7 +10,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiSwitch } from '@elasti import { noop } from 'lodash'; import React, { useCallback, useMemo, useState } from 'react'; import styled from 'styled-components'; -import { BulkAction } from '../../../../../common/detection_engine/schemas/common'; +import { BulkAction } from '../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { SINGLE_RULE_ACTIONS } from '../../../../common/lib/apm/user_actions'; import { useStartTransaction } from '../../../../common/lib/apm/use_start_transaction'; 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 new file mode 100644 index 0000000000000..57a7dd50a370e --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/get_schema.ts @@ -0,0 +1,45 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +import type { ActionTypeRegistryContract } from '@kbn/triggers-actions-ui-plugin/public'; +import { validateRuleActionsField } from '../../../containers/detection_engine/rules/validate_rule_actions_field'; + +import type { FormSchema } from '../../../../shared_imports'; +import type { ActionsStepRule } from '../../../pages/detection_engine/rules/types'; + +export const getSchema = ({ + actionTypeRegistry, +}: { + actionTypeRegistry: ActionTypeRegistryContract; +}): FormSchema => ({ + actions: { + validations: [ + { + validator: validateRuleActionsField(actionTypeRegistry), + }, + ], + }, + enabled: {}, + kibanaSiemAppUrl: {}, + throttle: { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepRuleActions.fieldThrottleLabel', + { + defaultMessage: 'Actions frequency', + } + ), + helpText: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepRuleActions.fieldThrottleHelpText', + { + defaultMessage: + 'Select when automated actions should be performed if a rule evaluates as true.', + } + ), + }, +}); 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 d8264794f1df8..b967304d57349 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 @@ -31,7 +31,7 @@ import { } from '../throttle_select_field'; import { RuleActionsField } from '../rule_actions_field'; import { useKibana } from '../../../../common/lib/kibana'; -import { getSchema } from './schema'; +import { getSchema } from './get_schema'; import * as I18n from './translations'; import { APP_UI_ID } from '../../../../../common/constants'; import { useManageCaseAction } from './use_manage_case_action'; 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 f0d3d7b7d351e..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 @@ -6,7 +6,6 @@ */ import { i18n } from '@kbn/i18n'; -import { startCase } from 'lodash/fp'; export const COMPLETE_WITHOUT_ENABLING = i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.stepScheduleRule.completeWithoutEnablingTitle', @@ -29,14 +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 INVALID_MUSTACHE_TEMPLATE = (paramKey: string) => - i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepRuleActions.invalidMustacheTemplateErrorMessage', - { - defaultMessage: '{key} is not valid mustache template', - values: { - key: startCase(paramKey), - }, - } - ); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts index 0e2f238aa527e..876a3a0a469a8 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts @@ -18,7 +18,7 @@ import { DETECTION_ENGINE_INSTALLED_INTEGRATIONS_URL, DETECTION_ENGINE_RULES_URL_FIND, } from '../../../../../common/constants'; -import type { BulkAction } from '../../../../../common/detection_engine/schemas/common'; +import type { BulkAction } from '../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; import type { FullResponseSchema, PreviewResponse, diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts index eaf9b3288dc2d..45c89c307de4e 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts @@ -25,11 +25,11 @@ import { import { RuleExecutionSummary } from '../../../../../common/detection_engine/rule_monitoring'; +import type { SortOrder } from '../../../../../common/detection_engine/schemas/common'; import type { - SortOrder, - BulkAction, BulkActionEditPayload, -} from '../../../../../common/detection_engine/schemas/common'; + BulkAction, +} from '../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; import { alias_purpose as savedObjectResolveAliasPurpose, outcome as savedObjectResolveOutcome, diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/validate_rule_actions_field/index.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/validate_rule_actions_field/index.tsx new file mode 100644 index 0000000000000..22d05080a408b --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/validate_rule_actions_field/index.tsx @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export { validateRuleActionsField } from './validate_rule_actions_field'; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/validate_rule_actions_field/translations.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/validate_rule_actions_field/translations.ts new file mode 100644 index 0000000000000..6540f7071ccd6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/validate_rule_actions_field/translations.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. + */ + +import { i18n } from '@kbn/i18n'; +import { startCase } from 'lodash/fp'; + +export const INVALID_MUSTACHE_TEMPLATE = (paramKey: string) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepRuleActions.invalidMustacheTemplateErrorMessage', + { + defaultMessage: '{key} is not valid mustache template', + values: { + key: startCase(paramKey), + }, + } + ); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/utils.test.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/validate_rule_actions_field/utils.test.ts similarity index 100% rename from x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/utils.test.ts rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/validate_rule_actions_field/utils.test.ts diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/utils.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/validate_rule_actions_field/utils.ts similarity index 100% rename from x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/utils.ts rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/validate_rule_actions_field/utils.ts diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/schema.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/validate_rule_actions_field/validate_rule_actions_field.test.tsx similarity index 96% rename from x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/schema.test.tsx rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/validate_rule_actions_field/validate_rule_actions_field.test.tsx index 58acba634311a..335d6faf631f5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/schema.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/validate_rule_actions_field/validate_rule_actions_field.test.tsx @@ -5,13 +5,13 @@ * 2.0. */ -import { validateSingleAction, validateRuleActionsField } from './schema'; +import { validateSingleAction, validateRuleActionsField } from './validate_rule_actions_field'; import { getActionTypeName, validateMustache, validateActionParams } from './utils'; import { actionTypeRegistryMock } from '@kbn/triggers-actions-ui-plugin/public/application/action_type_registry.mock'; -import type { FormHook } from '../../../../shared_imports'; +import type { FormHook } from '../../../../../shared_imports'; jest.mock('./utils'); -describe('stepRuleActions schema', () => { +describe('validate_rule_actions_field', () => { const actionTypeRegistry = actionTypeRegistryMock.create(); describe('validateSingleAction', () => { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/schema.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/validate_rule_actions_field/validate_rule_actions_field.ts similarity index 62% rename from x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/schema.tsx rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/validate_rule_actions_field/validate_rule_actions_field.ts index efc34c7b4d13d..18aa758d0a499 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/schema.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/validate_rule_actions_field/validate_rule_actions_field.ts @@ -7,19 +7,11 @@ /* istanbul ignore file */ -import { i18n } from '@kbn/i18n'; - import type { RuleAction, ActionTypeRegistryContract, } from '@kbn/triggers-actions-ui-plugin/public'; -import type { - FormSchema, - ValidationFunc, - ERROR_CODE, - ValidationError, -} from '../../../../shared_imports'; -import type { ActionsStepRule } from '../../../pages/detection_engine/rules/types'; +import type { ValidationFunc, ERROR_CODE, ValidationError } from '../../../../../shared_imports'; import { getActionTypeName, validateMustache, validateActionParams } from './utils'; export const validateSingleAction = async ( @@ -59,34 +51,3 @@ export const validateRuleActionsField = }; } }; - -export const getSchema = ({ - actionTypeRegistry, -}: { - actionTypeRegistry: ActionTypeRegistryContract; -}): FormSchema => ({ - actions: { - validations: [ - { - validator: validateRuleActionsField(actionTypeRegistry), - }, - ], - }, - enabled: {}, - kibanaSiemAppUrl: {}, - throttle: { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepRuleActions.fieldThrottleLabel', - { - defaultMessage: 'Actions frequency', - } - ), - helpText: i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepRuleActions.fieldThrottleHelpText', - { - defaultMessage: - 'Select when automated actions should be performed if a rule evaluates as true.', - } - ), - }, -}); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/actions.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/actions.ts index 28d3f4856579f..d80835209010f 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/actions.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/actions.ts @@ -7,8 +7,8 @@ import type { NavigateToAppOptions } from '@kbn/core/public'; import { APP_UI_ID } from '../../../../../../common/constants'; -import type { BulkActionEditPayload } from '../../../../../../common/detection_engine/schemas/common/schemas'; -import { BulkAction } from '../../../../../../common/detection_engine/schemas/common/schemas'; +import type { BulkActionEditPayload } from '../../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; +import { BulkAction } from '../../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; import type { HTTPError } from '../../../../../../common/detection_engine/types'; import { SecurityPageName } from '../../../../../app/types'; import { getEditRuleUrl } from '../../../../../common/components/link_to/redirect_to_detection_engine'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/bulk_action_dry_run_confirmation.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/bulk_action_dry_run_confirmation.tsx index 33d325b837d33..9207e0813ab70 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/bulk_action_dry_run_confirmation.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/bulk_action_dry_run_confirmation.tsx @@ -10,7 +10,7 @@ import { EuiConfirmModal } from '@elastic/eui'; import * as i18n from '../../translations'; import { BulkActionRuleErrorsList } from './bulk_action_rule_errors_list'; -import { BulkAction } from '../../../../../../../common/detection_engine/schemas/common/schemas'; +import { BulkAction } from '../../../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; import { assertUnreachable } from '../../../../../../../common/utility_types'; import type { BulkActionForConfirmation, DryRunResult } from './types'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/bulk_action_rule_errors_list.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/bulk_action_rule_errors_list.test.tsx index 82b78c319c056..987b244dd9fc6 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/bulk_action_rule_errors_list.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/bulk_action_rule_errors_list.test.tsx @@ -13,7 +13,7 @@ import { render, screen } from '@testing-library/react'; import { BulkActionRuleErrorsList } from './bulk_action_rule_errors_list'; import { BulkActionsDryRunErrCode } from '../../../../../../../common/constants'; import type { DryRunResult } from './types'; -import { BulkAction } from '../../../../../../../common/detection_engine/schemas/common/schemas'; +import { BulkAction } from '../../../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; const Wrapper: FC = ({ children }) => { return ( diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/bulk_action_rule_errors_list.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/bulk_action_rule_errors_list.tsx index 7f5b3ea3f74ee..8f0275000a4ef 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/bulk_action_rule_errors_list.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/bulk_action_rule_errors_list.tsx @@ -10,7 +10,7 @@ import { EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { BulkActionsDryRunErrCode } from '../../../../../../../common/constants'; -import { BulkAction } from '../../../../../../../common/detection_engine/schemas/common/schemas'; +import { BulkAction } from '../../../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; import type { DryRunResult, BulkActionForConfirmation } from './types'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/bulk_edit_flyout.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/bulk_edit_flyout.tsx index 4a4d33c358b0e..db96a63f6245e 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/bulk_edit_flyout.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/bulk_edit_flyout.tsx @@ -7,12 +7,13 @@ import React from 'react'; -import type { BulkActionEditPayload } from '../../../../../../../common/detection_engine/schemas/common/schemas'; -import { BulkActionEditType } from '../../../../../../../common/detection_engine/schemas/common/schemas'; +import type { BulkActionEditPayload } from '../../../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; +import { BulkActionEditType } from '../../../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; import { IndexPatternsForm } from './forms/index_patterns_form'; import { TagsForm } from './forms/tags_form'; import { TimelineTemplateForm } from './forms/timeline_template_form'; +import { RuleActionsForm } from './forms/rule_actions_form'; interface BulkEditFlyoutProps { onClose: () => void; @@ -37,6 +38,10 @@ const BulkEditFlyoutComponent = ({ editAction, tags, ...props }: BulkEditFlyoutP case BulkActionEditType.set_timeline: return ; + case BulkActionEditType.add_rule_actions: + case BulkActionEditType.set_rule_actions: + return ; + default: return null; } diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/forms/bulk_edit_form_wrapper.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/forms/bulk_edit_form_wrapper.tsx index 26a555b7de335..a4fafd8d21bfd 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/forms/bulk_edit_form_wrapper.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/forms/bulk_edit_form_wrapper.tsx @@ -7,6 +7,7 @@ import type { FC } from 'react'; import React from 'react'; +import type { EuiFlyoutSize } from '@elastic/eui'; import { useGeneratedHtmlId, EuiFlyout, @@ -32,6 +33,7 @@ interface BulkEditFormWrapperProps { children: React.ReactNode; onClose: () => void; onSubmit: () => void; + flyoutSize?: EuiFlyoutSize; } const BulkEditFormWrapperComponent: FC = ({ @@ -41,6 +43,7 @@ const BulkEditFormWrapperComponent: FC = ({ children, onClose, onSubmit, + flyoutSize = 's', }) => { const simpleFlyoutTitleId = useGeneratedHtmlId({ prefix: 'RulesBulkEditForm', @@ -48,7 +51,7 @@ const BulkEditFormWrapperComponent: FC = ({ const { isValid } = form; return ( - +

{title}

diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/forms/index_patterns_form.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/forms/index_patterns_form.tsx index 2ce632be3ef3c..adb19e397027b 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/forms/index_patterns_form.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/forms/index_patterns_form.tsx @@ -14,8 +14,8 @@ import * as i18n from '../../../translations'; import { DEFAULT_INDEX_KEY } from '../../../../../../../../common/constants'; import { useKibana } from '../../../../../../../common/lib/kibana'; -import type { BulkActionEditPayload } from '../../../../../../../../common/detection_engine/schemas/common/schemas'; -import { BulkActionEditType } from '../../../../../../../../common/detection_engine/schemas/common/schemas'; +import { BulkActionEditType } from '../../../../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; +import type { BulkActionEditPayload } from '../../../../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; import type { FormSchema } from '../../../../../../../shared_imports'; import { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/forms/rule_actions_form.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/forms/rule_actions_form.tsx new file mode 100644 index 0000000000000..8a9e52c9d5528 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/forms/rule_actions_form.tsx @@ -0,0 +1,244 @@ +/* + * 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 React, { useCallback, useMemo } from 'react'; +import { EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import type { + RuleAction, + ActionTypeRegistryContract, +} from '@kbn/triggers-actions-ui-plugin/public'; +import type { FormSchema } from '../../../../../../../shared_imports'; +import { + useForm, + UseField, + FIELD_TYPES, + useFormData, + getUseField, + Field, +} from '../../../../../../../shared_imports'; +import type { BulkActionEditPayload } from '../../../../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; +import { BulkActionEditType } from '../../../../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; +import { NOTIFICATION_THROTTLE_NO_ACTIONS } from '../../../../../../../../common/constants'; + +import { BulkEditFormWrapper } from './bulk_edit_form_wrapper'; +import { bulkAddRuleActions as i18n } from '../translations'; + +import { useKibana } from '../../../../../../../common/lib/kibana'; + +import { + ThrottleSelectField, + THROTTLE_OPTIONS, +} from '../../../../../../components/rules/throttle_select_field'; +import { getAllActionMessageParams } from '../../../helpers'; + +import { RuleActionsField } from '../../../../../../components/rules/rule_actions_field'; +import { validateRuleActionsField } from '../../../../../../containers/detection_engine/rules/validate_rule_actions_field'; + +const CommonUseField = getUseField({ component: Field }); + +export interface RuleActionsFormData { + throttle: string; + actions: RuleAction[]; + overwrite: boolean; +} + +const getFormSchema = ( + actionTypeRegistry: ActionTypeRegistryContract +): FormSchema => ({ + throttle: { + label: i18n.THROTTLE_LABEL, + helpText: i18n.THROTTLE_HELP_TEXT, + }, + actions: { + validations: [ + { + validator: validateRuleActionsField(actionTypeRegistry), + }, + ], + }, + overwrite: { + type: FIELD_TYPES.CHECKBOX, + label: i18n.OVERWRITE_LABEL, + }, +}); + +const defaultFormData: RuleActionsFormData = { + throttle: NOTIFICATION_THROTTLE_NO_ACTIONS, + actions: [], + overwrite: false, +}; + +interface RuleActionsFormProps { + rulesCount: number; + onClose: () => void; + onConfirm: (bulkActionEditPayload: BulkActionEditPayload) => void; +} + +const RuleActionsFormComponent = ({ rulesCount, onClose, onConfirm }: RuleActionsFormProps) => { + const { + services: { + triggersActionsUi: { actionTypeRegistry }, + }, + } = useKibana(); + + const formSchema = useMemo(() => getFormSchema(actionTypeRegistry), [actionTypeRegistry]); + + const { form } = useForm({ + schema: formSchema, + defaultValue: defaultFormData, + }); + + const [{ overwrite, throttle }] = useFormData({ form, watch: ['overwrite', 'throttle'] }); + + const handleSubmit = useCallback(async () => { + const { data, isValid } = await form.submit(); + if (!isValid) { + return; + } + + const { actions = [], throttle: throttleToSubmit, overwrite: overwriteValue } = data; + const editAction = overwriteValue + ? BulkActionEditType.set_rule_actions + : BulkActionEditType.add_rule_actions; + + onConfirm({ + type: editAction, + value: { + actions: actions.map(({ actionTypeId, ...action }) => action), + throttle: throttleToSubmit, + }, + }); + }, [form, onConfirm]); + + const throttleFieldComponentProps = useMemo( + () => ({ + idAria: 'bulkEditRulesRuleActionThrottle', + dataTestSubj: 'bulkEditRulesRuleActionThrottle', + hasNoInitialSelection: false, + euiFieldProps: { + options: THROTTLE_OPTIONS, + }, + }), + [] + ); + + const messageVariables = useMemo(() => getAllActionMessageParams(), []); + + const showActionsSelect = throttle !== NOTIFICATION_THROTTLE_NO_ACTIONS; + + return ( + + + } + > +
    +
  • + +
  • +
  • + + + + ), + overwriteActionsCheckbox: ( + + + + ), + }} + /> +
  • +
  • {i18n.RULE_VARIABLES_DETAIL}
  • +
+
+ + + + + + {showActionsSelect && ( + <> + + + + )} + + + + {overwrite && ( + <> + + + + + + ), + }} + /> + + + )} +
+ ); +}; + +export const RuleActionsForm = React.memo(RuleActionsFormComponent); +RuleActionsForm.displayName = 'RuleActionsForm'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/forms/tags_form.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/forms/tags_form.tsx index 366115623d041..e53469e27a09a 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/forms/tags_form.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/forms/tags_form.tsx @@ -11,8 +11,8 @@ import { FormattedMessage } from '@kbn/i18n-react'; import * as i18n from '../../../translations'; import { caseInsensitiveSort } from '../../helpers'; -import type { BulkActionEditPayload } from '../../../../../../../../common/detection_engine/schemas/common/schemas'; -import { BulkActionEditType } from '../../../../../../../../common/detection_engine/schemas/common/schemas'; +import type { BulkActionEditPayload } from '../../../../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; +import { BulkActionEditType } from '../../../../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; import type { FormSchema } from '../../../../../../../shared_imports'; import { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/forms/timeline_template_form.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/forms/timeline_template_form.tsx index 3c5852926a5d7..6aa5a3100c100 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/forms/timeline_template_form.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/forms/timeline_template_form.tsx @@ -11,8 +11,8 @@ import { EuiCallOut } from '@elastic/eui'; import type { FormSchema } from '../../../../../../../shared_imports'; import { useForm, UseField } from '../../../../../../../shared_imports'; import { PickTimeline } from '../../../../../../components/rules/pick_timeline'; -import type { BulkActionEditPayload } from '../../../../../../../../common/detection_engine/schemas/common/schemas'; -import { BulkActionEditType } from '../../../../../../../../common/detection_engine/schemas/common/schemas'; +import type { BulkActionEditPayload } from '../../../../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; +import { BulkActionEditType } from '../../../../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; import { BulkEditFormWrapper } from './bulk_edit_form_wrapper'; import { bulkApplyTimelineTemplate as i18n } from '../translations'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/translations.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/translations.tsx index cce2d3e032a83..3c7fdd9cb7a8c 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/translations.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/translations.tsx @@ -55,3 +55,42 @@ export const bulkApplyTimelineTemplate = { /> ), }; + +export const bulkAddRuleActions = { + FORM_TITLE: i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.formTitle', + { + defaultMessage: 'Add rule actions', + } + ), + + OVERWRITE_LABEL: i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.overwriteCheckboxLabel', + { + defaultMessage: 'Overwrite all selected rules actions', + } + ), + + THROTTLE_LABEL: i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.throttleLabel', + { + defaultMessage: 'Actions frequency', + } + ), + + THROTTLE_HELP_TEXT: i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.throttleHelpText', + { + defaultMessage: + 'Select when automated actions should be performed if a rule evaluates as true.', + } + ), + + RULE_VARIABLES_DETAIL: i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.ruleVariablesDetail', + { + defaultMessage: + 'Rule variables may affect only some of the rules you select, based on the rule types (for example, \\u007b\\u007bcontext.rule.threshold\\u007d\\u007d will only display values for threshold rules).', + } + ), +}; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/types.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/types.ts index 9041e83167469..d81c7c995a2fe 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/types.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/types.ts @@ -6,7 +6,7 @@ */ import type { BulkActionsDryRunErrCode } from '../../../../../../../common/constants'; -import type { BulkAction } from '../../../../../../../common/detection_engine/schemas/common/schemas'; +import type { BulkAction } from '../../../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; /** * Only 2 bulk actions are supported for for confirmation dry run modal: diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_actions.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_actions.tsx index 9c5383dc1a693..84da6c6cc6882 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_actions.tsx @@ -14,11 +14,11 @@ import { useIsMounted } from '@kbn/securitysolution-hook-utils'; import type { Toast } from '@kbn/core/public'; import { toMountPoint } from '@kbn/kibana-react-plugin/public'; -import type { BulkActionEditPayload } from '../../../../../../../common/detection_engine/schemas/common/schemas'; +import type { BulkActionEditPayload } from '../../../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; import { BulkAction, BulkActionEditType, -} from '../../../../../../../common/detection_engine/schemas/common/schemas'; +} from '../../../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; import { isMlRule } from '../../../../../../../common/machine_learning/helpers'; import { canEditRuleWithActions } from '../../../../../../common/utils/privileges'; import { useRulesTableContext } from '../rules_table/rules_table_context'; @@ -360,6 +360,16 @@ export const useBulkActions = ({ disabled: isEditDisabled, panel: 1, }, + { + key: i18n.BULK_ACTION_ADD_RULE_ACTIONS, + name: i18n.BULK_ACTION_ADD_RULE_ACTIONS, + 'data-test-subj': 'addRuleActionsBulk', + disabled: isEditDisabled, + onClick: handleBulkEdit(BulkActionEditType.add_rule_actions), + toolTipContent: missingActionPrivileges ? i18n.EDIT_RULE_SETTINGS_TOOLTIP : undefined, + toolTipPosition: 'right', + icon: undefined, + }, { key: i18n.BULK_ACTION_APPLY_TIMELINE_TEMPLATE, name: i18n.BULK_ACTION_APPLY_TIMELINE_TEMPLATE, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_actions_dry_run.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_actions_dry_run.ts index 28c4e4be608dc..5e14c0a57bf51 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_actions_dry_run.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_actions_dry_run.ts @@ -11,7 +11,7 @@ import { useMutation } from '@tanstack/react-query'; import type { BulkAction, BulkActionEditType, -} from '../../../../../../../common/detection_engine/schemas/common/schemas'; +} from '../../../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; import type { BulkActionResponse } from '../../../../../containers/detection_engine/rules'; import { performBulkAction } from '../../../../../containers/detection_engine/rules'; import { computeDryRunPayload } from './utils/compute_dry_run_payload'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_edit_form_flyout.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_edit_form_flyout.ts index babdd1bfa536e..6d629ae1869b4 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_edit_form_flyout.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_edit_form_flyout.ts @@ -8,9 +8,9 @@ import { useState, useCallback, useRef } from 'react'; import { useAsyncConfirmation } from '../rules_table/use_async_confirmation'; import type { - BulkActionEditType, BulkActionEditPayload, -} from '../../../../../../../common/detection_engine/schemas/common/schemas'; + BulkActionEditType, +} from '../../../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; import { useBoolState } from '../../../../../../common/hooks/use_bool_state'; export const useBulkEditFormFlyout = () => { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/utils/compute_dry_run_payload.test.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/utils/compute_dry_run_payload.test.ts index cbdb34654a99b..361f7edc4823f 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/utils/compute_dry_run_payload.test.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/utils/compute_dry_run_payload.test.ts @@ -8,7 +8,7 @@ import { BulkAction, BulkActionEditType, -} from '../../../../../../../../common/detection_engine/schemas/common/schemas'; +} from '../../../../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; import { computeDryRunPayload } from './compute_dry_run_payload'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/utils/compute_dry_run_payload.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/utils/compute_dry_run_payload.ts index d50d6840e899a..c6128100985b0 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/utils/compute_dry_run_payload.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/utils/compute_dry_run_payload.ts @@ -5,11 +5,12 @@ * 2.0. */ -import type { BulkActionEditPayload } from '../../../../../../../../common/detection_engine/schemas/common/schemas'; +import type { BulkActionEditPayload } from '../../../../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; import { BulkAction, BulkActionEditType, -} from '../../../../../../../../common/detection_engine/schemas/common/schemas'; +} from '../../../../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; +import { assertUnreachable } from '../../../../../../../../common/utility_types'; /** * helper utility that creates payload for _bulk_action API in dry mode @@ -53,5 +54,17 @@ export const computeDryRunPayload = ( value: { timeline_id: '', timeline_title: '' }, }, ]; + + case BulkActionEditType.add_rule_actions: + case BulkActionEditType.set_rule_actions: + return [ + { + type: editAction, + value: { throttle: '1h', actions: [] }, + }, + ]; + + default: + assertUnreachable(editAction); } }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_actions.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_actions.tsx index 8e32583a54c9b..cfd83c3ab408f 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_actions.tsx @@ -9,7 +9,7 @@ import type { DefaultItemAction } from '@elastic/eui'; import { EuiToolTip } from '@elastic/eui'; import React from 'react'; import type { NavigateToAppOptions } from '@kbn/core/public'; -import { BulkAction } from '../../../../../../common/detection_engine/schemas/common/schemas'; +import { BulkAction } from '../../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; import type { UseAppToasts } from '../../../../../common/hooks/use_app_toasts'; import { canEditRuleWithActions } from '../../../../../common/utils/privileges'; import type { Rule } from '../../../../containers/detection_engine/rules'; 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 86a54e099e7b2..278ed497d12e8 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 @@ -361,14 +361,44 @@ export const redirectToDetections = ( hasEncryptionKey === false || needsListsConfiguration; -const getRuleSpecificRuleParamKeys = (ruleType: Type) => { - const queryRuleParams = ['index', 'filters', 'language', 'query', 'saved_id']; +const commonRuleParamsKeys = [ + 'id', + 'name', + 'description', + 'false_positives', + 'rule_id', + 'max_signals', + 'risk_score', + 'output_index', + 'references', + 'severity', + 'timeline_id', + 'timeline_title', + 'threat', + 'type', + 'version', +]; +const queryRuleParams = ['index', 'filters', 'language', 'query', 'saved_id']; +const machineLearningRuleParams = ['anomaly_threshold', 'machine_learning_job_id']; +const thresholdRuleParams = ['threshold', ...queryRuleParams]; + +const getAllRuleParamsKeys = (): string[] => { + const allRuleParamsKeys = [ + ...commonRuleParamsKeys, + ...queryRuleParams, + ...machineLearningRuleParams, + ...thresholdRuleParams, + ].sort(); + return Array.from(new Set(allRuleParamsKeys)); +}; + +const getRuleSpecificRuleParamKeys = (ruleType: Type) => { switch (ruleType) { case 'machine_learning': - return ['anomaly_threshold', 'machine_learning_job_id']; + return machineLearningRuleParams; case 'threshold': - return ['threshold', ...queryRuleParams]; + return thresholdRuleParams; case 'new_terms': case 'threat_match': case 'query': @@ -380,24 +410,6 @@ const getRuleSpecificRuleParamKeys = (ruleType: Type) => { }; export const getActionMessageRuleParams = (ruleType: Type): string[] => { - const commonRuleParamsKeys = [ - 'id', - 'name', - 'description', - 'false_positives', - 'rule_id', - 'max_signals', - 'risk_score', - 'output_index', - 'references', - 'severity', - 'timeline_id', - 'timeline_title', - 'threat', - 'type', - 'version', - ]; - const ruleParamsKeys = [ ...commonRuleParamsKeys, ...getRuleSpecificRuleParamKeys(ruleType), @@ -406,12 +418,7 @@ export const getActionMessageRuleParams = (ruleType: Type): string[] => { return ruleParamsKeys; }; -export const getActionMessageParams = memoizeOne((ruleType: Type | undefined): ActionVariables => { - if (!ruleType) { - return { state: [], params: [] }; - } - const actionMessageRuleParams = getActionMessageRuleParams(ruleType); - // Prefixes are being added automatically by the ActionTypeForm +const transformRuleKeysToActionVariables = (actionMessageRuleParams: string[]): ActionVariables => { return { state: [{ name: 'signals_count', description: 'state.signals_count' }], params: [], @@ -428,8 +435,23 @@ export const getActionMessageParams = memoizeOne((ruleType: Type | undefined): A }), ], }; +}; + +export const getActionMessageParams = memoizeOne((ruleType: Type | undefined): ActionVariables => { + if (!ruleType) { + return { state: [], params: [] }; + } + const actionMessageRuleParams = getActionMessageRuleParams(ruleType); + + return transformRuleKeysToActionVariables(actionMessageRuleParams); }); +/** + * returns action variables available for all rule types + */ +export const getAllActionMessageParams = () => + transformRuleKeysToActionVariables(getAllRuleParamsKeys()); + // typed as null not undefined as the initial state for this value is null. export const userHasPermissions = (canUserCRUD: boolean | null): boolean => canUserCRUD != null ? canUserCRUD : true; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts index c732c0a536b73..676356130bada 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts @@ -180,6 +180,13 @@ export const BULK_ACTION_APPLY_TIMELINE_TEMPLATE = i18n.translate( } ); +export const BULK_ACTION_ADD_RULE_ACTIONS = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.addRuleActionsTitle', + { + defaultMessage: 'Add rule actions', + } +); + export const BULK_ACTION_MENU_TITLE = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.contextMenuTitle', { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts index 3c288184e0736..56d93a1ed0336 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts @@ -21,10 +21,10 @@ import { MAX_RULES_TO_UPDATE_IN_PARALLEL, RULES_TABLE_MAX_PAGE_SIZE, } from '../../../../../common/constants'; -import { BulkAction } from '../../../../../common/detection_engine/schemas/common/schemas'; import { performBulkActionSchema, performBulkActionQuerySchema, + BulkAction, } from '../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; import type { SetupPlugins } from '../../../../plugin'; import type { SecuritySolutionPluginRouter } from '../../../../types'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.ts index abfdc5fe491a7..16db4e4955538 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.ts @@ -13,7 +13,7 @@ import type { EqlRuleParams } from '../../schemas/rule_schemas'; import { eqlRuleParams } from '../../schemas/rule_schemas'; import { eqlExecutor } from '../../signals/executors/eql'; import type { CreateRuleOptions, SecurityAlertType } from '../types'; -import { validateImmutable, validateIndexPatterns } from '../utils'; +import { validateIndexPatterns } from '../utils'; export const createEqlAlertType = ( createOptions: CreateRuleOptions @@ -41,7 +41,6 @@ export const createEqlAlertType = ( * @returns mutatedRuleParams */ validateMutatedParams: (mutatedRuleParams) => { - validateImmutable(mutatedRuleParams.immutable); validateIndexPatterns(mutatedRuleParams.index); return mutatedRuleParams; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.ts index 94fc6d78965bb..6a370b381acd1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.ts @@ -13,7 +13,7 @@ import type { ThreatRuleParams } from '../../schemas/rule_schemas'; import { threatRuleParams } from '../../schemas/rule_schemas'; import { threatMatchExecutor } from '../../signals/executors/threat_match'; import type { CreateRuleOptions, SecurityAlertType } from '../types'; -import { validateImmutable, validateIndexPatterns } from '../utils'; +import { validateIndexPatterns } from '../utils'; export const createIndicatorMatchAlertType = ( createOptions: CreateRuleOptions @@ -42,7 +42,6 @@ export const createIndicatorMatchAlertType = ( * @returns mutatedRuleParams */ validateMutatedParams: (mutatedRuleParams) => { - validateImmutable(mutatedRuleParams.immutable); validateIndexPatterns(mutatedRuleParams.index); return mutatedRuleParams; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.ts index 926615fc8d176..c24e9c5af8d4f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.ts @@ -13,7 +13,6 @@ import type { MachineLearningRuleParams } from '../../schemas/rule_schemas'; import { machineLearningRuleParams } from '../../schemas/rule_schemas'; import { mlExecutor } from '../../signals/executors/ml'; import type { CreateRuleOptions, SecurityAlertType } from '../types'; -import { validateImmutable } from '../utils'; export const createMlAlertType = ( createOptions: CreateRuleOptions @@ -34,17 +33,6 @@ export const createMlAlertType = ( } return validated; }, - /** - * validate rule params when rule is bulk edited (update and created in future as well) - * returned params can be modified (useful in case of version increment) - * @param mutatedRuleParams - * @returns mutatedRuleParams - */ - validateMutatedParams: (mutatedRuleParams) => { - validateImmutable(mutatedRuleParams.immutable); - - return mutatedRuleParams; - }, }, }, actionGroups: [ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/new_terms/create_new_terms_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/new_terms/create_new_terms_alert_type.ts index da37ddc49fce7..8a1f941a92f31 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/new_terms/create_new_terms_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/new_terms/create_new_terms_alert_type.ts @@ -27,7 +27,7 @@ import { buildNewTermsAgg, } from './build_new_terms_aggregation'; import type { SignalSource } from '../../signals/types'; -import { validateImmutable, validateIndexPatterns } from '../utils'; +import { validateIndexPatterns } from '../utils'; import { parseDateString, validateHistoryWindowStart } from './utils'; import { addToSearchAfterReturn, createSearchAfterReturnType } from '../../signals/utils'; @@ -61,7 +61,6 @@ export const createNewTermsAlertType = ( * @returns mutatedRuleParams */ validateMutatedParams: (mutatedRuleParams) => { - validateImmutable(mutatedRuleParams.immutable); validateIndexPatterns(mutatedRuleParams.index); return mutatedRuleParams; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.ts index 14e309a83c959..5a940ebe364c5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.ts @@ -13,7 +13,7 @@ import type { QueryRuleParams } from '../../schemas/rule_schemas'; import { queryRuleParams } from '../../schemas/rule_schemas'; import { queryExecutor } from '../../signals/executors/query'; import type { CreateRuleOptions, SecurityAlertType } from '../types'; -import { validateImmutable, validateIndexPatterns } from '../utils'; +import { validateIndexPatterns } from '../utils'; export const createQueryAlertType = ( createOptions: CreateRuleOptions ): SecurityAlertType => { @@ -40,7 +40,6 @@ export const createQueryAlertType = ( * @returns mutatedRuleParams */ validateMutatedParams: (mutatedRuleParams) => { - validateImmutable(mutatedRuleParams.immutable); validateIndexPatterns(mutatedRuleParams.index); return mutatedRuleParams; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/saved_query/create_saved_query_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/saved_query/create_saved_query_alert_type.ts index f8009220581e1..325d9adfb1bda 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/saved_query/create_saved_query_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/saved_query/create_saved_query_alert_type.ts @@ -13,7 +13,7 @@ import type { CompleteRule, SavedQueryRuleParams } from '../../schemas/rule_sche import { savedQueryRuleParams } from '../../schemas/rule_schemas'; import { queryExecutor } from '../../signals/executors/query'; import type { CreateRuleOptions, SecurityAlertType } from '../types'; -import { validateImmutable, validateIndexPatterns } from '../utils'; +import { validateIndexPatterns } from '../utils'; export const createSavedQueryAlertType = ( createOptions: CreateRuleOptions ): SecurityAlertType => { @@ -40,7 +40,6 @@ export const createSavedQueryAlertType = ( * @returns mutatedRuleParams */ validateMutatedParams: (mutatedRuleParams) => { - validateImmutable(mutatedRuleParams.immutable); validateIndexPatterns(mutatedRuleParams.index); return mutatedRuleParams; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.ts index cacc2f91a925f..1a419ddfd09c6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.ts @@ -14,7 +14,7 @@ import { thresholdRuleParams } from '../../schemas/rule_schemas'; import { thresholdExecutor } from '../../signals/executors/threshold'; import type { ThresholdAlertState } from '../../signals/types'; import type { CreateRuleOptions, SecurityAlertType } from '../types'; -import { validateImmutable, validateIndexPatterns } from '../utils'; +import { validateIndexPatterns } from '../utils'; export const createThresholdAlertType = ( createOptions: CreateRuleOptions @@ -42,7 +42,6 @@ export const createThresholdAlertType = ( * @returns mutatedRuleParams */ validateMutatedParams: (mutatedRuleParams) => { - validateImmutable(mutatedRuleParams.immutable); validateIndexPatterns(mutatedRuleParams.index); return mutatedRuleParams; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/validate_mutated_params.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/validate_mutated_params.ts index 0aac2fc709588..f8a696f0c7f68 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/validate_mutated_params.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/validate_mutated_params.ts @@ -5,12 +5,6 @@ * 2.0. */ -export const validateImmutable = (immutable: boolean) => { - if (immutable === true) { - throw new Error("Elastic rule can't be edited"); - } -}; - export const validateIndexPatterns = (indices: string[] | undefined) => { if (indices?.length === 0) { throw new Error("Index patterns can't be empty"); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_actions/action_to_rules_client_operation.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_actions/action_to_rules_client_operation.test.ts index 8646a79d3d070..54da8ec923245 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_actions/action_to_rules_client_operation.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_actions/action_to_rules_client_operation.test.ts @@ -5,34 +5,40 @@ * 2.0. */ -import { BulkActionEditType } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { BulkActionEditType } from '../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; import { bulkEditActionToRulesClientOperation } from './action_to_rules_client_operation'; describe('bulkEditActionToRulesClientOperation', () => { - test('should transform tags bulk edit actions correctly f', () => { + test('should transform tags bulk edit actions correctly', () => { expect( bulkEditActionToRulesClientOperation({ type: BulkActionEditType.add_tags, value: ['test'] }) - ).toEqual({ - field: 'tags', - operation: 'add', - value: ['test'], - }); + ).toEqual([ + { + field: 'tags', + operation: 'add', + value: ['test'], + }, + ]); }); expect( bulkEditActionToRulesClientOperation({ type: BulkActionEditType.set_tags, value: ['test'] }) - ).toEqual({ - field: 'tags', - operation: 'set', - value: ['test'], - }); + ).toEqual([ + { + field: 'tags', + operation: 'set', + value: ['test'], + }, + ]); expect( bulkEditActionToRulesClientOperation({ type: BulkActionEditType.delete_tags, value: ['test'] }) - ).toEqual({ - field: 'tags', - operation: 'delete', - value: ['test'], - }); + ).toEqual([ + { + field: 'tags', + operation: 'delete', + value: ['test'], + }, + ]); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_actions/action_to_rules_client_operation.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_actions/action_to_rules_client_operation.ts index c75f7d0943e52..b462206aa8ff6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_actions/action_to_rules_client_operation.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_actions/action_to_rules_client_operation.ts @@ -7,10 +7,26 @@ import type { BulkEditOperation } from '@kbn/alerting-plugin/server'; -import type { BulkActionEditForRuleAttributes } from '../../../../../common/detection_engine/schemas/common/schemas'; -import { BulkActionEditType } from '../../../../../common/detection_engine/schemas/common/schemas'; +import type { BulkActionEditForRuleAttributes } from '../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; +import { BulkActionEditType } from '../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; import { assertUnreachable } from '../../../../../common/utility_types'; +import { transformToAlertThrottle, transformToNotifyWhen } from '../utils'; + +const getThrottleOperation = (throttle: string) => + ({ + field: 'throttle', + operation: 'set', + value: transformToAlertThrottle(throttle), + } as const); + +const getNotifyWhenOperation = (throttle: string) => + ({ + field: 'notifyWhen', + operation: 'set', + value: transformToNotifyWhen(throttle), + } as const); + /** * converts bulk edit action to format of rulesClient.bulkEdit operation * @param action BulkActionEditForRuleAttributes @@ -18,31 +34,60 @@ import { assertUnreachable } from '../../../../../common/utility_types'; */ export const bulkEditActionToRulesClientOperation = ( action: BulkActionEditForRuleAttributes -): BulkEditOperation => { +): BulkEditOperation[] => { switch (action.type) { // tags actions case BulkActionEditType.add_tags: - return { - field: 'tags', - operation: 'add', - value: action.value, - }; + return [ + { + field: 'tags', + operation: 'add', + value: action.value, + }, + ]; case BulkActionEditType.delete_tags: - return { - field: 'tags', - operation: 'delete', - value: action.value, - }; + return [ + { + field: 'tags', + operation: 'delete', + value: action.value, + }, + ]; case BulkActionEditType.set_tags: - return { - field: 'tags', - operation: 'set', - value: action.value, - }; + return [ + { + field: 'tags', + operation: 'set', + value: action.value, + }, + ]; + + // rule actions + case BulkActionEditType.add_rule_actions: + return [ + { + field: 'actions', + operation: 'add', + value: action.value.actions, + }, + getThrottleOperation(action.value.throttle), + getNotifyWhenOperation(action.value.throttle), + ]; + + case BulkActionEditType.set_rule_actions: + return [ + { + field: 'actions', + operation: 'set', + value: action.value.actions, + }, + getThrottleOperation(action.value.throttle), + getNotifyWhenOperation(action.value.throttle), + ]; default: - return assertUnreachable(action.type); + return assertUnreachable(action); } }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_actions/rule_params_modifier.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_actions/rule_params_modifier.test.ts index 592704842e6c2..8ea80e0fad430 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_actions/rule_params_modifier.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_actions/rule_params_modifier.test.ts @@ -6,7 +6,7 @@ */ import { addItemsToArray, deleteItemsFromArray, ruleParamsModifier } from './rule_params_modifier'; -import { BulkActionEditType } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { BulkActionEditType } from '../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; import type { RuleAlertType } from '../types'; describe('addItemsToArray', () => { @@ -38,9 +38,13 @@ describe('deleteItemsFromArray', () => { }); describe('ruleParamsModifier', () => { - const ruleParamsMock = { index: ['my-index-*'], version: 1 } as RuleAlertType['params']; + const ruleParamsMock = { + index: ['initial-index-*'], + version: 1, + immutable: false, + } as RuleAlertType['params']; - test('should increment version', () => { + test('should increment version if rule is custom (immutable === false)', () => { const editedRuleParams = ruleParamsModifier(ruleParamsMock, [ { type: BulkActionEditType.add_index_patterns, @@ -50,6 +54,16 @@ describe('ruleParamsModifier', () => { expect(editedRuleParams).toHaveProperty('version', ruleParamsMock.version + 1); }); + test('should not increment version if rule is prebuilt (immutable === true)', () => { + const editedRuleParams = ruleParamsModifier({ ...ruleParamsMock, immutable: true }, [ + { + type: BulkActionEditType.add_index_patterns, + value: ['my-index-*'], + }, + ]); + expect(editedRuleParams).toHaveProperty('version', ruleParamsMock.version); + }); + describe('index_patterns', () => { describe('add_index_patterns action', () => { test.each([ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_actions/rule_params_modifier.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_actions/rule_params_modifier.ts index 2b3fb5de9ef79..14d49b14421b3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_actions/rule_params_modifier.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_actions/rule_params_modifier.ts @@ -7,8 +7,8 @@ import type { RuleAlertType } from '../types'; -import type { BulkActionEditForRuleParams } from '../../../../../common/detection_engine/schemas/common/schemas'; -import { BulkActionEditType } from '../../../../../common/detection_engine/schemas/common/schemas'; +import type { BulkActionEditForRuleParams } from '../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; +import { BulkActionEditType } from '../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; import { invariant } from '../../../../../common/utils/invariant'; @@ -111,7 +111,10 @@ export const ruleParamsModifier = ( ); // increment version even if actions are empty, as attributes can be modified as well outside of ruleParamsModifier - modifiedParams.version += 1; + // version must not be modified for immutable rule. Otherwise prebuilt rules upgrade flow will be broken + if (existingRuleParams.immutable === false) { + modifiedParams.version += 1; + } return modifiedParams; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_actions/split_bulk_edit_actions.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_actions/split_bulk_edit_actions.test.ts index 0b7f540b4dcc3..46205c060be78 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_actions/split_bulk_edit_actions.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_actions/split_bulk_edit_actions.test.ts @@ -5,8 +5,8 @@ * 2.0. */ -import type { BulkActionEditPayload } from '../../../../../common/detection_engine/schemas/common/schemas'; -import { BulkActionEditType } from '../../../../../common/detection_engine/schemas/common/schemas'; +import type { BulkActionEditPayload } from '../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; +import { BulkActionEditType } from '../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; import { splitBulkEditActions } from './split_bulk_edit_actions'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_actions/split_bulk_edit_actions.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_actions/split_bulk_edit_actions.ts index e1ba96390d538..f9f16a7684294 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_actions/split_bulk_edit_actions.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_actions/split_bulk_edit_actions.ts @@ -5,12 +5,12 @@ * 2.0. */ +import { BulkActionEditType } from '../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; import type { BulkActionEditPayload, BulkActionEditForRuleAttributes, BulkActionEditForRuleParams, -} from '../../../../../common/detection_engine/schemas/common/schemas'; -import { BulkActionEditType } from '../../../../../common/detection_engine/schemas/common/schemas'; +} from '../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; /** * Split bulk edit actions in 2 chunks: actions applied to params and @@ -32,6 +32,8 @@ export const splitBulkEditActions = (actions: BulkActionEditPayload[]) => { case BulkActionEditType.add_tags: case BulkActionEditType.set_tags: case BulkActionEditType.delete_tags: + case BulkActionEditType.add_rule_actions: + case BulkActionEditType.set_rule_actions: acc.attributesActions.push(action); break; default: diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_actions/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_actions/utils.ts index 65a4af2308e0e..91cfe9544d550 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_actions/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_actions/utils.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { BulkActionEditType } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { BulkActionEditType } from '../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; /** * helper utility that defines whether bulk edit action is related to index patterns, i.e. one of: diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_actions/validations.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_actions/validations.ts index 28793f9fe9c88..5252fd1982ff3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_actions/validations.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_actions/validations.ts @@ -9,7 +9,8 @@ import type { Type as RuleType } from '@kbn/securitysolution-io-ts-alerting-type import { invariant } from '../../../../../common/utils/invariant'; import { isMlRule } from '../../../../../common/machine_learning/helpers'; import { BulkActionsDryRunErrCode } from '../../../../../common/constants'; -import type { BulkActionEditPayload } from '../../../../../common/detection_engine/schemas/common/schemas'; +import type { BulkActionEditPayload } from '../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; +import { BulkActionEditType } from '../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; import type { RuleAlertType } from '../types'; import { isIndexPatternsBulkEditAction } from './utils'; import { throwDryRunError } from './dry_run'; @@ -24,6 +25,8 @@ interface BulkActionsValidationArgs { interface BulkEditBulkActionsValidationArgs { ruleType: RuleType; mlAuthz: MlAuthz; + edit: BulkActionEditPayload[]; + immutable: boolean; } interface DryRunBulkEditBulkActionsValidationArgs { @@ -78,8 +81,26 @@ export const validateBulkDuplicateRule = async ({ rule, mlAuthz }: BulkActionsVa export const validateBulkEditRule = async ({ ruleType, mlAuthz, + edit, + immutable, }: BulkEditBulkActionsValidationArgs) => { await throwMlAuthError(mlAuthz, ruleType); + + // if rule can't be edited error will be thrown + const canRuleBeEdited = !immutable || istEditApplicableToImmutableRule(edit); + await throwDryRunError( + () => invariant(canRuleBeEdited, "Elastic rule can't be edited"), + BulkActionsDryRunErrCode.IMMUTABLE + ); +}; + +/** + * add_rule_actions, set_rule_actions can be applied to prebuilt/immutable rules + */ +const istEditApplicableToImmutableRule = (edit: BulkActionEditPayload[]): boolean => { + return edit.every(({ type }) => + [BulkActionEditType.set_rule_actions, BulkActionEditType.add_rule_actions].includes(type) + ); }; /** @@ -91,13 +112,12 @@ export const dryRunValidateBulkEditRule = async ({ edit, mlAuthz, }: DryRunBulkEditBulkActionsValidationArgs) => { - await validateBulkEditRule({ ruleType: rule.params.type, mlAuthz }); - - // if rule is immutable, it can't be edited - await throwDryRunError( - () => invariant(rule.params.immutable === false, "Elastic rule can't be edited"), - BulkActionsDryRunErrCode.IMMUTABLE - ); + await validateBulkEditRule({ + ruleType: rule.params.type, + mlAuthz, + edit, + immutable: rule.params.immutable, + }); // if rule is machine_learning, index pattern action can't be applied to it await throwDryRunError( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_edit_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_edit_rules.ts index 587d7d539caa6..b17b35fd010a8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_edit_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/bulk_edit_rules.ts @@ -5,15 +5,24 @@ * 2.0. */ -import type { RulesClient } from '@kbn/alerting-plugin/server'; -import type { BulkActionEditPayload } from '../../../../common/detection_engine/schemas/common'; +import pMap from 'p-map'; +import type { RulesClient, BulkEditError } from '@kbn/alerting-plugin/server'; +import type { + BulkActionEditPayload, + BulkActionEditPayloadRuleActions, +} from '../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; import { enrichFilterWithRuleTypeMapping } from './enrich_filter_with_rule_type_mappings'; import type { MlAuthz } from '../../machine_learning/authz'; - import { ruleParamsModifier } from './bulk_actions/rule_params_modifier'; import { splitBulkEditActions } from './bulk_actions/split_bulk_edit_actions'; import { validateBulkEditRule } from './bulk_actions/validations'; import { bulkEditActionToRulesClientOperation } from './bulk_actions/action_to_rules_client_operation'; +import { + NOTIFICATION_THROTTLE_NO_ACTIONS, + MAX_RULES_TO_UPDATE_IN_PARALLEL, +} from '../../../../common/constants'; +import { BulkActionEditType } from '../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; +import { readRules } from './read_rules'; import type { RuleAlertType } from './types'; @@ -32,7 +41,7 @@ export interface BulkEditRulesArguments { * @param BulkEditRulesArguments * @returns edited rules and caught errors */ -export const bulkEditRules = ({ +export const bulkEditRules = async ({ rulesClient, ids, actions, @@ -41,12 +50,75 @@ export const bulkEditRules = ({ }: BulkEditRulesArguments) => { const { attributesActions, paramsActions } = splitBulkEditActions(actions); - return rulesClient.bulkEdit({ + const result = await rulesClient.bulkEdit({ ...(ids ? { ids } : { filter: enrichFilterWithRuleTypeMapping(filter) }), - operations: attributesActions.map(bulkEditActionToRulesClientOperation), + operations: attributesActions.map(bulkEditActionToRulesClientOperation).flat(), paramsModifier: async (ruleParams: RuleAlertType['params']) => { - await validateBulkEditRule({ mlAuthz, ruleType: ruleParams.type }); + await validateBulkEditRule({ + mlAuthz, + ruleType: ruleParams.type, + edit: actions, + immutable: ruleParams.immutable, + }); return ruleParamsModifier(ruleParams, paramsActions); }, }); + + // rulesClient bulkEdit currently doesn't support bulk mute/unmute. + // this is a workaround to mitigate this, + // until https://github.com/elastic/kibana/issues/139084 is resolved + // if rule actions has been applied: + // - we go through each rule + // - mute/unmute if needed, refetch rule + // calling mute for rule needed only when rule was unmuted before and throttle value is NOTIFICATION_THROTTLE_NO_ACTIONS + // calling unmute needed only if rule was muted and throttle value is not NOTIFICATION_THROTTLE_NO_ACTIONS + const ruleActions = attributesActions.filter((rule): rule is BulkActionEditPayloadRuleActions => + [BulkActionEditType.set_rule_actions, BulkActionEditType.add_rule_actions].includes(rule.type) + ); + + // bulk edit actions are applying in a historical order. + // So, we need to find a rule action that will be applied the last, to be able to check if rule should be muted/unmuted + const rulesAction = ruleActions.pop(); + + if (rulesAction) { + const muteOrUnmuteErrors: BulkEditError[] = []; + const rulesToMuteOrUnmute = await pMap( + result.rules, + async (rule) => { + try { + if (rule.muteAll && rulesAction.value.throttle !== NOTIFICATION_THROTTLE_NO_ACTIONS) { + await rulesClient.unmuteAll({ id: rule.id }); + return (await readRules({ rulesClient, id: rule.id, ruleId: undefined })) ?? rule; + } else if ( + !rule.muteAll && + rulesAction.value.throttle === NOTIFICATION_THROTTLE_NO_ACTIONS + ) { + await rulesClient.muteAll({ id: rule.id }); + return (await readRules({ rulesClient, id: rule.id, ruleId: undefined })) ?? rule; + } + + return rule; + } catch (err) { + muteOrUnmuteErrors.push({ + message: err.message, + rule: { + id: rule.id, + name: rule.name, + }, + }); + + return null; + } + }, + { concurrency: MAX_RULES_TO_UPDATE_IN_PARALLEL } + ); + + return { + ...result, + rules: rulesToMuteOrUnmute.filter((rule): rule is RuleAlertType => rule != null), + errors: [...result.errors, ...muteOrUnmuteErrors], + }; + } + + return result; }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/perform_bulk_action.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/perform_bulk_action.ts index 4742eca71f9b0..4d4bda5e6b4e0 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/perform_bulk_action.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/perform_bulk_action.ts @@ -10,11 +10,14 @@ 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 { BulkAction, BulkActionEditType, -} from '@kbn/security-solution-plugin/common/detection_engine/schemas/common/schemas'; +} from '@kbn/security-solution-plugin/common/detection_engine/schemas/request/perform_bulk_action_schema'; import { RulesSchema } from '@kbn/security-solution-plugin/common/detection_engine/schemas/response'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { @@ -30,6 +33,7 @@ import { getLegacyActionSO, installPrePackagedRules, getSimpleMlRule, + getWebHookAction, } from '../../utils'; // eslint-disable-next-line import/no-default-export @@ -43,6 +47,31 @@ export default ({ getService }: FtrProviderContext): void => { const fetchRule = (ruleId: string) => supertest.get(`${DETECTION_ENGINE_RULES_URL}?rule_id=${ruleId}`).set('kbn-xsrf', 'true'); + const fetchPrebuiltRule = async () => { + const { body: findBody } = await supertest + .get( + `${DETECTION_ENGINE_RULES_URL}/_find?per_page=1&filter=alert.attributes.params.immutable: true` + ) + .set('kbn-xsrf', 'true'); + + return findBody.data[0]; + }; + + /** + * allows to get access to internal property: notifyWhen + */ + const fetchRuleByAlertApi = (ruleId: string) => + supertest.get(`/api/alerting/rule/${ruleId}`).set('kbn-xsrf', 'true'); + + const createWebHookAction = async () => + ( + await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'true') + .send(getWebHookAction()) + .expect(200) + ).body; + describe('perform_bulk_action', () => { beforeEach(async () => { await createSignalsIndex(supertest, log); @@ -905,24 +934,50 @@ export default ({ getService }: FtrProviderContext): void => { expect(rule.timeline_title).to.be(undefined); }); - it('should return error when trying to bulk edit immutable rule', async () => { - await installPrePackagedRules(supertest, log); - const { body: findBody } = await supertest - .get( - `${DETECTION_ENGINE_RULES_URL}/_find?per_page=1&filter=alert.attributes.params.immutable: true` - ) - .set('kbn-xsrf', 'true') - .send(); - const immutableRule = findBody.data[0]; + it('should return error if index patterns action is applied to machine learning rule', async () => { + const mlRule = await createRule(supertest, log, getSimpleMlRule()); const { body } = await postBulkAction() .send({ - ids: [immutableRule.id], + ids: [mlRule.id], action: BulkAction.edit, [BulkAction.edit]: [ { - type: BulkActionEditType.add_tags, - value: ['new-tag'], + type: BulkActionEditType.add_index_patterns, + value: ['index-*'], + }, + ], + }) + .expect(500); + + expect(body.attributes.summary).to.eql({ failed: 1, succeeded: 0, total: 1 }); + expect(body.attributes.errors[0]).to.eql({ + message: + "Index patterns can't be added. Machine learning rule doesn't have index patterns property", + status_code: 500, + rules: [ + { + id: mlRule.id, + name: mlRule.name, + }, + ], + }); + }); + + it('should return error if all index patterns removed from a rule', async () => { + const rule = await createRule(supertest, log, { + ...getSimpleRule(), + index: ['simple-index-*'], + }); + + const { body } = await postBulkAction() + .send({ + ids: [rule.id], + action: BulkAction.edit, + [BulkAction.edit]: [ + { + type: BulkActionEditType.delete_index_patterns, + value: ['simple-index-*'], }, ], }) @@ -930,12 +985,12 @@ export default ({ getService }: FtrProviderContext): void => { expect(body.attributes.summary).to.eql({ failed: 1, succeeded: 0, total: 1 }); expect(body.attributes.errors[0]).to.eql({ - message: "Mutated params invalid: Elastic rule can't be edited", + message: "Mutated params invalid: Index patterns can't be empty", status_code: 500, rules: [ { - id: immutableRule.id, - name: immutableRule.name, + id: rule.id, + name: rule.name, }, ], }); @@ -964,6 +1019,642 @@ export default ({ getService }: FtrProviderContext): void => { expect(updatedRule.version).to.be(rule.version + 1); }); + + describe('prebuilt rules', () => { + const cases = [ + { + type: BulkActionEditType.add_tags, + value: ['new-tag'], + }, + { + type: BulkActionEditType.set_tags, + value: ['new-tag'], + }, + { + type: BulkActionEditType.delete_tags, + value: ['new-tag'], + }, + { + type: BulkActionEditType.add_index_patterns, + value: ['test-*'], + }, + { + type: BulkActionEditType.set_index_patterns, + value: ['test-*'], + }, + { + type: BulkActionEditType.delete_index_patterns, + value: ['test-*'], + }, + { + type: BulkActionEditType.set_timeline, + value: { timeline_id: 'mock-id', timeline_title: 'mock-title' }, + }, + ]; + cases.forEach(({ type, value }) => { + it(`should return error when trying to apply "${type}" edit action to prebuilt rule`, async () => { + await installPrePackagedRules(supertest, log); + const prebuiltRule = await fetchPrebuiltRule(); + + const { body } = await postBulkAction() + .send({ + ids: [prebuiltRule.id], + action: BulkAction.edit, + [BulkAction.edit]: [ + { + type, + value, + }, + ], + }) + .expect(500); + + expect(body.attributes.summary).to.eql({ failed: 1, succeeded: 0, total: 1 }); + expect(body.attributes.errors[0]).to.eql({ + message: "Elastic rule can't be edited", + status_code: 500, + rules: [ + { + id: prebuiltRule.id, + name: prebuiltRule.name, + }, + ], + }); + }); + }); + }); + + describe('rule actions', () => { + const webHookActionMock = { + group: 'default', + params: { + body: '{}', + }, + }; + + describe('set_rule_actions', () => { + it('should set action correctly', async () => { + const ruleId = 'ruleId'; + const createdRule = await createRule(supertest, log, getSimpleRule(ruleId)); + + // create a new action + const hookAction = await createWebHookAction(); + + const { body } = await postBulkAction() + .send({ + ids: [createdRule.id], + action: BulkAction.edit, + [BulkAction.edit]: [ + { + type: BulkActionEditType.set_rule_actions, + value: { + throttle: '1h', + actions: [ + { + ...webHookActionMock, + id: hookAction.id, + }, + ], + }, + }, + ], + }) + .expect(200); + + // Check that the updated rule is returned with the response + expect(body.attributes.results.updated[0].actions).to.eql([ + { + ...webHookActionMock, + id: hookAction.id, + action_type_id: '.webhook', + }, + ]); + + // Check that the updates have been persisted + const { body: readRule } = await fetchRule(ruleId).expect(200); + + expect(readRule.actions).to.eql([ + { + ...webHookActionMock, + id: hookAction.id, + action_type_id: '.webhook', + }, + ]); + }); + + it('should set actions to empty list, actions payload is empty list', async () => { + // create a new action + const hookAction = await createWebHookAction(); + + const defaultRuleAction = { + id: hookAction.id, + action_type_id: '.webhook', + group: 'default', + params: { + body: '{"test":"a default action"}', + }, + }; + + const ruleId = 'ruleId'; + const createdRule = await createRule(supertest, log, { + ...getSimpleRule(ruleId), + actions: [defaultRuleAction], + throttle: '1d', + }); + + const { body } = await postBulkAction() + .send({ + ids: [createdRule.id], + action: BulkAction.edit, + [BulkAction.edit]: [ + { + type: BulkActionEditType.set_rule_actions, + value: { + throttle: '1h', + actions: [], + }, + }, + ], + }) + .expect(200); + + // Check that the updated rule is returned with the response + expect(body.attributes.results.updated[0].actions).to.eql([]); + + // Check that the updates have been persisted + const { body: readRule } = await fetchRule(ruleId).expect(200); + + expect(readRule.actions).to.eql([]); + }); + }); + + describe('add_rule_actions', () => { + it('should add action correctly to empty actions list', async () => { + const ruleId = 'ruleId'; + const createdRule = await createRule(supertest, log, getSimpleRule(ruleId)); + + // create a new action + const hookAction = await createWebHookAction(); + + const { body } = await postBulkAction() + .send({ + ids: [createdRule.id], + action: BulkAction.edit, + [BulkAction.edit]: [ + { + type: BulkActionEditType.add_rule_actions, + value: { + throttle: '1h', + actions: [ + { + ...webHookActionMock, + id: hookAction.id, + }, + ], + }, + }, + ], + }) + .expect(200); + + // Check that the updated rule is returned with the response + expect(body.attributes.results.updated[0].actions).to.eql([ + { + ...webHookActionMock, + id: hookAction.id, + action_type_id: '.webhook', + }, + ]); + + // Check that the updates have been persisted + const { body: readRule } = await fetchRule(ruleId).expect(200); + + expect(readRule.actions).to.eql([ + { + ...webHookActionMock, + id: hookAction.id, + action_type_id: '.webhook', + }, + ]); + }); + + it('should add action correctly to non empty actions list', async () => { + // create a new action + const hookAction = await createWebHookAction(); + + const defaultRuleAction = { + id: hookAction.id, + action_type_id: '.webhook', + group: 'default', + params: { + body: '{"test":"a default action"}', + }, + }; + + const ruleId = 'ruleId'; + const createdRule = await createRule(supertest, log, { + ...getSimpleRule(ruleId), + actions: [defaultRuleAction], + throttle: '1d', + }); + + const { body } = await postBulkAction() + .send({ + ids: [createdRule.id], + action: BulkAction.edit, + [BulkAction.edit]: [ + { + type: BulkActionEditType.add_rule_actions, + value: { + throttle: '1h', + actions: [ + { + ...webHookActionMock, + id: hookAction.id, + }, + ], + }, + }, + ], + }) + .expect(200); + + // Check that the updated rule is returned with the response + expect(body.attributes.results.updated[0].actions).to.eql([ + defaultRuleAction, + { + ...webHookActionMock, + id: hookAction.id, + action_type_id: '.webhook', + }, + ]); + + // Check that the updates have been persisted + const { body: readRule } = await fetchRule(ruleId).expect(200); + + expect(readRule.actions).to.eql([ + defaultRuleAction, + { + ...webHookActionMock, + id: hookAction.id, + action_type_id: '.webhook', + }, + ]); + }); + + it('should not change actions of rule if empty list of actions added', async () => { + // create a new action + const hookAction = await createWebHookAction(); + + const defaultRuleAction = { + id: hookAction.id, + action_type_id: '.webhook', + group: 'default', + params: { + body: '{"test":"a default action"}', + }, + }; + + const ruleId = 'ruleId'; + const createdRule = await createRule(supertest, log, { + ...getSimpleRule(ruleId), + actions: [defaultRuleAction], + throttle: '1d', + }); + + const { body } = await postBulkAction() + .send({ + ids: [createdRule.id], + action: BulkAction.edit, + [BulkAction.edit]: [ + { + type: BulkActionEditType.add_rule_actions, + value: { + throttle: '1h', + actions: [], + }, + }, + ], + }) + .expect(200); + + // Check that the updated rule is returned with the response + expect(body.attributes.results.updated[0].actions).to.eql([defaultRuleAction]); + + // Check that the updates have been persisted + const { body: readRule } = await fetchRule(ruleId).expect(200); + + expect(readRule.actions).to.eql([defaultRuleAction]); + }); + + it('should change throttle if actions list in payload is empty', async () => { + // create a new action + const hookAction = await createWebHookAction(); + + const defaultRuleAction = { + id: hookAction.id, + action_type_id: '.webhook', + group: 'default', + params: { + body: '{"test":"a default action"}', + }, + }; + + const ruleId = 'ruleId'; + const createdRule = await createRule(supertest, log, { + ...getSimpleRule(ruleId), + actions: [defaultRuleAction], + throttle: '8h', + }); + + const { body } = await postBulkAction() + .send({ + ids: [createdRule.id], + action: BulkAction.edit, + [BulkAction.edit]: [ + { + type: BulkActionEditType.add_rule_actions, + value: { + throttle: '1h', + actions: [], + }, + }, + ], + }) + .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 updates have been persisted + const { body: readRule } = await fetchRule(ruleId).expect(200); + + expect(readRule.throttle).to.eql('1h'); + }); + }); + + describe('prebuilt rules', () => { + const cases = [ + { + type: BulkActionEditType.set_rule_actions, + }, + { + type: BulkActionEditType.add_rule_actions, + }, + ]; + cases.forEach(({ type }) => { + it(`should apply "${type}" rule action to prebuilt rule`, async () => { + await installPrePackagedRules(supertest, log); + const prebuiltRule = await fetchPrebuiltRule(); + const hookAction = await createWebHookAction(); + + const { body } = await postBulkAction() + .send({ + ids: [prebuiltRule.id], + action: BulkAction.edit, + [BulkAction.edit]: [ + { + type, + value: { + throttle: '1h', + actions: [ + { + ...webHookActionMock, + id: hookAction.id, + }, + ], + }, + }, + ], + }) + .expect(200); + + const editedRule = body.attributes.results.updated[0]; + // Check that the updated rule is returned with the response + expect(editedRule.actions).to.eql([ + { + ...webHookActionMock, + id: hookAction.id, + action_type_id: '.webhook', + }, + ]); + // version of prebuilt rule should not change + expect(editedRule.version).to.be(prebuiltRule.version); + + // Check that the updates have been persisted + const { body: readRule } = await fetchRule(prebuiltRule.rule_id).expect(200); + + expect(readRule.actions).to.eql([ + { + ...webHookActionMock, + id: hookAction.id, + action_type_id: '.webhook', + }, + ]); + expect(prebuiltRule.version).to.be(readRule.version); + }); + }); + + // if rule action is applied together with another edit action, that can't be applied to prebuilt rule (for example: tags action) + // bulk edit request should return error + it(`should return error if one of edit action is not eligible for prebuilt rule`, async () => { + await installPrePackagedRules(supertest, log); + const prebuiltRule = await fetchPrebuiltRule(); + const hookAction = await createWebHookAction(); + + const { body } = await postBulkAction() + .send({ + ids: [prebuiltRule.id], + action: BulkAction.edit, + [BulkAction.edit]: [ + { + type: BulkActionEditType.set_rule_actions, + value: { + throttle: '1h', + actions: [ + { + ...webHookActionMock, + id: hookAction.id, + }, + ], + }, + }, + { + type: BulkActionEditType.set_tags, + value: ['tag-1'], + }, + ], + }) + .expect(500); + + expect(body.attributes.summary).to.eql({ failed: 1, succeeded: 0, total: 1 }); + expect(body.attributes.errors[0]).to.eql({ + message: "Elastic rule can't be edited", + status_code: 500, + rules: [ + { + id: prebuiltRule.id, + name: prebuiltRule.name, + }, + ], + }); + + // Check that the updates were not made + const { body: readRule } = await fetchRule(prebuiltRule.rule_id).expect(200); + + expect(readRule.actions).to.eql(prebuiltRule.actions); + expect(readRule.tags).to.eql(prebuiltRule.tags); + expect(readRule.version).to.be(prebuiltRule.version); + }); + }); + + describe('throttle', () => { + const casesForEmptyActions = [ + { + payloadThrottle: NOTIFICATION_THROTTLE_NO_ACTIONS, + }, + { + payloadThrottle: NOTIFICATION_THROTTLE_RULE, + }, + { + payloadThrottle: '1d', + }, + ]; + casesForEmptyActions.forEach(({ payloadThrottle }) => { + it(`throttle is set to NOTIFICATION_THROTTLE_NO_ACTIONS, if payload throttle="${payloadThrottle}" and actions list is empty`, async () => { + const ruleId = 'ruleId'; + const createdRule = await createRule(supertest, log, { + ...getSimpleRule(ruleId), + throttle: '8h', + }); + + const { body } = await postBulkAction() + .send({ + ids: [createdRule.id], + action: BulkAction.edit, + [BulkAction.edit]: [ + { + type: BulkActionEditType.set_rule_actions, + value: { + throttle: payloadThrottle, + actions: [], + }, + }, + ], + }) + .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 updates have been persisted + const { body: rule } = await fetchRule(ruleId).expect(200); + + expect(rule.throttle).to.eql(NOTIFICATION_THROTTLE_NO_ACTIONS); + }); + }); + + const casesForNonEmptyActions = [ + { + payloadThrottle: NOTIFICATION_THROTTLE_NO_ACTIONS, + expectedThrottle: NOTIFICATION_THROTTLE_NO_ACTIONS, + }, + { + payloadThrottle: NOTIFICATION_THROTTLE_RULE, + expectedThrottle: NOTIFICATION_THROTTLE_RULE, + }, + { + payloadThrottle: '1h', + expectedThrottle: '1h', + }, + ]; + casesForNonEmptyActions.forEach(({ payloadThrottle, expectedThrottle }) => { + it(`throttle is set correctly, if payload throttle="${payloadThrottle}" and actions non empty`, async () => { + // create a new action + const hookAction = await createWebHookAction(); + + const ruleId = 'ruleId'; + const createdRule = await createRule(supertest, log, getSimpleRule(ruleId)); + + const { body } = await postBulkAction() + .send({ + ids: [createdRule.id], + action: BulkAction.edit, + [BulkAction.edit]: [ + { + type: BulkActionEditType.set_rule_actions, + value: { + throttle: payloadThrottle, + actions: [ + { + id: hookAction.id, + group: 'default', + params: { body: '{}' }, + }, + ], + }, + }, + ], + }) + .expect(200); + + // Check that the updated rule is returned with the response + expect(body.attributes.results.updated[0].throttle).to.eql(expectedThrottle); + + // Check that the updates have been persisted + const { body: rule } = await fetchRule(ruleId).expect(200); + + expect(rule.throttle).to.eql(expectedThrottle); + }); + }); + }); + + describe('notifyWhen', () => { + const cases = [ + { + payload: { throttle: NOTIFICATION_THROTTLE_NO_ACTIONS }, + // keeps existing default value which is onActiveAlert + expected: { notifyWhen: 'onActiveAlert' }, + }, + { + payload: { throttle: '1d' }, + expected: { notifyWhen: 'onThrottleInterval' }, + }, + { + payload: { throttle: NOTIFICATION_THROTTLE_RULE }, + expected: { notifyWhen: 'onActiveAlert' }, + }, + ]; + cases.forEach(({ payload, expected }) => { + it(`should set notifyWhen correctly, if payload throttle="${payload.throttle}"`, async () => { + const createdRule = await createRule(supertest, log, getSimpleRule('ruleId')); + + await postBulkAction() + .send({ + ids: [createdRule.id], + action: BulkAction.edit, + [BulkAction.edit]: [ + { + type: BulkActionEditType.set_rule_actions, + value: { + throttle: payload.throttle, + actions: [], + }, + }, + ], + }) + .expect(200); + + // Check whether notifyWhen set correctly + const { body: rule } = await fetchRuleByAlertApi(createdRule.id).expect(200); + + expect(rule.notify_when).to.eql(expected.notifyWhen); + }); + }); + }); + }); }); describe('overwrite_data_views', () => { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/perform_bulk_action_dry_run.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/perform_bulk_action_dry_run.ts index 429b34f3f0a54..e9c3b3b68487d 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/perform_bulk_action_dry_run.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/perform_bulk_action_dry_run.ts @@ -13,7 +13,7 @@ import { import { BulkAction, BulkActionEditType, -} from '@kbn/security-solution-plugin/common/detection_engine/schemas/common/schemas'; +} from '@kbn/security-solution-plugin/common/detection_engine/schemas/request/perform_bulk_action_schema'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { createRule,