From b4b316a7204ee563a02ff3011a9501f2994dc925 Mon Sep 17 00:00:00 2001 From: Ievgen Sorokopud Date: Thu, 2 May 2024 18:10:51 +0200 Subject: [PATCH] [Security Solution] Bulk editing rule custom highlighted fields (#179312) **Resolves: https://github.com/elastic/kibana/issues/164301** **Resolves: https://github.com/elastic/security-team/issues/8958** ## Summary With these changes we introduce a new feature - Bulk custom highlighted fields update. It works similarly to bulk tags and indices update. Here is the overview of the work that has been done: https://github.com/elastic/kibana/assets/2700761/b1ba6670-9984-43c9-9f1e-e18a2b7f071f ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] https://github.com/elastic/security-docs/issues/5090 - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [ ] [ESS 100 times](https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/5834) - [ ] [Serverless 100 times](https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/5835) --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Devin W. Hurley --- .../bulk_actions/bulk_actions_route.gen.ts | 18 + .../bulk_actions_route.schema.yaml | 20 + .../bulk_actions/bulk_actions_route.test.ts | 74 ++- .../bulk_actions/bulk_actions_types.ts | 2 + .../security_solution/common/constants.ts | 1 + .../common/experimental_features.ts | 5 + .../public/common/lib/telemetry/constants.ts | 5 + .../bulk_actions/bulk_edit_flyout.tsx | 6 + .../forms/investigation_fields_form.tsx | 179 ++++++ .../bulk_actions/use_bulk_actions.tsx | 45 ++ .../compute_dry_run_edit_payload.test.ts | 3 + .../utils/compute_dry_run_edit_payload.ts | 10 + .../detection_engine/rules/translations.ts | 79 +++ .../rule_management/api/register_routes.ts | 2 +- .../api/rules/bulk_actions/route.test.ts | 11 +- .../api/rules/bulk_actions/route.ts | 10 +- .../logic/bulk_actions/bulk_edit_rules.ts | 5 +- .../bulk_actions/rule_params_modifier.test.ts | 534 +++++++++++++++--- .../bulk_actions/rule_params_modifier.ts | 121 +++- .../logic/bulk_actions/utils.ts | 15 + .../logic/bulk_actions/validations.ts | 16 +- .../config/ess/config.base.ts | 1 + .../configs/serverless.config.ts | 3 + .../perform_bulk_action.ts | 204 +++++++ .../test/security_solution_cypress/config.ts | 3 + .../bulk_actions/bulk_edit_rules.cy.ts | 102 +++- .../cypress/screens/rule_details.ts | 2 + .../cypress/screens/rules_bulk_actions.ts | 19 + .../cypress/tasks/rule_details.ts | 8 + .../cypress/tasks/rules_bulk_actions.ts | 45 ++ .../serverless_config.ts | 3 + 31 files changed, 1457 insertions(+), 94 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/forms/investigation_fields_form.tsx diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.gen.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.gen.ts index c2ed7dd7061b5..7a3caa983f984 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.gen.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.gen.ts @@ -26,6 +26,7 @@ import { RuleActionAlertsFilter, IndexPatternArray, RuleTagArray, + InvestigationFields, TimelineTemplateId, TimelineTemplateTitle, } from '../../model/rule_schema/common_attributes.gen'; @@ -52,6 +53,7 @@ export const BulkActionsDryRunErrCode = z.enum([ 'MACHINE_LEARNING_AUTH', 'MACHINE_LEARNING_INDEX_PATTERN', 'ESQL_INDEX_PATTERN', + 'INVESTIGATION_FIELDS_FEATURE', ]); export type BulkActionsDryRunErrCodeEnum = typeof BulkActionsDryRunErrCode.enum; export const BulkActionsDryRunErrCodeEnum = BulkActionsDryRunErrCode.enum; @@ -187,6 +189,9 @@ export const BulkActionEditType = z.enum([ 'add_rule_actions', 'set_rule_actions', 'set_schedule', + 'add_investigation_fields', + 'delete_investigation_fields', + 'set_investigation_fields', ]); export type BulkActionEditTypeEnum = typeof BulkActionEditType.enum; export const BulkActionEditTypeEnum = BulkActionEditType.enum; @@ -239,6 +244,18 @@ export const BulkActionEditPayloadTags = z.object({ value: RuleTagArray, }); +export type BulkActionEditPayloadInvestigationFields = z.infer< + typeof BulkActionEditPayloadInvestigationFields +>; +export const BulkActionEditPayloadInvestigationFields = z.object({ + type: z.enum([ + 'add_investigation_fields', + 'delete_investigation_fields', + 'set_investigation_fields', + ]), + value: InvestigationFields, +}); + export type BulkActionEditPayloadTimeline = z.infer; export const BulkActionEditPayloadTimeline = z.object({ type: z.literal('set_timeline'), @@ -252,6 +269,7 @@ export type BulkActionEditPayload = z.infer; export const BulkActionEditPayload = z.union([ BulkActionEditPayloadTags, BulkActionEditPayloadIndexPatterns, + BulkActionEditPayloadInvestigationFields, BulkActionEditPayloadTimeline, BulkActionEditPayloadRuleActions, BulkActionEditPayloadSchedule, diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.schema.yaml index 10422772785e3..6b5a3aa1c6982 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.schema.yaml @@ -76,6 +76,7 @@ components: - MACHINE_LEARNING_AUTH - MACHINE_LEARNING_INDEX_PATTERN - ESQL_INDEX_PATTERN + - INVESTIGATION_FIELDS_FEATURE NormalizedRuleError: type: object @@ -281,6 +282,9 @@ components: - add_rule_actions - set_rule_actions - set_schedule + - add_investigation_fields + - delete_investigation_fields + - set_investigation_fields # 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 NormalizedRuleAction: @@ -381,6 +385,21 @@ components: - type - value + BulkActionEditPayloadInvestigationFields: + type: object + properties: + type: + type: string + enum: + - add_investigation_fields + - delete_investigation_fields + - set_investigation_fields + value: + $ref: '../../model/rule_schema/common_attributes.schema.yaml#/components/schemas/InvestigationFields' + required: + - type + - value + BulkActionEditPayloadTimeline: type: object properties: @@ -406,6 +425,7 @@ components: anyOf: - $ref: '#/components/schemas/BulkActionEditPayloadTags' - $ref: '#/components/schemas/BulkActionEditPayloadIndexPatterns' + - $ref: '#/components/schemas/BulkActionEditPayloadInvestigationFields' - $ref: '#/components/schemas/BulkActionEditPayloadTimeline' - $ref: '#/components/schemas/BulkActionEditPayloadRuleActions' - $ref: '#/components/schemas/BulkActionEditPayloadSchedule' diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.test.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.test.ts index ff5289f79d98d..74bdf8707629e 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.test.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.test.ts @@ -187,7 +187,7 @@ describe('Perform bulk action request schema', () => { expectParseError(result); expect(stringifyZodError(result.error)).toMatchInlineSnapshot( - `"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 9 more"` + `"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 11 more"` ); }); @@ -249,7 +249,7 @@ describe('Perform bulk action request schema', () => { expectParseError(result); expect(stringifyZodError(result.error)).toMatchInlineSnapshot( - `"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 9 more"` + `"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 11 more"` ); }); @@ -299,6 +299,62 @@ describe('Perform bulk action request schema', () => { }); }); + describe('investigation_fields', () => { + test('valid request: set_investigation_fields edit action', () => { + const payload: PerformBulkActionRequestBody = { + query: 'name: test', + action: BulkActionTypeEnum.edit, + [BulkActionTypeEnum.edit]: [ + { + type: BulkActionEditTypeEnum.set_investigation_fields, + value: { field_names: ['field-1'] }, + }, + ], + }; + + const result = PerformBulkActionRequestBody.safeParse(payload); + + expectParseSuccess(result); + expect(result.data).toEqual(payload); + }); + + test('valid request: add_investigation_fields edit action', () => { + const payload: PerformBulkActionRequestBody = { + query: 'name: test', + action: BulkActionTypeEnum.edit, + [BulkActionTypeEnum.edit]: [ + { + type: BulkActionEditTypeEnum.add_investigation_fields, + value: { field_names: ['field-2'] }, + }, + ], + }; + + const result = PerformBulkActionRequestBody.safeParse(payload); + + expectParseSuccess(result); + expect(result.data).toEqual(payload); + }); + + test('valid request: delete_investigation_fields edit action', () => { + const payload: PerformBulkActionRequestBody = { + query: 'name: test', + action: BulkActionTypeEnum.edit, + [BulkActionTypeEnum.edit]: [ + { + type: BulkActionEditTypeEnum.delete_investigation_fields, + value: { field_names: ['field-3'] }, + }, + ], + }; + + const result = PerformBulkActionRequestBody.safeParse(payload); + + expectParseSuccess(result); + expect(result.data).toEqual(payload); + }); + }); + describe('timeline', () => { test('invalid request: wrong timeline payload type', () => { const payload = { @@ -311,7 +367,7 @@ describe('Perform bulk action request schema', () => { expectParseError(result); expect(stringifyZodError(result.error)).toMatchInlineSnapshot( - `"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 7 more"` + `"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 9 more"` ); }); @@ -333,7 +389,7 @@ describe('Perform bulk action request schema', () => { expectParseError(result); expect(stringifyZodError(result.error)).toMatchInlineSnapshot( - `"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 10 more"` + `"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 12 more"` ); }); @@ -371,7 +427,7 @@ describe('Perform bulk action request schema', () => { expectParseError(result); expect(stringifyZodError(result.error)).toMatchInlineSnapshot( - `"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 7 more"` + `"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 9 more"` ); }); @@ -416,7 +472,7 @@ describe('Perform bulk action request schema', () => { expectParseError(result); expect(stringifyZodError(result.error)).toMatchInlineSnapshot( - `"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 10 more"` + `"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 12 more"` ); }); @@ -438,7 +494,7 @@ describe('Perform bulk action request schema', () => { expectParseError(result); expect(stringifyZodError(result.error)).toMatchInlineSnapshot( - `"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 10 more"` + `"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 12 more"` ); }); @@ -476,7 +532,7 @@ describe('Perform bulk action request schema', () => { expectParseError(result); expect(stringifyZodError(result.error)).toMatchInlineSnapshot( - `"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 7 more"` + `"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 9 more"` ); }); @@ -498,7 +554,7 @@ describe('Perform bulk action request schema', () => { expectParseError(result); expect(stringifyZodError(result.error)).toMatchInlineSnapshot( - `"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 11 more"` + `"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 13 more"` ); }); diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_types.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_types.ts index 6e57e5abe2410..c160b6fb21c27 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_types.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_types.ts @@ -7,6 +7,7 @@ import type { BulkActionEditPayloadIndexPatterns, + BulkActionEditPayloadInvestigationFields, BulkActionEditPayloadRuleActions, BulkActionEditPayloadSchedule, BulkActionEditPayloadTags, @@ -26,5 +27,6 @@ export type BulkActionEditForRuleAttributes = */ export type BulkActionEditForRuleParams = | BulkActionEditPayloadIndexPatterns + | BulkActionEditPayloadInvestigationFields | BulkActionEditPayloadTimeline | BulkActionEditPayloadSchedule; diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 0a470b0d234f3..ceff046d8c678 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -442,6 +442,7 @@ export enum BulkActionsDryRunErrCode { MACHINE_LEARNING_AUTH = 'MACHINE_LEARNING_AUTH', MACHINE_LEARNING_INDEX_PATTERN = 'MACHINE_LEARNING_INDEX_PATTERN', ESQL_INDEX_PATTERN = 'ESQL_INDEX_PATTERN', + INVESTIGATION_FIELDS_FEATURE = 'INVESTIGATION_FIELDS_FEATURE', } export const MAX_NUMBER_OF_NEW_TERMS_FIELDS = 3; diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index f575a6ffcd091..175a40288b9d8 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -261,6 +261,11 @@ export const allowedExperimentalValues = Object.freeze({ * Enables the new modal for the value list items */ valueListItemsModalEnabled: true, + + /** + * Enables the new rule's bulk action to manage custom highlighted fields + */ + bulkCustomHighlightedFieldsEnabled: false, }); type ExperimentalConfigKeys = Array; diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/constants.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/constants.ts index a6eff07ac00ff..b7893592296e9 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/constants.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/constants.ts @@ -41,6 +41,11 @@ export enum TELEMETRY_EVENT { DELETE_VALUE_LIST_ITEM = 'delete_value_list_item', EDIT_VALUE_LIST_ITEM = 'edit_value_list_item', ADDITIONAL_UPLOAD_VALUE_LIST_ITEM = 'additinonal_upload_value_list_item', + + // Bulk custom highlighted fields action + ADD_INVESTIGATION_FIELDS = 'add_investigation_fields', + SET_INVESTIGATION_FIELDS = 'set_investigation_fields', + DELETE_INVESTIGATION_FIELDS = 'delete_investigation_fields', } export enum TelemetryEventTypes { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/bulk_edit_flyout.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/bulk_edit_flyout.tsx index 8ef1f9723cd39..d4b01000233df 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/bulk_edit_flyout.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/bulk_edit_flyout.tsx @@ -18,6 +18,7 @@ import { TagsForm } from './forms/tags_form'; import { TimelineTemplateForm } from './forms/timeline_template_form'; import { RuleActionsForm } from './forms/rule_actions_form'; import { ScheduleForm } from './forms/schedule_form'; +import { InvestigationFieldsForm } from './forms/investigation_fields_form'; interface BulkEditFlyoutProps { onClose: () => void; @@ -38,6 +39,11 @@ const BulkEditFlyoutComponent = ({ editAction, ...props }: BulkEditFlyoutProps) case BulkActionEditTypeEnum.set_tags: return ; + case BulkActionEditTypeEnum.add_investigation_fields: + case BulkActionEditTypeEnum.delete_investigation_fields: + case BulkActionEditTypeEnum.set_investigation_fields: + return ; + case BulkActionEditTypeEnum.set_timeline: return ; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/forms/investigation_fields_form.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/forms/investigation_fields_form.tsx new file mode 100644 index 0000000000000..449664e20222a --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/forms/investigation_fields_form.tsx @@ -0,0 +1,179 @@ +/* + * 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 from 'react'; +import { EuiFormRow, EuiCallOut } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import { useKibana } from '../../../../../../common/lib/kibana'; +import { DEFAULT_INDEX_KEY } from '../../../../../../../common/constants'; +import { METRIC_TYPE, TELEMETRY_EVENT, track } from '../../../../../../common/lib/telemetry'; +import * as i18n from '../../../../../../detections/pages/detection_engine/rules/translations'; + +import { useFetchIndex } from '../../../../../../common/containers/source'; + +import { BulkActionEditTypeEnum } from '../../../../../../../common/api/detection_engine/rule_management'; +import type { BulkActionEditPayload } from '../../../../../../../common/api/detection_engine/rule_management'; + +import type { FormSchema } from '../../../../../../shared_imports'; +import { + Field, + getUseField, + useFormData, + useForm, + FIELD_TYPES, + fieldValidators, +} from '../../../../../../shared_imports'; + +import { BulkEditFormWrapper } from './bulk_edit_form_wrapper'; + +const CommonUseField = getUseField({ component: Field }); + +type InvestigationFieldsEditActions = + | BulkActionEditTypeEnum['add_investigation_fields'] + | BulkActionEditTypeEnum['delete_investigation_fields'] + | BulkActionEditTypeEnum['set_investigation_fields']; + +interface InvestigationFieldsFormData { + investigationFields: string[]; + overwrite: boolean; +} + +const schema: FormSchema = { + investigationFields: { + fieldsToValidateOnChange: ['investigationFields'], + type: FIELD_TYPES.COMBO_BOX, + validations: [ + { + validator: fieldValidators.emptyField( + i18n.BULK_EDIT_FLYOUT_FORM_ADD_INVESTIGATION_FIELDS_REQUIRED_ERROR + ), + }, + ], + }, + overwrite: { + type: FIELD_TYPES.CHECKBOX, + label: i18n.BULK_EDIT_FLYOUT_FORM_ADD_INVESTIGATION_FIELDS_OVERWRITE_LABEL, + }, +}; + +const initialFormData: InvestigationFieldsFormData = { + investigationFields: [], + overwrite: false, +}; + +const getFormConfig = (editAction: InvestigationFieldsEditActions) => + editAction === BulkActionEditTypeEnum.add_investigation_fields + ? { + indexLabel: i18n.BULK_EDIT_FLYOUT_FORM_ADD_INVESTIGATION_FIELDS_LABEL, + indexHelpText: i18n.BULK_EDIT_FLYOUT_FORM_ADD_INVESTIGATION_FIELDS_HELP_TEXT, + formTitle: i18n.BULK_EDIT_FLYOUT_FORM_ADD_INVESTIGATION_FIELDS_TITLE, + } + : { + indexLabel: i18n.BULK_EDIT_FLYOUT_FORM_DELETE_INVESTIGATION_FIELDS_LABEL, + indexHelpText: i18n.BULK_EDIT_FLYOUT_FORM_DELETE_INVESTIGATION_FIELDS_HELP_TEXT, + formTitle: i18n.BULK_EDIT_FLYOUT_FORM_DELETE_INVESTIGATION_FIELDS_TITLE, + }; + +interface InvestigationFieldsFormProps { + editAction: InvestigationFieldsEditActions; + rulesCount: number; + onClose: () => void; + onConfirm: (bulkActionEditPayload: BulkActionEditPayload) => void; +} + +const InvestigationFieldsFormComponent = ({ + editAction, + rulesCount, + onClose, + onConfirm, +}: InvestigationFieldsFormProps) => { + const { form } = useForm({ + defaultValue: initialFormData, + schema, + }); + + const { uiSettings } = useKibana().services; + const defaultPatterns = uiSettings.get(DEFAULT_INDEX_KEY); + + const { indexHelpText, indexLabel, formTitle } = getFormConfig(editAction); + + const [{ overwrite }] = useFormData({ + form, + watch: ['overwrite'], + }); + const [_, { indexPatterns }] = useFetchIndex(defaultPatterns, false); + const fieldOptions = indexPatterns.fields.map((field) => ({ + label: field.name, + })); + + const handleSubmit = async () => { + const { data, isValid } = await form.submit(); + if (!isValid) { + return; + } + + const event = data.overwrite + ? TELEMETRY_EVENT.SET_INVESTIGATION_FIELDS + : editAction === 'delete_investigation_fields' + ? TELEMETRY_EVENT.DELETE_INVESTIGATION_FIELDS + : TELEMETRY_EVENT.ADD_INVESTIGATION_FIELDS; + track(METRIC_TYPE.CLICK, event); + + onConfirm({ + value: { field_names: data.investigationFields }, + type: data.overwrite ? BulkActionEditTypeEnum.set_investigation_fields : editAction, + }); + }; + + return ( + + + {editAction === BulkActionEditTypeEnum.add_investigation_fields && ( + + )} + {overwrite && ( + + + + + + )} + + ); +}; + +export const InvestigationFieldsForm = React.memo(InvestigationFieldsFormComponent); +InvestigationFieldsForm.displayName = 'InvestigationFieldsForm'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/use_bulk_actions.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/use_bulk_actions.tsx index 0e57febd33979..70d522323a964 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/use_bulk_actions.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/use_bulk_actions.tsx @@ -12,6 +12,7 @@ import type { Toast } from '@kbn/core/public'; import { toMountPoint } from '@kbn/react-kibana-mount'; import { euiThemeVars } from '@kbn/ui-theme'; import React, { useCallback } from 'react'; +import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; import { useKibana } from '../../../../../common/lib/kibana'; import { convertRulesFilterToKQL } from '../../../../../../common/detection_engine/rule_management/rule_filtering'; import { DuplicateOptions } from '../../../../../../common/detection_engine/rule_management/constants'; @@ -82,6 +83,10 @@ export const useBulkActions = ({ actions: { clearRulesSelection, setIsPreflightInProgress }, } = rulesTableContext; + const isBulkCustomHighlightedFieldsEnabled = useIsExperimentalFeatureEnabled( + 'bulkCustomHighlightedFieldsEnabled' + ); + const getBulkItemsPopoverContent = useCallback( (closePopover: () => void): EuiContextMenuPanelDescriptor[] => { const selectedRules = rules.filter(({ id }) => selectedRuleIds.includes(id)); @@ -331,6 +336,17 @@ export const useBulkActions = ({ disabled: isEditDisabled, panel: 1, }, + ...(isBulkCustomHighlightedFieldsEnabled + ? [ + { + key: i18n.BULK_ACTION_INVESTIGATION_FIELDS, + name: i18n.BULK_ACTION_INVESTIGATION_FIELDS, + 'data-test-subj': 'investigationFieldsBulkEditRule', + disabled: isEditDisabled, + panel: 3, + }, + ] + : []), { key: i18n.BULK_ACTION_ADD_RULE_ACTIONS, name: i18n.BULK_ACTION_ADD_RULE_ACTIONS, @@ -461,6 +477,34 @@ export const useBulkActions = ({ }, ], }, + { + id: 3, + title: i18n.BULK_ACTION_MENU_TITLE, + items: [ + { + key: i18n.BULK_ACTION_ADD_INVESTIGATION_FIELDS, + name: i18n.BULK_ACTION_ADD_INVESTIGATION_FIELDS, + 'data-test-subj': 'addInvestigationFieldsBulkEditRule', + onClick: handleBulkEdit(BulkActionEditTypeEnum.add_investigation_fields), + disabled: isEditDisabled, + toolTipContent: missingActionPrivileges + ? i18n.LACK_OF_KIBANA_ACTIONS_FEATURE_PRIVILEGES + : undefined, + toolTipProps: { position: 'right' }, + }, + { + key: i18n.BULK_ACTION_DELETE_INVESTIGATION_FIELDS, + name: i18n.BULK_ACTION_DELETE_INVESTIGATION_FIELDS, + 'data-test-subj': 'deleteInvestigationFieldsBulkEditRule', + onClick: handleBulkEdit(BulkActionEditTypeEnum.delete_investigation_fields), + disabled: isEditDisabled, + toolTipContent: missingActionPrivileges + ? i18n.LACK_OF_KIBANA_ACTIONS_FEATURE_PRIVILEGES + : undefined, + toolTipProps: { position: 'right' }, + }, + ], + }, ]; }, [ @@ -468,6 +512,7 @@ export const useBulkActions = ({ selectedRuleIds, hasActionsPrivileges, isAllSelected, + isBulkCustomHighlightedFieldsEnabled, loadingRuleIds, startTransaction, hasMlPermissions, diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/utils/compute_dry_run_edit_payload.test.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/utils/compute_dry_run_edit_payload.test.ts index 0549306036fd2..29d1c03a1b0e4 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/utils/compute_dry_run_edit_payload.test.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/utils/compute_dry_run_edit_payload.test.ts @@ -12,6 +12,9 @@ import { computeDryRunEditPayload } from './compute_dry_run_edit_payload'; describe('computeDryRunEditPayload', () => { test.each<[BulkActionEditType, unknown]>([ + [BulkActionEditTypeEnum.set_investigation_fields, { field_names: ['@timestamp'] }], + [BulkActionEditTypeEnum.delete_investigation_fields, { field_names: ['@timestamp'] }], + [BulkActionEditTypeEnum.add_investigation_fields, { field_names: ['@timestamp'] }], [BulkActionEditTypeEnum.set_index_patterns, []], [BulkActionEditTypeEnum.delete_index_patterns, []], [BulkActionEditTypeEnum.add_index_patterns, []], diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/utils/compute_dry_run_edit_payload.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/utils/compute_dry_run_edit_payload.ts index ba5d565e393d0..340e2345b33db 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/utils/compute_dry_run_edit_payload.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/utils/compute_dry_run_edit_payload.ts @@ -20,6 +20,16 @@ import { assertUnreachable } from '../../../../../../../common/utility_types'; */ export function computeDryRunEditPayload(editAction: BulkActionEditType): BulkActionEditPayload[] { switch (editAction) { + case BulkActionEditTypeEnum.add_investigation_fields: + case BulkActionEditTypeEnum.delete_investigation_fields: + case BulkActionEditTypeEnum.set_investigation_fields: + return [ + { + type: editAction, + value: { field_names: ['@timestamp'] }, + }, + ]; + case BulkActionEditTypeEnum.add_index_patterns: case BulkActionEditTypeEnum.delete_index_patterns: case BulkActionEditTypeEnum.set_index_patterns: 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 7ceb3fa661ba6..9e228cba4c74c 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 @@ -159,6 +159,27 @@ export const BULK_ACTION_DELETE_TAGS = i18n.translate( } ); +export const BULK_ACTION_INVESTIGATION_FIELDS = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.investigationFieldsTitle', + { + defaultMessage: 'Custom highlighted fields', + } +); + +export const BULK_ACTION_ADD_INVESTIGATION_FIELDS = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.addInvestigationFieldsTitle', + { + defaultMessage: 'Add custom highlighted fields', + } +); + +export const BULK_ACTION_DELETE_INVESTIGATION_FIELDS = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.deleteInvestigationFieldsTitle', + { + defaultMessage: 'Delete custom highlighted fields', + } +); + export const BULK_ACTION_APPLY_TIMELINE_TEMPLATE = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.applyTimelineTemplateTitle', { @@ -408,6 +429,64 @@ export const BULK_EDIT_FLYOUT_FORM_DELETE_TAGS_TITLE = i18n.translate( } ); +export const BULK_EDIT_FLYOUT_FORM_ADD_INVESTIGATION_FIELDS_REQUIRED_ERROR = i18n.translate( + 'xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.investigationFieldsRequiredErrorMessage', + { + defaultMessage: 'A minimum of one custom highlighted field is required.', + } +); + +export const BULK_EDIT_FLYOUT_FORM_ADD_INVESTIGATION_FIELDS_OVERWRITE_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.addInvestigationFieldsOverwriteCheckboxLabel', + { + defaultMessage: "Overwrite all selected rules' custom highlighted fields", + } +); + +export const BULK_EDIT_FLYOUT_FORM_ADD_INVESTIGATION_FIELDS_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.addInvestigationFieldsComboboxLabel', + { + defaultMessage: 'Add custom highlighted fields for selected rules', + } +); + +export const BULK_EDIT_FLYOUT_FORM_ADD_INVESTIGATION_FIELDS_HELP_TEXT = i18n.translate( + 'xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.addInvestigationFieldsComboboxHelpText', + { + defaultMessage: + 'Enter fields that you would like to add. By default, the dropdown includes fields of the index patterns defined in Security Solution advanced settings.', + } +); + +export const BULK_EDIT_FLYOUT_FORM_ADD_INVESTIGATION_FIELDS_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.addInvestigationFieldsTitle', + { + defaultMessage: 'Add custom highlighted fields', + } +); + +export const BULK_EDIT_FLYOUT_FORM_DELETE_INVESTIGATION_FIELDS_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.deleteInvestigationFieldsComboboxLabel', + { + defaultMessage: 'Delete custom highlighted fields for selected rules', + } +); + +export const BULK_EDIT_FLYOUT_FORM_DELETE_INVESTIGATION_FIELDS_HELP_TEXT = i18n.translate( + 'xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.deleteInvestigationFieldsComboboxHelpText', + { + defaultMessage: + 'Enter fields that you would like to delete. By default, the dropdown includes fields of the index patterns defined in Security Solution advanced settings.', + } +); + +export const BULK_EDIT_FLYOUT_FORM_DELETE_INVESTIGATION_FIELDS_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.deleteInvestigationFieldsTitle', + { + defaultMessage: 'Delete custom highlighted fields', + } +); + export const EXPORT_FILENAME = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.allRules.exportFilenameTitle', { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/register_routes.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/register_routes.ts index ed0f19c7015b6..75414a8cd08ef 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/register_routes.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/register_routes.ts @@ -47,7 +47,7 @@ export const registerRuleManagementRoutes = ( bulkDeleteRulesRoute(router, logger); // Rules bulk actions - performBulkActionRoute(router, ml, logger); + performBulkActionRoute(router, config, ml, logger); // Rules export/import exportRulesRoute(router, config, logger); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.test.ts index 576ba1376ba3c..0b7710db26425 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.test.ts @@ -18,7 +18,12 @@ import { getFindResultWithSingleHit, getFindResultWithMultiHits, } from '../../../../routes/__mocks__/request_responses'; -import { requestContextMock, serverMock, requestMock } from '../../../../routes/__mocks__'; +import { + createMockConfig, + requestContextMock, + serverMock, + requestMock, +} from '../../../../routes/__mocks__'; import { performBulkActionRoute } from './route'; import { getPerformBulkActionEditSchemaMock, @@ -32,6 +37,7 @@ jest.mock('../../../logic/crud/read_rules', () => ({ readRules: jest.fn() })); describe('Perform bulk action route', () => { const readRulesMock = readRules as jest.Mock; + let config: ReturnType; let server: ReturnType; let { clients, context } = requestContextMock.createTools(); let ml: ReturnType; @@ -42,6 +48,7 @@ describe('Perform bulk action route', () => { server = serverMock.create(); logger = loggingSystemMock.createLogger(); ({ clients, context } = requestContextMock.createTools()); + config = createMockConfig(); ml = mlServicesMock.createSetupContract(); clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); @@ -50,7 +57,7 @@ describe('Perform bulk action route', () => { errors: [], total: 1, }); - performBulkActionRoute(server.router, ml, logger); + performBulkActionRoute(server.router, config, ml, logger); }); describe('status codes', () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.ts index 8dc0a5ec651bb..ae0298ada97c1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.ts @@ -8,6 +8,7 @@ import type { IKibanaResponse, Logger } from '@kbn/core/server'; import { AbortError } from '@kbn/kibana-utils-plugin/common'; import { transformError } from '@kbn/securitysolution-es-utils'; +import type { ConfigType } from '../../../../../../config'; import type { PerformBulkActionResponse } from '../../../../../../../common/api/detection_engine/rule_management'; import { BulkActionTypeEnum, @@ -47,6 +48,7 @@ const MAX_ROUTE_CONCURRENCY = 5; export const performBulkActionRoute = ( router: SecuritySolutionPluginRouter, + config: ConfigType, ml: SetupPlugins['ml'], logger: Logger ) => { @@ -143,6 +145,7 @@ export const performBulkActionRoute = ( ids: body.ids, actions: body.edit, mlAuthz, + experimentalFeatures: config.experimentalFeatures, }); return buildBulkResponse(response, { @@ -303,7 +306,12 @@ export const performBulkActionRoute = ( concurrency: MAX_RULES_TO_UPDATE_IN_PARALLEL, items: rules, executor: async (rule) => { - await dryRunValidateBulkEditRule({ mlAuthz, rule, edit: body.edit }); + await dryRunValidateBulkEditRule({ + mlAuthz, + rule, + edit: body.edit, + experimentalFeatures: config.experimentalFeatures, + }); return rule; }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/bulk_edit_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/bulk_edit_rules.ts index fd2f1644480c0..3c7e0e7af16ee 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/bulk_edit_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/bulk_edit_rules.ts @@ -7,6 +7,7 @@ import type { RulesClient } from '@kbn/alerting-plugin/server'; +import type { ExperimentalFeatures } from '../../../../../../common'; import type { BulkActionEditPayload } from '../../../../../../common/api/detection_engine/rule_management'; import type { MlAuthz } from '../../../../machine_learning/authz'; @@ -25,6 +26,7 @@ export interface BulkEditRulesArguments { filter?: string; ids?: string[]; mlAuthz: MlAuthz; + experimentalFeatures: ExperimentalFeatures; } /** @@ -40,6 +42,7 @@ export const bulkEditRules = async ({ actions, filter, mlAuthz, + experimentalFeatures, }: BulkEditRulesArguments) => { const { attributesActions, paramsActions } = splitBulkEditActions(actions); const operations = attributesActions.map(bulkEditActionToRulesClientOperation).flat(); @@ -53,7 +56,7 @@ export const bulkEditRules = async ({ edit: actions, immutable: ruleParams.immutable, }); - return ruleParamsModifier(ruleParams, paramsActions); + return ruleParamsModifier(ruleParams, paramsActions, experimentalFeatures); }, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/rule_params_modifier.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/rule_params_modifier.test.ts index 93044fc0fed18..af8fca55cf5fb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/rule_params_modifier.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/rule_params_modifier.test.ts @@ -8,6 +8,11 @@ import { addItemsToArray, deleteItemsFromArray, ruleParamsModifier } from './rule_params_modifier'; import { BulkActionEditTypeEnum } from '../../../../../../common/api/detection_engine/rule_management'; import type { RuleAlertType } from '../../../rule_schema'; +import type { ExperimentalFeatures } from '../../../../../../common'; + +const mockExperimentalFeatures = { + bulkCustomHighlightedFieldsEnabled: true, +} as ExperimentalFeatures; describe('addItemsToArray', () => { test('should add single item to array', () => { @@ -45,22 +50,30 @@ describe('ruleParamsModifier', () => { } as RuleAlertType['params']; test('should increment version if rule is custom (immutable === false)', () => { - const { modifiedParams } = ruleParamsModifier(ruleParamsMock, [ - { - type: BulkActionEditTypeEnum.add_index_patterns, - value: ['my-index-*'], - }, - ]); + const { modifiedParams } = ruleParamsModifier( + ruleParamsMock, + [ + { + type: BulkActionEditTypeEnum.add_index_patterns, + value: ['my-index-*'], + }, + ], + mockExperimentalFeatures + ); expect(modifiedParams).toHaveProperty('version', ruleParamsMock.version + 1); }); test('should not increment version if rule is prebuilt (immutable === true)', () => { - const { modifiedParams } = ruleParamsModifier({ ...ruleParamsMock, immutable: true }, [ - { - type: BulkActionEditTypeEnum.add_index_patterns, - value: ['my-index-*'], - }, - ]); + const { modifiedParams } = ruleParamsModifier( + { ...ruleParamsMock, immutable: true }, + [ + { + type: BulkActionEditTypeEnum.add_index_patterns, + value: ['my-index-*'], + }, + ], + mockExperimentalFeatures + ); expect(modifiedParams).toHaveProperty('version', ruleParamsMock.version); }); @@ -133,7 +146,8 @@ describe('ruleParamsModifier', () => { type: BulkActionEditTypeEnum.add_index_patterns, value: indexPatternsToAdd, }, - ] + ], + mockExperimentalFeatures ); expect(modifiedParams).toHaveProperty('index', resultingIndexPatterns); expect(isParamsUpdateSkipped).toBe(isUpdateSkipped); @@ -197,7 +211,8 @@ describe('ruleParamsModifier', () => { type: BulkActionEditTypeEnum.delete_index_patterns, value: indexPatternsToDelete, }, - ] + ], + mockExperimentalFeatures ); expect(modifiedParams).toHaveProperty('index', resultingIndexPatterns); expect(isParamsUpdateSkipped).toBe(isUpdateSkipped); @@ -252,7 +267,8 @@ describe('ruleParamsModifier', () => { type: BulkActionEditTypeEnum.set_index_patterns, value: indexPatternsToOverwrite, }, - ] + ], + mockExperimentalFeatures ); expect(modifiedParams).toHaveProperty('index', resultingIndexPatterns); expect(isParamsUpdateSkipped).toBe(isUpdateSkipped); @@ -270,7 +286,8 @@ describe('ruleParamsModifier', () => { type: BulkActionEditTypeEnum.delete_index_patterns, value: ['index-2-*'], }, - ] + ], + mockExperimentalFeatures ); expect(modifiedParams).not.toHaveProperty('index'); expect(isParamsUpdateSkipped).toBe(true); @@ -285,7 +302,8 @@ describe('ruleParamsModifier', () => { value: ['index'], overwrite_data_views: true, }, - ] + ], + mockExperimentalFeatures ); expect(modifiedParams).toHaveProperty('dataViewId', undefined); expect(isParamsUpdateSkipped).toBe(false); @@ -300,7 +318,8 @@ describe('ruleParamsModifier', () => { value: ['index'], overwrite_data_views: true, }, - ] + ], + mockExperimentalFeatures ); expect(modifiedParams).toHaveProperty('dataViewId', undefined); expect(isParamsUpdateSkipped).toBe(false); @@ -315,7 +334,8 @@ describe('ruleParamsModifier', () => { value: ['index'], overwrite_data_views: true, }, - ] + ], + mockExperimentalFeatures ); expect(modifiedParams).toHaveProperty('dataViewId', undefined); expect(modifiedParams).toHaveProperty('index', ['test-*']); @@ -331,7 +351,8 @@ describe('ruleParamsModifier', () => { value: ['index'], overwrite_data_views: true, }, - ] + ], + mockExperimentalFeatures ); expect(modifiedParams).toHaveProperty('dataViewId', undefined); expect(modifiedParams).toHaveProperty('index', undefined); @@ -340,12 +361,16 @@ describe('ruleParamsModifier', () => { test('should throw error on adding index pattern if rule is of machine learning type', () => { expect(() => - ruleParamsModifier({ type: 'machine_learning' } as RuleAlertType['params'], [ - { - type: BulkActionEditTypeEnum.add_index_patterns, - value: ['my-index-*'], - }, - ]) + ruleParamsModifier( + { type: 'machine_learning' } as RuleAlertType['params'], + [ + { + type: BulkActionEditTypeEnum.add_index_patterns, + value: ['my-index-*'], + }, + ], + mockExperimentalFeatures + ) ).toThrow( "Index patterns can't be added. Machine learning rule doesn't have index patterns property" ); @@ -353,12 +378,16 @@ describe('ruleParamsModifier', () => { test('should throw error on deleting index pattern if rule is of machine learning type', () => { expect(() => - ruleParamsModifier({ type: 'machine_learning' } as RuleAlertType['params'], [ - { - type: BulkActionEditTypeEnum.delete_index_patterns, - value: ['my-index-*'], - }, - ]) + ruleParamsModifier( + { type: 'machine_learning' } as RuleAlertType['params'], + [ + { + type: BulkActionEditTypeEnum.delete_index_patterns, + value: ['my-index-*'], + }, + ], + mockExperimentalFeatures + ) ).toThrow( "Index patterns can't be deleted. Machine learning rule doesn't have index patterns property" ); @@ -366,12 +395,16 @@ describe('ruleParamsModifier', () => { test('should throw error on overwriting index pattern if rule is of machine learning type', () => { expect(() => - ruleParamsModifier({ type: 'machine_learning' } as RuleAlertType['params'], [ - { - type: BulkActionEditTypeEnum.set_index_patterns, - value: ['my-index-*'], - }, - ]) + ruleParamsModifier( + { type: 'machine_learning' } as RuleAlertType['params'], + [ + { + type: BulkActionEditTypeEnum.set_index_patterns, + value: ['my-index-*'], + }, + ], + mockExperimentalFeatures + ) ).toThrow( "Index patterns can't be overwritten. Machine learning rule doesn't have index patterns property" ); @@ -379,51 +412,404 @@ describe('ruleParamsModifier', () => { test('should throw error on adding index pattern if rule is of ES|QL type', () => { expect(() => - ruleParamsModifier({ type: 'esql' } as RuleAlertType['params'], [ - { - type: BulkActionEditTypeEnum.add_index_patterns, - value: ['my-index-*'], - }, - ]) + ruleParamsModifier( + { type: 'esql' } as RuleAlertType['params'], + [ + { + type: BulkActionEditTypeEnum.add_index_patterns, + value: ['my-index-*'], + }, + ], + mockExperimentalFeatures + ) ).toThrow("Index patterns can't be added. ES|QL rule doesn't have index patterns property"); }); test('should throw error on deleting index pattern if rule is of ES|QL type', () => { expect(() => - ruleParamsModifier({ type: 'esql' } as RuleAlertType['params'], [ - { - type: BulkActionEditTypeEnum.delete_index_patterns, - value: ['my-index-*'], - }, - ]) + ruleParamsModifier( + { type: 'esql' } as RuleAlertType['params'], + [ + { + type: BulkActionEditTypeEnum.delete_index_patterns, + value: ['my-index-*'], + }, + ], + mockExperimentalFeatures + ) ).toThrow("Index patterns can't be deleted. ES|QL rule doesn't have index patterns property"); }); test('should throw error on overwriting index pattern if rule is of ES|QL type', () => { expect(() => - ruleParamsModifier({ type: 'esql' } as RuleAlertType['params'], [ - { - type: BulkActionEditTypeEnum.set_index_patterns, - value: ['my-index-*'], - }, - ]) + ruleParamsModifier( + { type: 'esql' } as RuleAlertType['params'], + [ + { + type: BulkActionEditTypeEnum.set_index_patterns, + value: ['my-index-*'], + }, + ], + mockExperimentalFeatures + ) ).toThrow( "Index patterns can't be overwritten. ES|QL rule doesn't have index patterns property" ); }); }); + describe('investigation_fields', () => { + describe('add_investigation_fields action', () => { + test.each([ + [ + '3 existing investigation fields + 2 of them = 3 investigation fields', + { + existingInvestigationFields: { field_names: ['field-1', 'field-2', 'field-3'] }, + investigationFieldsToAdd: { field_names: ['field-2', 'field-3'] }, + resultingInvestigationFields: { field_names: ['field-1', 'field-2', 'field-3'] }, + isParamsUpdateSkipped: true, + }, + ], + [ + '3 existing investigation fields + 2 other investigation fields (none of them) = 5 investigation fields', + { + existingInvestigationFields: { field_names: ['field-1', 'field-2', 'field-3'] }, + investigationFieldsToAdd: { field_names: ['field-4', 'field-5'] }, + resultingInvestigationFields: { + field_names: ['field-1', 'field-2', 'field-3', 'field-4', 'field-5'], + }, + isParamsUpdateSkipped: false, + }, + ], + [ + '3 existing investigation fields + 1 of them + 2 other investigation fields (none of them) = 5 investigation fields', + { + existingInvestigationFields: { field_names: ['field-1', 'field-2', 'field-3'] }, + investigationFieldsToAdd: { field_names: ['field-3', 'field-4', 'field-5'] }, + resultingInvestigationFields: { + field_names: ['field-1', 'field-2', 'field-3', 'field-4', 'field-5'], + }, + isParamsUpdateSkipped: false, + }, + ], + [ + '3 existing investigation fields + 0 investigation fields = 3 investigation fields', + { + existingInvestigationFields: { field_names: ['field-1', 'field-2', 'field-3'] }, + investigationFieldsToAdd: { field_names: [] }, + resultingInvestigationFields: { field_names: ['field-1', 'field-2', 'field-3'] }, + isParamsUpdateSkipped: true, + }, + ], + [ + '`undefined` existing investigation fields + 1 investigation field = 1 investigation field', + { + existingInvestigationFields: undefined, + investigationFieldsToAdd: { field_names: ['field-1'] }, + resultingInvestigationFields: { field_names: ['field-1'] }, + isParamsUpdateSkipped: false, + }, + ], + [ + '`undefined` existing investigation fields + 1 investigation field = 1 investigation field', + { + existingInvestigationFields: undefined, + investigationFieldsToAdd: { field_names: ['field-1'] }, + resultingInvestigationFields: { field_names: ['field-1'] }, + isParamsUpdateSkipped: false, + }, + ], + [ + '3 existing `legacy` investigation fields + 2 other investigation fields (none of them) = 5 investigation fields', + { + existingInvestigationFields: ['field-1', 'field-2', 'field-3'], + investigationFieldsToAdd: { field_names: ['field-4', 'field-5'] }, + resultingInvestigationFields: { + field_names: ['field-1', 'field-2', 'field-3', 'field-4', 'field-5'], + }, + isParamsUpdateSkipped: false, + }, + ], + ])( + 'should add investigation fields to rule, case:"%s"', + ( + caseName, + { + existingInvestigationFields, + investigationFieldsToAdd, + resultingInvestigationFields, + isParamsUpdateSkipped, + } + ) => { + const { modifiedParams, isParamsUpdateSkipped: isUpdateSkipped } = ruleParamsModifier( + { + ...ruleParamsMock, + investigationFields: existingInvestigationFields, + } as RuleAlertType['params'], + [ + { + type: BulkActionEditTypeEnum.add_investigation_fields, + value: investigationFieldsToAdd, + }, + ], + mockExperimentalFeatures + ); + expect(modifiedParams).toHaveProperty( + 'investigationFields', + resultingInvestigationFields + ); + expect(isParamsUpdateSkipped).toBe(isUpdateSkipped); + } + ); + }); + + describe('delete_investigation_fields action', () => { + test.each([ + [ + '3 existing investigation fields - 2 of them = 1 investigation field', + { + existingInvestigationFields: { field_names: ['field-1', 'field-2', 'field-3'] }, + investigationFieldsToDelete: { field_names: ['field-2', 'field-3'] }, + resultingInvestigationFields: { field_names: ['field-1'] }, + isParamsUpdateSkipped: false, + }, + ], + [ + '3 existing investigation fields - 2 other investigation fields (none of them) = 3 investigation fields', + { + existingInvestigationFields: { field_names: ['field-1', 'field-2', 'field-3'] }, + investigationFieldsToDelete: { field_names: ['field-4', 'field-5'] }, + resultingInvestigationFields: { field_names: ['field-1', 'field-2', 'field-3'] }, + isParamsUpdateSkipped: true, + }, + ], + [ + '3 existing investigation fields - 1 of them - 2 other investigation fields (none of them) = 2 investigation fields', + { + existingInvestigationFields: { field_names: ['field-1', 'field-2', 'field-3'] }, + investigationFieldsToDelete: { field_names: ['field-3', 'field-4', 'field-5'] }, + resultingInvestigationFields: { field_names: ['field-1', 'field-2'] }, + isParamsUpdateSkipped: false, + }, + ], + [ + '3 existing investigation fields - 0 investigation fields = 3 investigation fields', + { + existingInvestigationFields: { field_names: ['field-1', 'field-2', 'field-3'] }, + investigationFieldsToDelete: { field_names: [] }, + resultingInvestigationFields: { field_names: ['field-1', 'field-2', 'field-3'] }, + isParamsUpdateSkipped: true, + }, + ], + [ + '`undefined` existing investigation fields - 2 of them = `undeinfed` investigation fields', + { + existingInvestigationFields: undefined, + investigationFieldsToDelete: { field_names: ['field-2', 'field-3'] }, + resultingInvestigationFields: undefined, + isParamsUpdateSkipped: true, + }, + ], + [ + '3 existing `legacy` investigation fields - 2 of them = 1 investigation field', + { + existingInvestigationFields: ['field-1', 'field-2', 'field-3'], + investigationFieldsToDelete: { field_names: ['field-2', 'field-3'] }, + resultingInvestigationFields: { field_names: ['field-1'] }, + isParamsUpdateSkipped: false, + }, + ], + ])( + 'should delete investigation fields from rule, case:"%s"', + ( + caseName, + { + existingInvestigationFields, + investigationFieldsToDelete, + resultingInvestigationFields, + isParamsUpdateSkipped, + } + ) => { + const { modifiedParams, isParamsUpdateSkipped: isUpdateSkipped } = ruleParamsModifier( + { + ...ruleParamsMock, + investigationFields: existingInvestigationFields, + } as RuleAlertType['params'], + [ + { + type: BulkActionEditTypeEnum.delete_investigation_fields, + value: investigationFieldsToDelete, + }, + ], + mockExperimentalFeatures + ); + expect(modifiedParams).toHaveProperty( + 'investigationFields', + resultingInvestigationFields + ); + expect(isParamsUpdateSkipped).toBe(isUpdateSkipped); + } + ); + }); + + describe('set_investigation_fields action', () => { + test.each([ + [ + '3 existing investigation fields overwritten with 2 of them = 2 existing investigation fields', + { + existingInvestigationFields: { field_names: ['field-1', 'field-2', 'field-3'] }, + investigationFieldsToOverwrite: { field_names: ['field-2', 'field-3'] }, + resultingInvestigationFields: { field_names: ['field-2', 'field-3'] }, + isParamsUpdateSkipped: false, + }, + ], + [ + '3 existing investigation fields overwritten with 2 other investigation fields = 2 other investigation fields', + { + existingInvestigationFields: { field_names: ['field-1', 'field-2', 'field-3'] }, + investigationFieldsToOverwrite: { field_names: ['field-4', 'field-5'] }, + resultingInvestigationFields: { field_names: ['field-4', 'field-5'] }, + isParamsUpdateSkipped: false, + }, + ], + [ + '3 existing investigation fields overwritten with 1 of them + 2 other investigation fields = 1 existing investigation field + 2 other investigation fields', + { + existingInvestigationFields: { field_names: ['field-1', 'field-2', 'field-3'] }, + investigationFieldsToOverwrite: { field_names: ['field-3', 'field-4', 'field-5'] }, + resultingInvestigationFields: { field_names: ['field-3', 'field-4', 'field-5'] }, + isParamsUpdateSkipped: false, + }, + ], + [ + '`undefined` existing investigation fields overwritten with 2 of them = 2 existing investigation fields', + { + existingInvestigationFields: undefined, + investigationFieldsToOverwrite: { field_names: ['field-2', 'field-3'] }, + resultingInvestigationFields: { field_names: ['field-2', 'field-3'] }, + isParamsUpdateSkipped: false, + }, + ], + [ + '3 existing `legacy` investigation fields overwritten with 1 of them + 2 other investigation fields = 1 existing investigation field + 2 other investigation fields', + { + existingInvestigationFields: ['field-1', 'field-2', 'field-3'], + investigationFieldsToOverwrite: { field_names: ['field-3', 'field-4', 'field-5'] }, + resultingInvestigationFields: { field_names: ['field-3', 'field-4', 'field-5'] }, + isParamsUpdateSkipped: false, + }, + ], + ])( + 'should overwrite investigation fields in rule, case:"%s"', + ( + caseName, + { + existingInvestigationFields, + investigationFieldsToOverwrite, + resultingInvestigationFields, + isParamsUpdateSkipped, + } + ) => { + const { modifiedParams, isParamsUpdateSkipped: isUpdateSkipped } = ruleParamsModifier( + { + ...ruleParamsMock, + investigationFields: existingInvestigationFields, + } as RuleAlertType['params'], + [ + { + type: BulkActionEditTypeEnum.set_investigation_fields, + value: investigationFieldsToOverwrite, + }, + ], + mockExperimentalFeatures + ); + expect(modifiedParams).toHaveProperty( + 'investigationFields', + resultingInvestigationFields + ); + expect(isParamsUpdateSkipped).toBe(isUpdateSkipped); + } + ); + }); + + describe('feature flag disabled state', () => { + test('should throw error on adding investigation fields if feature is disabled', () => { + expect(() => + ruleParamsModifier( + { + ...ruleParamsMock, + investigationFields: ['field-1', 'field-2', 'field-3'], + } as RuleAlertType['params'], + [ + { + type: BulkActionEditTypeEnum.add_investigation_fields, + value: { field_names: ['field-4'] }, + }, + ], + { + bulkCustomHighlightedFieldsEnabled: false, + } as ExperimentalFeatures + ) + ).toThrow("Custom highlighted fields can't be added. Feature is disabled."); + }); + + test('should throw error on overwriting investigation fields if feature is disabled', () => { + expect(() => + ruleParamsModifier( + { + ...ruleParamsMock, + investigationFields: ['field-1', 'field-2', 'field-3'], + } as RuleAlertType['params'], + [ + { + type: BulkActionEditTypeEnum.set_investigation_fields, + value: { field_names: ['field-4'] }, + }, + ], + { + bulkCustomHighlightedFieldsEnabled: false, + } as ExperimentalFeatures + ) + ).toThrow("Custom highlighted fields can't be overwritten. Feature is disabled."); + }); + + test('should throw error on deleting investigation fields if feature is disabled', () => { + expect(() => + ruleParamsModifier( + { + ...ruleParamsMock, + investigationFields: ['field-1', 'field-2', 'field-3'], + } as RuleAlertType['params'], + [ + { + type: BulkActionEditTypeEnum.delete_investigation_fields, + value: { field_names: ['field-1'] }, + }, + ], + { + bulkCustomHighlightedFieldsEnabled: false, + } as ExperimentalFeatures + ) + ).toThrow("Custom highlighted fields can't be deleted. Feature is disabled."); + }); + }); + }); + describe('timeline', () => { test('should set timeline', () => { - const { modifiedParams, isParamsUpdateSkipped } = ruleParamsModifier(ruleParamsMock, [ - { - type: BulkActionEditTypeEnum.set_timeline, - value: { - timeline_id: '91832785-286d-4ebe-b884-1a208d111a70', - timeline_title: 'Test timeline', + const { modifiedParams, isParamsUpdateSkipped } = ruleParamsModifier( + ruleParamsMock, + [ + { + type: BulkActionEditTypeEnum.set_timeline, + value: { + timeline_id: '91832785-286d-4ebe-b884-1a208d111a70', + timeline_title: 'Test timeline', + }, }, - }, - ]); + ], + mockExperimentalFeatures + ); expect(modifiedParams.timelineId).toBe('91832785-286d-4ebe-b884-1a208d111a70'); expect(modifiedParams.timelineTitle).toBe('Test timeline'); @@ -436,15 +822,19 @@ describe('ruleParamsModifier', () => { const INTERVAL_IN_MINUTES = 5; const LOOKBACK_IN_MINUTES = 1; const FROM_IN_SECONDS = (INTERVAL_IN_MINUTES + LOOKBACK_IN_MINUTES) * 60; - const { modifiedParams, isParamsUpdateSkipped } = ruleParamsModifier(ruleParamsMock, [ - { - type: BulkActionEditTypeEnum.set_schedule, - value: { - interval: `${INTERVAL_IN_MINUTES}m`, - lookback: `${LOOKBACK_IN_MINUTES}m`, + const { modifiedParams, isParamsUpdateSkipped } = ruleParamsModifier( + ruleParamsMock, + [ + { + type: BulkActionEditTypeEnum.set_schedule, + value: { + interval: `${INTERVAL_IN_MINUTES}m`, + lookback: `${LOOKBACK_IN_MINUTES}m`, + }, }, - }, - ]); + ], + mockExperimentalFeatures + ); // eslint-disable-next-line @typescript-eslint/no-explicit-any expect((modifiedParams as any).interval).toBeUndefined(); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/rule_params_modifier.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/rule_params_modifier.ts index 6bfdfcf394aac..2cae6218ab76c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/rule_params_modifier.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/rule_params_modifier.ts @@ -8,10 +8,12 @@ import moment from 'moment'; import { parseInterval } from '@kbn/data-plugin/common/search/aggs/utils/date_interval_utils'; import type { RuleParamsModifierResult } from '@kbn/alerting-plugin/server/rules_client/methods/bulk_edit'; -import type { RuleAlertType } from '../../../rule_schema'; +import type { ExperimentalFeatures } from '../../../../../../common'; +import type { InvestigationFieldsCombined, RuleAlertType } from '../../../rule_schema'; import type { BulkActionEditForRuleParams, BulkActionEditPayloadIndexPatterns, + BulkActionEditPayloadInvestigationFields, } from '../../../../../../common/api/detection_engine/rule_management'; import { BulkActionEditTypeEnum } from '../../../../../../common/api/detection_engine/rule_management'; import { invariant } from '../../../../../../common/utils/invariant'; @@ -63,9 +65,52 @@ const shouldSkipIndexPatternsBulkAction = ( return false; }; +// Check if the investigation fields added to the rule already exist in it +const hasInvestigationFields = ( + investigationFields: InvestigationFieldsCombined | undefined, + action: BulkActionEditPayloadInvestigationFields +) => + action.value.field_names.every((field) => + (Array.isArray(investigationFields) + ? investigationFields + : investigationFields?.field_names ?? [] + ).includes(field) + ); + +// Check if the investigation fields to be deleted don't exist in the rule +const hasNoInvestigationFields = ( + investigationFields: InvestigationFieldsCombined | undefined, + action: BulkActionEditPayloadInvestigationFields +) => + action.value.field_names.every( + (field) => + !( + Array.isArray(investigationFields) + ? investigationFields + : investigationFields?.field_names ?? [] + ).includes(field) + ); + +const shouldSkipInvestigationFieldsBulkAction = ( + investigationFields: InvestigationFieldsCombined | undefined, + action: BulkActionEditPayloadInvestigationFields +) => { + if (action.type === BulkActionEditTypeEnum.add_investigation_fields) { + return hasInvestigationFields(investigationFields, action); + } + + if (action.type === BulkActionEditTypeEnum.delete_investigation_fields) { + return hasNoInvestigationFields(investigationFields, action); + } + + return false; +}; + +// eslint-disable-next-line complexity const applyBulkActionEditToRuleParams = ( existingRuleParams: RuleAlertType['params'], - action: BulkActionEditForRuleParams + action: BulkActionEditForRuleParams, + experimentalFeatures: ExperimentalFeatures ): { ruleParams: RuleAlertType['params']; isActionSkipped: boolean; @@ -151,6 +196,69 @@ const applyBulkActionEditToRuleParams = ( ruleParams.index = action.value; break; } + // investigation_fields actions + case BulkActionEditTypeEnum.add_investigation_fields: { + invariant( + experimentalFeatures.bulkCustomHighlightedFieldsEnabled, + "Custom highlighted fields can't be added. Feature is disabled." + ); + + if (shouldSkipInvestigationFieldsBulkAction(ruleParams.investigationFields, action)) { + isActionSkipped = true; + break; + } + + ruleParams.investigationFields = { + field_names: addItemsToArray( + (Array.isArray(ruleParams.investigationFields) + ? ruleParams.investigationFields + : ruleParams.investigationFields?.field_names) ?? [], + action.value.field_names + ), + }; + break; + } + case BulkActionEditTypeEnum.delete_investigation_fields: { + invariant( + experimentalFeatures.bulkCustomHighlightedFieldsEnabled, + "Custom highlighted fields can't be deleted. Feature is disabled." + ); + + if (shouldSkipInvestigationFieldsBulkAction(ruleParams.investigationFields, action)) { + isActionSkipped = true; + break; + } + + if (ruleParams.investigationFields) { + const fieldNames = deleteItemsFromArray( + (Array.isArray(ruleParams.investigationFields) + ? ruleParams.investigationFields + : ruleParams.investigationFields?.field_names) ?? [], + action.value.field_names + ); + ruleParams.investigationFields = + fieldNames.length > 0 + ? { + field_names: fieldNames, + } + : undefined; + } + break; + } + case BulkActionEditTypeEnum.set_investigation_fields: { + invariant( + experimentalFeatures.bulkCustomHighlightedFieldsEnabled, + "Custom highlighted fields can't be overwritten. Feature is disabled." + ); + + if (shouldSkipInvestigationFieldsBulkAction(ruleParams.investigationFields, action)) { + isActionSkipped = true; + break; + } + + ruleParams.investigationFields = action.value; + break; + } // timeline actions case BulkActionEditTypeEnum.set_timeline: { ruleParams = { @@ -192,12 +300,17 @@ const applyBulkActionEditToRuleParams = ( */ export const ruleParamsModifier = ( existingRuleParams: RuleAlertType['params'], - actions: BulkActionEditForRuleParams[] + actions: BulkActionEditForRuleParams[], + experimentalFeatures: ExperimentalFeatures ): RuleParamsModifierResult => { let isParamsUpdateSkipped = true; const modifiedParams = actions.reduce((acc, action) => { - const { ruleParams, isActionSkipped } = applyBulkActionEditToRuleParams(acc, action); + const { ruleParams, isActionSkipped } = applyBulkActionEditToRuleParams( + acc, + action, + experimentalFeatures + ); // The rule was updated with at least one action, so mark our rule as updated if (!isActionSkipped) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/utils.ts index 18634fb7162b7..08b3487ede61f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/utils.ts @@ -21,3 +21,18 @@ export const isIndexPatternsBulkEditAction = (editAction: BulkActionEditType) => ]; return indexPatternsActions.includes(editAction); }; + +/** + * helper utility that defines whether bulk edit action is related to investigation fields, i.e. one of: + * 'add_investigation_fields', 'delete_investigation_fields', 'set_investigation_fields' + * @param editAction {@link BulkActionEditType} + * @returns {boolean} + */ +export const isInvestigationFieldsBulkEditAction = (editAction: BulkActionEditType) => { + const investigationFieldsActions: BulkActionEditType[] = [ + BulkActionEditTypeEnum.add_investigation_fields, + BulkActionEditTypeEnum.delete_investigation_fields, + BulkActionEditTypeEnum.set_investigation_fields, + ]; + return investigationFieldsActions.includes(editAction); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/validations.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/validations.ts index 806c90e41ac12..092808cf1cc5c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/validations.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/validations.ts @@ -6,6 +6,7 @@ */ import type { Type as RuleType } from '@kbn/securitysolution-io-ts-alerting-types'; +import type { ExperimentalFeatures } from '../../../../../../common'; import { invariant } from '../../../../../../common/utils/invariant'; import { isMlRule } from '../../../../../../common/machine_learning/helpers'; import { isEsqlRule } from '../../../../../../common/detection_engine/utils'; @@ -16,7 +17,7 @@ import type { } from '../../../../../../common/api/detection_engine/rule_management'; import { BulkActionEditTypeEnum } from '../../../../../../common/api/detection_engine/rule_management'; import type { RuleAlertType } from '../../../rule_schema'; -import { isIndexPatternsBulkEditAction } from './utils'; +import { isIndexPatternsBulkEditAction, isInvestigationFieldsBulkEditAction } from './utils'; import { throwDryRunError } from './dry_run'; import type { MlAuthz } from '../../../../machine_learning/authz'; import { throwAuthzError } from '../../../../machine_learning/validation'; @@ -37,6 +38,7 @@ interface DryRunBulkEditBulkActionsValidationArgs { rule: RuleAlertType; mlAuthz: MlAuthz; edit: BulkActionEditPayload[]; + experimentalFeatures: ExperimentalFeatures; } /** @@ -113,6 +115,7 @@ export const dryRunValidateBulkEditRule = async ({ rule, edit, mlAuthz, + experimentalFeatures, }: DryRunBulkEditBulkActionsValidationArgs) => { await validateBulkEditRule({ ruleType: rule.params.type, @@ -142,4 +145,15 @@ export const dryRunValidateBulkEditRule = async ({ ), BulkActionsDryRunErrCode.ESQL_INDEX_PATTERN ); + + // check whether "custom highlighted fields" feature is enabled + await throwDryRunError( + () => + invariant( + experimentalFeatures.bulkCustomHighlightedFieldsEnabled || + !edit.some((action) => isInvestigationFieldsBulkEditAction(action.type)), + 'Bulk custom highlighted fields action feature is disabled.' + ), + BulkActionsDryRunErrCode.INVESTIGATION_FIELDS_FEATURE + ); }; diff --git a/x-pack/test/security_solution_api_integration/config/ess/config.base.ts b/x-pack/test/security_solution_api_integration/config/ess/config.base.ts index c2fe5a723627f..cc47b97377e6c 100644 --- a/x-pack/test/security_solution_api_integration/config/ess/config.base.ts +++ b/x-pack/test/security_solution_api_integration/config/ess/config.base.ts @@ -82,6 +82,7 @@ export function createTestConfig(options: CreateTestConfigOptions, testFiles?: s 'previewTelemetryUrlEnabled', 'riskScoringPersistence', 'riskScoringRoutesEnabled', + 'bulkCustomHighlightedFieldsEnabled', ])}`, '--xpack.task_manager.poll_interval=1000', `--xpack.actions.preconfigured=${JSON.stringify(PRECONFIGURED_ACTION_CONNECTORS)}`, diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/configs/serverless.config.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/configs/serverless.config.ts index 137ee1f67b9b3..f6ba7fa49895e 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/configs/serverless.config.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/configs/serverless.config.ts @@ -17,5 +17,8 @@ export default createTestConfig({ 'testing_ignored.constant', '/testing_regex*/', ])}`, // See tests within the file "ignore_fields.ts" which use these values in "alertIgnoreFields" + `--xpack.securitySolution.enableExperimental=${JSON.stringify([ + 'bulkCustomHighlightedFieldsEnabled', + ])}`, ], }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action.ts index fb7543b9fe700..2a833ff72bbaa 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action.ts @@ -1151,6 +1151,210 @@ export default ({ getService }: FtrProviderContext): void => { ); }); + describe('investigation fields actions', () => { + it('should set investigation fields in rules', async () => { + const ruleId = 'ruleId'; + await createRule(supertest, log, getSimpleRule(ruleId)); + + const { body: bulkEditResponse } = await securitySolutionApi + .performBulkAction({ + query: {}, + body: { + query: '', + action: BulkActionTypeEnum.edit, + [BulkActionTypeEnum.edit]: [ + { + type: BulkActionEditTypeEnum.set_investigation_fields, + value: { field_names: ['field-1'] }, + }, + ], + }, + }) + .expect(200); + + expect(bulkEditResponse.attributes.summary).to.eql({ + failed: 0, + skipped: 0, + succeeded: 1, + total: 1, + }); + + // Check that the updated rule is returned with the response + expect(bulkEditResponse.attributes.results.updated[0].investigation_fields).to.eql({ + field_names: ['field-1'], + }); + + // Check that the updates have been persisted + const { body: updatedRule } = await fetchRule(ruleId).expect(200); + + expect(updatedRule.investigation_fields).to.eql({ field_names: ['field-1'] }); + }); + + it('should add investigation fields to rules', async () => { + const ruleId = 'ruleId'; + const investigationFields = { field_names: ['field-1', 'field-2'] }; + const resultingFields = { field_names: ['field-1', 'field-2', 'field-3'] }; + await createRule(supertest, log, { + ...getSimpleRule(ruleId), + investigation_fields: investigationFields, + }); + + const { body: bulkEditResponse } = await securitySolutionApi + .performBulkAction({ + query: {}, + body: { + query: '', + action: BulkActionTypeEnum.edit, + [BulkActionTypeEnum.edit]: [ + { + type: BulkActionEditTypeEnum.add_investigation_fields, + value: { field_names: ['field-3'] }, + }, + ], + }, + }) + .expect(200); + + expect(bulkEditResponse.attributes.summary).to.eql({ + failed: 0, + skipped: 0, + succeeded: 1, + total: 1, + }); + + // Check that the updated rule is returned with the response + expect(bulkEditResponse.attributes.results.updated[0].investigation_fields).to.eql( + resultingFields + ); + + // Check that the updates have been persisted + const { body: updatedRule } = await fetchRule(ruleId).expect(200); + + expect(updatedRule.investigation_fields).to.eql(resultingFields); + }); + + it('should delete investigation fields from rules', async () => { + const ruleId = 'ruleId'; + const investigationFields = { field_names: ['field-1', 'field-2'] }; + const resultingFields = { field_names: ['field-1'] }; + await createRule(supertest, log, { + ...getSimpleRule(ruleId), + investigation_fields: investigationFields, + }); + + const { body: bulkEditResponse } = await securitySolutionApi + .performBulkAction({ + query: {}, + body: { + query: '', + action: BulkActionTypeEnum.edit, + [BulkActionTypeEnum.edit]: [ + { + type: BulkActionEditTypeEnum.delete_investigation_fields, + value: { field_names: ['field-2'] }, + }, + ], + }, + }) + .expect(200); + + expect(bulkEditResponse.attributes.summary).to.eql({ + failed: 0, + skipped: 0, + succeeded: 1, + total: 1, + }); + + // Check that the updated rule is returned with the response + expect(bulkEditResponse.attributes.results.updated[0].investigation_fields).to.eql( + resultingFields + ); + + // Check that the updates have been persisted + const { body: updatedRule } = await fetchRule(ruleId).expect(200); + + expect(updatedRule.investigation_fields).to.eql(resultingFields); + }); + + const skipIndexPatternsUpdateCases = [ + // Delete no-ops + { + caseName: '0 existing fields - 2 fields = 0 fields', + existingInvestigationFields: undefined, + investigationFieldsToUpdate: { field_names: ['field-1', 'field-2'] }, + resultingInvestigationFields: undefined, + operation: BulkActionEditTypeEnum.delete_investigation_fields, + }, + { + caseName: '3 existing fields - 2 other fields (none of them) = 3 fields', + existingInvestigationFields: { field_names: ['field-1', 'field-2', 'field-3'] }, + investigationFieldsToUpdate: { field_names: ['field-8', 'field-9'] }, + resultingInvestigationFields: { field_names: ['field-1', 'field-2', 'field-3'] }, + operation: BulkActionEditTypeEnum.delete_investigation_fields, + }, + // Add no-ops + { + caseName: '3 existing fields + 2 exisiting fields= 3 fields', + existingInvestigationFields: { field_names: ['field-1', 'field-2', 'field-3'] }, + investigationFieldsToUpdate: { field_names: ['field-1', 'field-2'] }, + resultingInvestigationFields: { field_names: ['field-1', 'field-2', 'field-3'] }, + operation: BulkActionEditTypeEnum.add_investigation_fields, + }, + ]; + + skipIndexPatternsUpdateCases.forEach( + ({ + caseName, + existingInvestigationFields, + investigationFieldsToUpdate, + resultingInvestigationFields, + operation, + }) => { + it(`should skip rule updated for investigation fields, case: "${caseName}"`, async () => { + const ruleId = 'ruleId'; + + await createRule(supertest, log, { + ...getSimpleRule(ruleId), + investigation_fields: existingInvestigationFields, + }); + + const { body: bulkEditResponse } = await securitySolutionApi + .performBulkAction({ + query: {}, + body: { + query: '', + action: BulkActionTypeEnum.edit, + [BulkActionTypeEnum.edit]: [ + { + type: operation, + value: investigationFieldsToUpdate, + }, + ], + }, + }) + .expect(200); + + expect(bulkEditResponse.attributes.summary).to.eql({ + failed: 0, + skipped: 1, + succeeded: 0, + total: 1, + }); + + // Check that the rules is returned as skipped with expected skip reason + expect(bulkEditResponse.attributes.results.skipped[0].skip_reason).to.eql( + 'RULE_NOT_MODIFIED' + ); + + // Check that the no changes have been persisted + const { body: updatedRule } = await fetchRule(ruleId).expect(200); + + expect(updatedRule.investigation_fields).to.eql(resultingInvestigationFields); + }); + } + ); + }); + it('should set timeline template values in rule', async () => { const ruleId = 'ruleId'; const timelineId = '91832785-286d-4ebe-b884-1a208d111a70'; diff --git a/x-pack/test/security_solution_cypress/config.ts b/x-pack/test/security_solution_cypress/config.ts index 2a7dad8cd8559..588843b731f45 100644 --- a/x-pack/test/security_solution_cypress/config.ts +++ b/x-pack/test/security_solution_cypress/config.ts @@ -44,6 +44,9 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { // See https://github.com/elastic/kibana/pull/125396 for details '--xpack.alerting.rules.minimumScheduleInterval.value=1s', '--xpack.ruleRegistry.unsafe.legacyMultiTenancy.enabled=true', + `--xpack.securitySolution.enableExperimental=${JSON.stringify([ + 'bulkCustomHighlightedFieldsEnabled', + ])}`, // mock cloud to enable the guided onboarding tour in e2e tests '--xpack.cloud.id=test', `--home.disableWelcomeScreen=true`, diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_edit_rules.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_edit_rules.cy.ts index 573fc2c556abe..4584c0d5719e5 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_edit_rules.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_edit_rules.cy.ts @@ -20,9 +20,13 @@ import { TAGS_RULE_BULK_MENU_ITEM, INDEX_PATTERNS_RULE_BULK_MENU_ITEM, APPLY_TIMELINE_RULE_BULK_MENU_ITEM, + RULES_BULK_EDIT_INVESTIGATION_FIELDS_WARNING, } from '../../../../../screens/rules_bulk_actions'; -import { TIMELINE_TEMPLATE_DETAILS } from '../../../../../screens/rule_details'; +import { + INVESTIGATION_FIELDS_DETAILS, + TIMELINE_TEMPLATE_DETAILS, +} from '../../../../../screens/rule_details'; import { EUI_CHECKBOX, EUI_FILTER_SELECT_ITEM } from '../../../../../screens/common/controls'; @@ -72,10 +76,19 @@ import { assertRuleScheduleValues, assertUpdateScheduleWarningExists, assertDefaultValuesAreAppliedToScheduleFields, + openBulkEditAddInvestigationFieldsForm, + typeInvestigationFields, + checkOverwriteInvestigationFieldsCheckbox, + openBulkEditDeleteInvestigationFieldsForm, } from '../../../../../tasks/rules_bulk_actions'; import { createRuleAssetSavedObject } from '../../../../../helpers/rules'; -import { hasIndexPatterns, getDetails } from '../../../../../tasks/rule_details'; +import { + hasIndexPatterns, + getDetails, + hasInvestigationFields, + assertDetailsNotExist, +} from '../../../../../tasks/rule_details'; import { login } from '../../../../../tasks/login'; import { visitRulesManagementTable } from '../../../../../tasks/rules_management'; import { createRule } from '../../../../../tasks/api_calls/rules'; @@ -102,14 +115,16 @@ import { setRowsPerPageTo, sortByTableColumn } from '../../../../../tasks/table_ const RULE_NAME = 'Custom rule for bulk actions'; const EUI_SELECTABLE_LIST_ITEM_SR_TEXT = '. To check this option, press Enter.'; -const prePopulatedIndexPatterns = ['index-1-*', 'index-2-*']; +const prePopulatedIndexPatterns = ['index-1-*', 'index-2-*', 'auditbeat-*']; const prePopulatedTags = ['test-default-tag-1', 'test-default-tag-2']; +const prePopulatedInvestigationFields = ['agent.version', 'host.name']; const expectedNumberOfMachineLearningRulesToBeEdited = 1; const defaultRuleData = { index: prePopulatedIndexPatterns, tags: prePopulatedTags, + investigation_fields: { field_names: prePopulatedInvestigationFields }, timeline_title: 'Generic Threat Match Timeline', timeline_id: '495ad7a7-316e-4544-8a0f-9c098daee76e', }; @@ -129,6 +144,7 @@ describe('Detection rules, bulk edit', { tags: ['@ess', '@serverless'] }, () => getMachineLearningRule({ name: 'New ML Rule Test', tags: ['test-default-tag-1', 'test-default-tag-2'], + investigation_fields: { field_names: prePopulatedInvestigationFields }, enabled: false, }) ); @@ -562,6 +578,86 @@ describe('Detection rules, bulk edit', { tags: ['@ess', '@serverless'] }, () => }); }); + describe('Investigation fields actions', () => { + it('Add investigation fields to custom rules', () => { + getRulesManagementTableRows().then((rows) => { + const fieldsToBeAdded = ['source.ip', 'destination.ip']; + const resultingFields = [...prePopulatedInvestigationFields, ...fieldsToBeAdded]; + + selectAllRules(); + + // open add custom highlighted fields form and add 2 new fields + openBulkEditAddInvestigationFieldsForm(); + typeInvestigationFields(fieldsToBeAdded); + submitBulkEditForm(); + waitForBulkEditActionToFinish({ updatedCount: rows.length }); + + // check if rule has been updated + goToRuleDetailsOf(RULE_NAME); + hasInvestigationFields(resultingFields.join('')); + }); + }); + + it('Overwrite investigation fields in custom rules', () => { + getRulesManagementTableRows().then((rows) => { + const fieldsToOverwrite = ['source.ip']; + + selectAllRules(); + + // open add tags form, check overwrite tags and warning message, type tags + openBulkEditAddInvestigationFieldsForm(); + checkOverwriteInvestigationFieldsCheckbox(); + + cy.get(RULES_BULK_EDIT_INVESTIGATION_FIELDS_WARNING).should( + 'have.text', + `You’re about to overwrite custom highlighted fields for ${rows.length} selected rules, press Save to apply changes.` + ); + + typeInvestigationFields(fieldsToOverwrite); + submitBulkEditForm(); + waitForBulkEditActionToFinish({ updatedCount: rows.length }); + + // check if rule has been updated + goToRuleDetailsOf(RULE_NAME); + hasInvestigationFields(fieldsToOverwrite.join('')); + }); + }); + + it('Delete investigation fields from custom rules', () => { + getRulesManagementTableRows().then((rows) => { + const fieldsToDelete = prePopulatedInvestigationFields.slice(0, 1); + const resultingFields = prePopulatedInvestigationFields.slice(1); + + selectAllRules(); + + // open add tags form, check overwrite tags, type tags + openBulkEditDeleteInvestigationFieldsForm(); + typeInvestigationFields(fieldsToDelete); + submitBulkEditForm(); + waitForBulkEditActionToFinish({ updatedCount: rows.length }); + + // check if rule has been updated + goToRuleDetailsOf(RULE_NAME); + hasInvestigationFields(resultingFields.join('')); + }); + }); + + it('Delete all investigation fields from custom rules', () => { + getRulesManagementTableRows().then((rows) => { + selectAllRules(); + + openBulkEditDeleteInvestigationFieldsForm(); + typeInvestigationFields(prePopulatedInvestigationFields); + submitBulkEditForm(); + waitForBulkEditActionToFinish({ updatedCount: rows.length }); + + // check if rule has been updated + goToRuleDetailsOf(RULE_NAME); + assertDetailsNotExist(INVESTIGATION_FIELDS_DETAILS); + }); + }); + }); + describe('Timeline templates', () => { beforeEach(() => { loadPrepackagedTimelineTemplates(); diff --git a/x-pack/test/security_solution_cypress/cypress/screens/rule_details.ts b/x-pack/test/security_solution_cypress/cypress/screens/rule_details.ts index b8c524b0084ce..056f0231912e4 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/rule_details.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/rule_details.ts @@ -54,6 +54,8 @@ export const FALSE_POSITIVES_DETAILS = 'False positive examples'; export const INDEX_PATTERNS_DETAILS = 'Index patterns'; +export const INVESTIGATION_FIELDS_DETAILS = 'Custom highlighted fields'; + export const ENDPOINT_EXCEPTIONS_TAB = 'a[data-test-subj="navigation-endpoint_exceptions"]'; export const INDICATOR_INDEX_PATTERNS = 'Indicator index patterns'; diff --git a/x-pack/test/security_solution_cypress/cypress/screens/rules_bulk_actions.ts b/x-pack/test/security_solution_cypress/cypress/screens/rules_bulk_actions.ts index 69efa1d31c2a7..cdf458a1e9ad8 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/rules_bulk_actions.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/rules_bulk_actions.ts @@ -83,6 +83,25 @@ export const RULES_BULK_EDIT_OVERWRITE_TAGS_CHECKBOX = export const RULES_BULK_EDIT_TAGS_WARNING = '[data-test-subj="bulkEditRulesTagsWarning"]'; +// INVESTIGATION FIELDS +export const INVESTIGATION_FIELDS_RULE_BULK_MENU_ITEM = + '[data-test-subj="investigationFieldsBulkEditRule"]'; + +export const ADD_INVESTIGATION_FIELDS_RULE_BULK_MENU_ITEM = + '[data-test-subj="addInvestigationFieldsBulkEditRule"]'; + +export const DELETE_INVESTIGATION_FIELDS_RULE_BULK_MENU_ITEM = + '[data-test-subj="deleteInvestigationFieldsBulkEditRule"]'; + +export const RULES_BULK_EDIT_INVESTIGATION_FIELDS = + '[data-test-subj="bulkEditRulesInvestigationFields"]'; + +export const RULES_BULK_EDIT_OVERWRITE_INVESTIGATION_FIELDS_CHECKBOX = + '[data-test-subj="bulkEditRulesOverwriteInvestigationFields"]'; + +export const RULES_BULK_EDIT_INVESTIGATION_FIELDS_WARNING = + '[data-test-subj="bulkEditRulesInvestigationFieldsWarning"]'; + // ENABLE/DISABLE export const ENABLE_RULE_BULK_BTN = '[data-test-subj="enableRuleBulk"]'; diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/rule_details.ts b/x-pack/test/security_solution_cypress/cypress/tasks/rule_details.ts index 172408aa677c4..b5b82d78783c4 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/rule_details.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/rule_details.ts @@ -34,6 +34,8 @@ import { EXCEPTIONS_TAB_EXPIRED_FILTER, EXCEPTIONS_TAB_ACTIVE_FILTER, RULE_NAME_HEADER, + INVESTIGATION_FIELDS_DETAILS, + ABOUT_DETAILS, } from '../screens/rule_details'; import { RuleDetailsTabs, ruleDetailsUrl } from '../urls/rule_details'; import { @@ -179,6 +181,12 @@ export const hasIndexPatterns = (indexPatterns: string) => { }); }; +export const hasInvestigationFields = (fields: string) => { + cy.get(ABOUT_DETAILS).within(() => { + getDetails(INVESTIGATION_FIELDS_DETAILS).should('have.text', fields); + }); +}; + export const goToRuleEditSettings = () => { cy.get(EDIT_RULE_SETTINGS_LINK).click(); }; diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/rules_bulk_actions.ts b/x-pack/test/security_solution_cypress/cypress/tasks/rules_bulk_actions.ts index 79960f6aa7464..c74214c60fbac 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/rules_bulk_actions.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/rules_bulk_actions.ts @@ -21,6 +21,7 @@ import { import { EUI_SELECTABLE_LIST_ITEM, TIMELINE_SEARCHBOX } from '../screens/common/controls'; import { ADD_INDEX_PATTERNS_RULE_BULK_MENU_ITEM, + ADD_INVESTIGATION_FIELDS_RULE_BULK_MENU_ITEM, ADD_RULE_ACTIONS_MENU_ITEM, ADD_TAGS_RULE_BULK_MENU_ITEM, APPLY_TIMELINE_RULE_BULK_MENU_ITEM, @@ -28,18 +29,22 @@ import { BULK_ACTIONS_PROGRESS_BTN, BULK_EXPORT_ACTION_BTN, DELETE_INDEX_PATTERNS_RULE_BULK_MENU_ITEM, + DELETE_INVESTIGATION_FIELDS_RULE_BULK_MENU_ITEM, DELETE_RULE_BULK_BTN, DELETE_TAGS_RULE_BULK_MENU_ITEM, DISABLE_RULE_BULK_BTN, DUPLICATE_RULE_BULK_BTN, ENABLE_RULE_BULK_BTN, INDEX_PATTERNS_RULE_BULK_MENU_ITEM, + INVESTIGATION_FIELDS_RULE_BULK_MENU_ITEM, RULES_BULK_EDIT_FORM_CONFIRM_BTN, RULES_BULK_EDIT_FORM_TITLE, RULES_BULK_EDIT_INDEX_PATTERNS, + RULES_BULK_EDIT_INVESTIGATION_FIELDS, RULES_BULK_EDIT_OVERWRITE_ACTIONS_CHECKBOX, RULES_BULK_EDIT_OVERWRITE_DATA_VIEW_CHECKBOX, RULES_BULK_EDIT_OVERWRITE_INDEX_PATTERNS_CHECKBOX, + RULES_BULK_EDIT_OVERWRITE_INVESTIGATION_FIELDS_CHECKBOX, RULES_BULK_EDIT_OVERWRITE_TAGS_CHECKBOX, RULES_BULK_EDIT_SCHEDULES_WARNING, RULES_BULK_EDIT_TAGS, @@ -232,6 +237,46 @@ export const checkTagsInTagsFilter = (tags: string[], srOnlyText: string = '') = }); }; +// EDIT-INVESTIGATION FIELDS +const clickInvestigationFieldsMenuItem = () => { + cy.get(BULK_ACTIONS_BTN).click(); + cy.get(INVESTIGATION_FIELDS_RULE_BULK_MENU_ITEM).click(); +}; + +export const clickAddInvestigationFieldsMenuItem = () => { + clickInvestigationFieldsMenuItem(); + cy.get(ADD_INVESTIGATION_FIELDS_RULE_BULK_MENU_ITEM).click(); +}; + +export const openBulkEditAddInvestigationFieldsForm = () => { + clickAddInvestigationFieldsMenuItem(); + + cy.get(RULES_BULK_EDIT_FORM_TITLE).should('have.text', 'Add custom highlighted fields'); +}; + +export const openBulkEditDeleteInvestigationFieldsForm = () => { + clickInvestigationFieldsMenuItem(); + cy.get(DELETE_INVESTIGATION_FIELDS_RULE_BULK_MENU_ITEM).click(); + + cy.get(RULES_BULK_EDIT_FORM_TITLE).should('have.text', 'Delete custom highlighted fields'); +}; + +export const typeInvestigationFields = (fields: string[]) => { + cy.get(RULES_BULK_EDIT_INVESTIGATION_FIELDS) + .find('input') + .type(fields.join('{enter}') + '{enter}'); +}; + +export const checkOverwriteInvestigationFieldsCheckbox = () => { + cy.get(RULES_BULK_EDIT_OVERWRITE_INVESTIGATION_FIELDS_CHECKBOX) + .should('have.text', "Overwrite all selected rules' custom highlighted fields") + .click(); + cy.get(RULES_BULK_EDIT_OVERWRITE_INVESTIGATION_FIELDS_CHECKBOX) + .should('have.text', "Overwrite all selected rules' custom highlighted fields") + .get('input') + .should('be.checked'); +}; + // EDIT-SCHEDULE export const clickUpdateScheduleMenuItem = () => { cy.get(BULK_ACTIONS_BTN).click(); diff --git a/x-pack/test/security_solution_cypress/serverless_config.ts b/x-pack/test/security_solution_cypress/serverless_config.ts index d0ee1613f6e4c..51462be717fc8 100644 --- a/x-pack/test/security_solution_cypress/serverless_config.ts +++ b/x-pack/test/security_solution_cypress/serverless_config.ts @@ -34,6 +34,9 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { { product_line: 'endpoint', product_tier: 'complete' }, { product_line: 'cloud', product_tier: 'complete' }, ])}`, + `--xpack.securitySolution.enableExperimental=${JSON.stringify([ + 'bulkCustomHighlightedFieldsEnabled', + ])}`, ], }, testRunner: SecuritySolutionConfigurableCypressTestRunner,