[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:


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 <snowmiser111@gmail.com>
This commit is contained in:
Ievgen Sorokopud 2024-05-02 18:10:51 +02:00 committed by GitHub
parent 887bb7d49b
commit b4b316a720
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 1457 additions and 94 deletions

View file

@ -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<typeof BulkActionEditPayloadTimeline>;
export const BulkActionEditPayloadTimeline = z.object({
type: z.literal('set_timeline'),
@ -252,6 +269,7 @@ export type BulkActionEditPayload = z.infer<typeof BulkActionEditPayload>;
export const BulkActionEditPayload = z.union([
BulkActionEditPayloadTags,
BulkActionEditPayloadIndexPatterns,
BulkActionEditPayloadInvestigationFields,
BulkActionEditPayloadTimeline,
BulkActionEditPayloadRuleActions,
BulkActionEditPayloadSchedule,

View file

@ -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'

View file

@ -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"`
);
});

View file

@ -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;

View file

@ -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;

View file

@ -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<keyof ExperimentalFeatures>;

View file

@ -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 {

View file

@ -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 <TagsForm {...props} editAction={editAction} />;
case BulkActionEditTypeEnum.add_investigation_fields:
case BulkActionEditTypeEnum.delete_investigation_fields:
case BulkActionEditTypeEnum.set_investigation_fields:
return <InvestigationFieldsForm {...props} editAction={editAction} />;
case BulkActionEditTypeEnum.set_timeline:
return <TimelineTemplateForm {...props} />;

View file

@ -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<InvestigationFieldsFormData> = {
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<string[]>(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 (
<BulkEditFormWrapper form={form} onClose={onClose} onSubmit={handleSubmit} title={formTitle}>
<CommonUseField
path="investigationFields"
config={{ ...schema.investigationFields, label: indexLabel, helpText: indexHelpText }}
componentProps={{
idAria: 'bulkEditRulesInvestigationFields',
'data-test-subj': 'bulkEditRulesInvestigationFields',
euiFieldProps: {
fullWidth: true,
placeholder: '',
noSuggestions: false,
options: fieldOptions,
},
}}
/>
{editAction === BulkActionEditTypeEnum.add_investigation_fields && (
<CommonUseField
path="overwrite"
componentProps={{
idAria: 'bulkEditRulesOverwriteInvestigationFields',
'data-test-subj': 'bulkEditRulesOverwriteInvestigationFields',
}}
/>
)}
{overwrite && (
<EuiFormRow fullWidth>
<EuiCallOut
color="warning"
size="s"
data-test-subj="bulkEditRulesInvestigationFieldsWarning"
>
<FormattedMessage
id="xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.setInvestigationFieldsWarningCallout"
defaultMessage="Youre about to overwrite custom highlighted fields for {rulesCount, plural, one {# selected rule} other {# selected rules}}, press Save to
apply changes."
values={{ rulesCount }}
/>
</EuiCallOut>
</EuiFormRow>
)}
</BulkEditFormWrapper>
);
};
export const InvestigationFieldsForm = React.memo(InvestigationFieldsFormComponent);
InvestigationFieldsForm.displayName = 'InvestigationFieldsForm';

View file

@ -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,

View file

@ -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, []],

View file

@ -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:

View file

@ -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',
{

View file

@ -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);

View file

@ -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<typeof createMockConfig>;
let server: ReturnType<typeof serverMock.create>;
let { clients, context } = requestContextMock.createTools();
let ml: ReturnType<typeof mlServicesMock.createSetupContract>;
@ -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', () => {

View file

@ -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;
},

View file

@ -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);
},
});

View file

@ -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();

View file

@ -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<RuleAlertType['params']> => {
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) {

View file

@ -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);
};

View file

@ -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
);
};

View file

@ -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)}`,

View file

@ -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',
])}`,
],
});

View file

@ -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';

View file

@ -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`,

View file

@ -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',
`Youre 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();

View file

@ -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';

View file

@ -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"]';

View file

@ -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();
};

View file

@ -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();

View file

@ -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,