From d0e4ce55006a617432b83e53d79f09d174e7a909 Mon Sep 17 00:00:00 2001 From: Vitalii Dmyterko <92328789+vitaliidm@users.noreply.github.com> Date: Thu, 19 Jun 2025 15:20:12 +0100 Subject: [PATCH 1/4] [Security Solution][Detection Engine] adds simplified bulk edit for alert suppression (#223090) ## Summary - addresses https://github.com/elastic/security-team/issues/9190 (issue's description does not contain details, for product requirements refer to https://github.com/elastic/security-team/issues/9190#issuecomment-2943723763) - adds simplified bulk editing, when user can only overwrite or remove alert suppression for multiple rules ### DEMO https://github.com/user-attachments/assets/88dc2953-e3fa-44c3-b896-ff533c66553f ### Feature flag ```yml xpack.securitySolution.enableExperimental: - bulkEditAlertSuppressionEnabled ``` ### Flaky test runner FTR - https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/8360 Cypress - https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/8361 ### Docs issue https://github.com/elastic/docs-content/issues/1719 ### Test plan https://github.com/elastic/security-team/pull/12813 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Maxim Palenov (cherry picked from commit 40dccf51a2ea3fd4e2b2b8b86564e669ad8896cb) # Conflicts: # oas_docs/output/kibana.serverless.yaml # oas_docs/output/kibana.yaml # x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.gen.ts # x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.schema.yaml # x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.test.ts # x-pack/solutions/security/plugins/security_solution/docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml # x-pack/solutions/security/plugins/security_solution/docs/openapi/serverless/security_solution_detections_api_2023_10_31.bundled.schema.yaml # x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts # x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.ts # x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action_dry_run.ts --- .../bulk_actions/bulk_actions_route.gen.ts | 43 ++ .../bulk_actions/bulk_actions_route.mock.ts | 10 + .../bulk_actions_route.schema.yaml | 90 ++++ .../bulk_actions/bulk_actions_types.ts | 2 + .../rule_management/rule_filtering.test.ts | 9 + .../rule_management/rule_filtering.ts | 6 + .../common/experimental_features.ts | 5 + .../public/common/lib/telemetry/constants.ts | 5 + .../detection_engine/common/translations.ts | 28 ++ .../components/alert_suppression_edit.tsx | 3 + .../suppression_fields_selector.tsx | 3 + .../rule_management/logic/types.ts | 1 + .../bulk_action_rule_errors_list.test.tsx | 27 ++ .../bulk_action_rule_errors_list.tsx | 31 +- ..._delete_alert_suprression_confirmation.tsx | 62 +++ .../bulk_actions/bulk_edit_flyout.tsx | 8 + ...t_alert_suppression_for_threshold_form.tsx | 105 +++++ .../forms/set_alert_suppression_form.tsx | 135 ++++++ .../rules_table/bulk_actions/translations.tsx | 78 ++++ .../bulk_actions/use_bulk_actions.tsx | 59 +++ .../compute_dry_run_edit_payload.test.ts | 9 +- .../utils/compute_dry_run_edit_payload.ts | 21 + .../utils/prepare_search_params.test.ts | 20 + .../utils/prepare_search_params.ts | 12 + .../components/rules_table/rules_tables.tsx | 27 +- .../routes/__mocks__/request_responses.ts | 8 + .../rule_management/api/register_routes.ts | 2 +- .../api/rules/bulk_actions/route.test.ts | 81 +++- .../api/rules/bulk_actions/route.ts | 15 +- ...eck_alert_suppression_bulk_edit_support.ts | 46 ++ .../bulk_actions/rule_params_modifier.test.ts | 306 +++++++++++++ .../bulk_actions/rule_params_modifier.ts | 75 ++- .../logic/bulk_actions/utils.test.ts | 56 +++ .../logic/bulk_actions/utils.ts | 26 ++ .../logic/bulk_actions/validations.ts | 26 +- .../config/ess/config.base.ts | 1 + .../config/serverless/config.base.ts | 3 + .../trial_license_complete_tier/index.ts | 1 + .../perform_bulk_action_dry_run.ts | 101 +++- .../perform_bulk_action_suppression.ts | 432 ++++++++++++++++++ .../test/security_solution_cypress/config.ts | 3 + .../bulk_edit_rules_suppression.cy.ts | 230 ++++++++++ ...ulk_edit_rules_suppression_basic_ess.cy.ts | 51 +++ ...es_suppression_essentials_serverless.cy.ts | 76 +++ .../cypress/screens/rules_bulk_actions.ts | 13 + .../cypress/tasks/rules_bulk_actions.ts | 36 ++ .../serverless_config.ts | 3 + 47 files changed, 2373 insertions(+), 17 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/bulk_edit_delete_alert_suprression_confirmation.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/forms/set_alert_suppression_for_threshold_form.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/forms/set_alert_suppression_form.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/check_alert_suppression_bulk_edit_support.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/utils.test.ts create mode 100644 x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action_suppression.ts create mode 100644 x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_edit_rules_suppression.cy.ts create mode 100644 x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_edit_rules_suppression_basic_ess.cy.ts create mode 100644 x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_edit_rules_suppression_essentials_serverless.cy.ts diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.gen.ts b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.gen.ts index 7e38aa6036f53..7198b39555ce3 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.gen.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.gen.ts @@ -29,7 +29,9 @@ import { InvestigationFields, TimelineTemplateId, TimelineTemplateTitle, + AlertSuppression, } from '../../model/rule_schema/common_attributes.gen'; +import { ThresholdAlertSuppression } from '../../model/rule_schema/specific_attributes/threshold_attributes.gen'; export type BulkEditSkipReason = z.infer; export const BulkEditSkipReason = z.literal('RULE_NOT_MODIFIED'); @@ -56,6 +58,8 @@ export const BulkActionsDryRunErrCode = z.enum([ 'ESQL_INDEX_PATTERN', 'MANUAL_RULE_RUN_FEATURE', 'MANUAL_RULE_RUN_DISABLED_RULE', + 'THRESHOLD_RULE_TYPE_IN_SUPPRESSION', + 'UNSUPPORTED_RULE_IN_SUPPRESSION_FOR_THRESHOLD', ]); export type BulkActionsDryRunErrCodeEnum = typeof BulkActionsDryRunErrCode.enum; export const BulkActionsDryRunErrCodeEnum = BulkActionsDryRunErrCode.enum; @@ -233,6 +237,9 @@ export const BulkActionEditType = z.enum([ 'add_investigation_fields', 'delete_investigation_fields', 'set_investigation_fields', + 'delete_alert_suppression', + 'set_alert_suppression', + 'set_alert_suppression_for_threshold', ]); export type BulkActionEditTypeEnum = typeof BulkActionEditType.enum; export const BulkActionEditTypeEnum = BulkActionEditType.enum; @@ -357,6 +364,41 @@ export const BulkActionEditPayloadTimeline = z.object({ }), }); +export type BulkActionEditPayloadSetAlertSuppression = z.infer< + typeof BulkActionEditPayloadSetAlertSuppression +>; +export const BulkActionEditPayloadSetAlertSuppression = z.object({ + type: z.literal('set_alert_suppression'), + value: AlertSuppression, +}); + +export type BulkActionEditPayloadSetAlertSuppressionForThreshold = z.infer< + typeof BulkActionEditPayloadSetAlertSuppressionForThreshold +>; +export const BulkActionEditPayloadSetAlertSuppressionForThreshold = z.object({ + type: z.literal('set_alert_suppression_for_threshold'), + value: ThresholdAlertSuppression, +}); + +export type BulkActionEditPayloadDeleteAlertSuppression = z.infer< + typeof BulkActionEditPayloadDeleteAlertSuppression +>; +export const BulkActionEditPayloadDeleteAlertSuppression = z.object({ + type: z.literal('delete_alert_suppression'), +}); + +export const BulkActionEditPayloadAlertSuppressionInternal = z.union([ + BulkActionEditPayloadSetAlertSuppression, + BulkActionEditPayloadSetAlertSuppressionForThreshold, + BulkActionEditPayloadDeleteAlertSuppression, +]); + +export type BulkActionEditPayloadAlertSuppression = z.infer< + typeof BulkActionEditPayloadAlertSuppressionInternal +>; +export const BulkActionEditPayloadAlertSuppression = + BulkActionEditPayloadAlertSuppressionInternal as z.ZodType; + export const BulkActionEditPayloadInternal = z.union([ BulkActionEditPayloadTags, BulkActionEditPayloadIndexPatterns, @@ -364,6 +406,7 @@ export const BulkActionEditPayloadInternal = z.union([ BulkActionEditPayloadTimeline, BulkActionEditPayloadRuleActions, BulkActionEditPayloadSchedule, + BulkActionEditPayloadAlertSuppression, ]); export type BulkActionEditPayload = z.infer; diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.mock.ts b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.mock.ts index 37471efb0a958..0dd70021056b7 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.mock.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.mock.ts @@ -20,3 +20,13 @@ export const getPerformBulkActionEditSchemaMock = (): PerformRulesBulkActionRequ action: BulkActionTypeEnum.edit, [BulkActionTypeEnum.edit]: [{ type: BulkActionEditTypeEnum.add_tags, value: ['tag1'] }], }); + +export const getPerformBulkActionEditAlertSuppressionSchemaMock = + (): PerformRulesBulkActionRequestBody => ({ + query: '', + ids: undefined, + action: BulkActionTypeEnum.edit, + [BulkActionTypeEnum.edit]: [ + { type: BulkActionEditTypeEnum.set_alert_suppression, value: { group_by: ['field1'] } }, + ], + }); diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.schema.yaml b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.schema.yaml index eb760bdc38b11..d97515dc49883 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.schema.yaml +++ b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.schema.yaml @@ -359,6 +359,47 @@ paths: eventAction: trigger timestamp: 2023-10-31T00:00:00Z group: default3 + example27: + summary: Edit - Set alert suppression to rules (idempotent) + description: The following request set alert suppression to the rules with the specified IDs. + value: + ids: + - '12345678-1234-1234-1234-1234567890ab' + - '87654321-4321-4321-4321-0987654321ba' + action: 'edit' + edit: + - type: 'set_alert_suppression' + value: + group_by: + - 'source.ip' + duration: + value: 1 + unit: 'h' + missing_fields_strategy: 'suppress' + example28: + summary: Edit - Set alert suppression to threshold rules (idempotent) + description: The following request set alert suppression to threshold rules with the specified IDs. + value: + ids: + - '12345678-1234-1234-1234-1234567890ab' + - '87654321-4321-4321-4321-0987654321ba' + action: 'edit' + edit: + - type: 'set_alert_suppression_for_threshold' + value: + duration: + value: 1 + unit: 'h' + example29: + summary: Edit - Removes alert suppression from rules (idempotent) + description: The following request removes alert suppression from the rules with the specified IDs. If the rules do not have alert suppression, no changes are made. + value: + ids: + - '12345678-1234-1234-1234-1234567890ab' + - '87654321-4321-4321-4321-0987654321ba' + action: 'edit' + edit: + - type: 'delete_alert_suppression' responses: 200: description: OK @@ -1040,6 +1081,8 @@ components: - ESQL_INDEX_PATTERN - MANUAL_RULE_RUN_FEATURE - MANUAL_RULE_RUN_DISABLED_RULE + - THRESHOLD_RULE_TYPE_IN_SUPPRESSION + - UNSUPPORTED_RULE_IN_SUPPRESSION_FOR_THRESHOLD NormalizedRuleError: type: object @@ -1286,6 +1329,9 @@ components: - add_investigation_fields - delete_investigation_fields - set_investigation_fields + - delete_alert_suppression + - set_alert_suppression + - set_alert_suppression_for_threshold # Per rulesClient.bulkEdit rules actions operation contract (x-pack/platform/plugins/shared/alerting/server/rules_client/rules_client.ts) normalized rule action object is expected (NormalizedAlertAction) as value for the edit operation NormalizedRuleAction: @@ -1458,6 +1504,48 @@ components: - type - value + BulkActionEditPayloadSetAlertSuppression: + type: object + properties: + type: + type: string + enum: + - set_alert_suppression + value: + $ref: '../../model/rule_schema/common_attributes.schema.yaml#/components/schemas/AlertSuppression' + required: + - type + - value + + BulkActionEditPayloadSetAlertSuppressionForThreshold: + type: object + properties: + type: + type: string + enum: + - set_alert_suppression_for_threshold + value: + $ref: '../../model/rule_schema/specific_attributes/threshold_attributes.schema.yaml#/components/schemas/ThresholdAlertSuppression' + required: + - type + - value + + BulkActionEditPayloadDeleteAlertSuppression: + type: object + properties: + type: + type: string + enum: + - delete_alert_suppression + required: + - type + + BulkActionEditPayloadAlertSuppression: + anyOf: + - $ref: '#/components/schemas/BulkActionEditPayloadSetAlertSuppression' + - $ref: '#/components/schemas/BulkActionEditPayloadSetAlertSuppressionForThreshold' + - $ref: '#/components/schemas/BulkActionEditPayloadDeleteAlertSuppression' + BulkActionEditPayload: anyOf: - $ref: '#/components/schemas/BulkActionEditPayloadTags' @@ -1466,6 +1554,8 @@ components: - $ref: '#/components/schemas/BulkActionEditPayloadTimeline' - $ref: '#/components/schemas/BulkActionEditPayloadRuleActions' - $ref: '#/components/schemas/BulkActionEditPayloadSchedule' + - $ref: '#/components/schemas/BulkActionEditPayloadAlertSuppression' + BulkEditRules: allOf: diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_types.ts b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_types.ts index c160b6fb21c27..9a0487c3c0eb3 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_types.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_types.ts @@ -12,6 +12,7 @@ import type { BulkActionEditPayloadSchedule, BulkActionEditPayloadTags, BulkActionEditPayloadTimeline, + BulkActionEditPayloadAlertSuppression, } from './bulk_actions_route.gen'; /** @@ -29,4 +30,5 @@ export type BulkActionEditForRuleParams = | BulkActionEditPayloadIndexPatterns | BulkActionEditPayloadInvestigationFields | BulkActionEditPayloadTimeline + | BulkActionEditPayloadAlertSuppression | BulkActionEditPayloadSchedule; diff --git a/x-pack/solutions/security/plugins/security_solution/common/detection_engine/rule_management/rule_filtering.test.ts b/x-pack/solutions/security/plugins/security_solution/common/detection_engine/rule_management/rule_filtering.test.ts index d40aadadb184f..afd2dd6829cae 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/detection_engine/rule_management/rule_filtering.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/detection_engine/rule_management/rule_filtering.test.ts @@ -86,4 +86,13 @@ describe('convertRulesFilterToKQL', () => { expect(kql).toBe('NOT alert.attributes.params.type: ("machine_learning" OR "saved_query")'); }); + + it('handles presence of "includeRuleTypes" properly', () => { + const kql = convertRulesFilterToKQL({ + ...filterOptions, + includeRuleTypes: ['query', 'eql'], + }); + + expect(kql).toBe('alert.attributes.params.type: ("query" OR "eql")'); + }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/common/detection_engine/rule_management/rule_filtering.ts b/x-pack/solutions/security/plugins/security_solution/common/detection_engine/rule_management/rule_filtering.ts index 692f2fa55a5e5..2e9e7e425096d 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/detection_engine/rule_management/rule_filtering.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/detection_engine/rule_management/rule_filtering.ts @@ -37,6 +37,7 @@ interface RulesFilterOptions { ruleExecutionStatus: RuleExecutionStatus; customizationStatus: RuleCustomizationStatus; ruleIds: string[]; + includeRuleTypes?: Type[]; } /** @@ -55,6 +56,7 @@ export function convertRulesFilterToKQL({ excludeRuleTypes = [], ruleExecutionStatus, customizationStatus, + includeRuleTypes = [], }: Partial): string { const kql: string[] = []; @@ -82,6 +84,10 @@ export function convertRulesFilterToKQL({ kql.push(`NOT ${convertRuleTypesToKQL(excludeRuleTypes)}`); } + if (includeRuleTypes.length) { + kql.push(convertRuleTypesToKQL(includeRuleTypes)); + } + if (ruleExecutionStatus === RuleExecutionStatusEnum.succeeded) { kql.push(`${LAST_RUN_OUTCOME_FIELD}: "succeeded"`); } else if (ruleExecutionStatus === RuleExecutionStatusEnum['partial failure']) { diff --git a/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts b/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts index f7ca6252fcda9..3ffb2562e6553 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts @@ -198,6 +198,11 @@ export const allowedExperimentalValues = Object.freeze({ */ filterProcessDescendantsForEventFiltersEnabled: true, + /** + * Enables the rule's bulk action to manage alert suppression + */ + bulkEditAlertSuppressionEnabled: false, + /** * Enables the new data ingestion hub */ diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/lib/telemetry/constants.ts b/x-pack/solutions/security/plugins/security_solution/public/common/lib/telemetry/constants.ts index 08bc1d4a62a83..08a24017d4193 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/lib/telemetry/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/lib/telemetry/constants.ts @@ -49,6 +49,11 @@ export enum TELEMETRY_EVENT { SET_INVESTIGATION_FIELDS = 'set_investigation_fields', DELETE_INVESTIGATION_FIELDS = 'delete_investigation_fields', + // Bulk edit alert suppression + SET_ALERT_SUPPRESSION_FOR_THRESHOLD = 'set_alert_suppression_for_threshold', + SET_ALERT_SUPPRESSION = 'set_alert_suppression', + DELETE_ALERT_SUPPRESSION = 'delete_alert_suppression', + // AI assistant on rule creation form OPEN_ASSISTANT_ON_RULE_QUERY_ERROR = 'open_assistant_on_rule_query_error', } diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/common/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/common/translations.ts index 16981a604d6bb..0f1f4be32d42f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/common/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/common/translations.ts @@ -187,6 +187,34 @@ export const BULK_ACTION_DELETE_INVESTIGATION_FIELDS = i18n.translate( } ); +export const BULK_ACTION_ALERT_SUPPRESSION = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.alertSuppressionTitle', + { + defaultMessage: 'Alert suppression', + } +); + +export const BULK_ACTION_SET_ALERT_SUPPRESSION = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.setAlertSuppression', + { + defaultMessage: 'Apply alert suppression', + } +); + +export const BULK_ACTION_SET_ALERT_SUPPRESSION_FOR_THRESHOLD = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.setAlertSuppressionForThreshold', + { + defaultMessage: 'Apply alert suppression to threshold rules', + } +); + +export const BULK_ACTION_DELETE_ALERT_SUPPRESSION = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.deleteAlertSuppression', + { + defaultMessage: 'Remove alert suppression', + } +); + export const BULK_ACTION_APPLY_TIMELINE_TEMPLATE = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.applyTimelineTemplateTitle', { diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/alert_suppression_edit.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/alert_suppression_edit.tsx index d7cea2a00ba65..14d4ef8744f66 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/alert_suppression_edit.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/alert_suppression_edit.tsx @@ -20,6 +20,7 @@ interface AlertSuppressionEditProps { disabled?: boolean; disabledText?: string; warningText?: string; + fullWidth?: boolean; } export const AlertSuppressionEdit = memo(function AlertSuppressionEdit({ @@ -28,6 +29,7 @@ export const AlertSuppressionEdit = memo(function AlertSuppressionEdit({ disabled, disabledText, warningText, + fullWidth, }: AlertSuppressionEditProps): JSX.Element { const [{ [ALERT_SUPPRESSION_FIELDS_FIELD_NAME]: suppressionFields }] = useFormData<{ [ALERT_SUPPRESSION_FIELDS_FIELD_NAME]: string[]; @@ -41,6 +43,7 @@ export const AlertSuppressionEdit = memo(function AlertSuppressionEdit({ suppressibleFields={suppressibleFields} labelAppend={labelAppend} disabled={disabled} + fullWidth={fullWidth} /> {warningText && ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/suppression_fields_selector.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/suppression_fields_selector.tsx index 72eea027288f0..08b34d301c56f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/suppression_fields_selector.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/alert_suppression_edit/components/suppression_fields_selector.tsx @@ -17,12 +17,14 @@ interface SuppressionFieldsSelectorProps { suppressibleFields: DataViewFieldBase[]; labelAppend?: React.ReactNode; disabled?: boolean; + fullWidth?: boolean; } export function SuppressionFieldsSelector({ suppressibleFields, labelAppend, disabled, + fullWidth, }: SuppressionFieldsSelectorProps): JSX.Element { return ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts index 98383e93b444c..8407707d76a87 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts @@ -109,6 +109,7 @@ export interface FilterOptions { ruleSource?: RuleCustomizationStatus[]; // undefined is to display all the rules showRulesWithGaps?: boolean; gapSearchRange?: GapRangeValue; + includeRuleTypes?: Type[]; } export interface FetchRulesResponse { diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/bulk_action_rule_errors_list.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/bulk_action_rule_errors_list.test.tsx index 623b7e9b69bd8..bd9acf20e2cf4 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/bulk_action_rule_errors_list.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/bulk_action_rule_errors_list.test.tsx @@ -111,4 +111,31 @@ describe('Component BulkEditRuleErrorsList', () => { expect(screen.getByText(value)).toBeInTheDocument(); }); + + test.each([ + [ + BulkActionsDryRunErrCodeEnum.THRESHOLD_RULE_TYPE_IN_SUPPRESSION, + "2 threshold rules can't be edited.", + ], + [ + BulkActionsDryRunErrCodeEnum.UNSUPPORTED_RULE_IN_SUPPRESSION_FOR_THRESHOLD, + "2 rules can't be edited.", + ], + ])('should render correct message for "%s" errorCode', (errorCode, value) => { + const ruleErrors: DryRunResult['ruleErrors'] = [ + { + message: 'test failure', + errorCode, + ruleIds: ['rule:1', 'rule:2'], + }, + ]; + render( + , + { + wrapper: TestProviders, + } + ); + + expect(screen.getByText(new RegExp(value, 'i'))).toBeInTheDocument(); + }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/bulk_action_rule_errors_list.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/bulk_action_rule_errors_list.tsx index f8ee7a177287c..5e06aa4b71ab8 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/bulk_action_rule_errors_list.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/bulk_action_rule_errors_list.tsx @@ -14,7 +14,10 @@ import { BulkActionTypeEnum, BulkActionsDryRunErrCodeEnum, } from '../../../../../../common/api/detection_engine/rule_management'; - +import { + BULK_ACTION_SET_ALERT_SUPPRESSION, + BULK_ACTION_SET_ALERT_SUPPRESSION_FOR_THRESHOLD, +} from '../../../../common/translations'; import type { DryRunResult, BulkActionForConfirmation } from './types'; import { usePrebuiltRuleCustomizationUpsellingMessage } from '../../../../rule_management/logic/prebuilt_rules/use_prebuilt_rule_customization_upselling_message'; @@ -84,6 +87,32 @@ const BulkEditRuleErrorItem = ({ /> ); + case BulkActionsDryRunErrCodeEnum.THRESHOLD_RULE_TYPE_IN_SUPPRESSION: + return ( +
  • + {BULK_ACTION_SET_ALERT_SUPPRESSION_FOR_THRESHOLD}, + }} + /> +
  • + ); + case BulkActionsDryRunErrCodeEnum.UNSUPPORTED_RULE_IN_SUPPRESSION_FOR_THRESHOLD: + return ( +
  • + {BULK_ACTION_SET_ALERT_SUPPRESSION}, + }} + /> +
  • + ); default: return (
  • diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/bulk_edit_delete_alert_suprression_confirmation.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/bulk_edit_delete_alert_suprression_confirmation.tsx new file mode 100644 index 0000000000000..913a5062d19d4 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/bulk_edit_delete_alert_suprression_confirmation.tsx @@ -0,0 +1,62 @@ +/* + * 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 { EuiConfirmModal } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { METRIC_TYPE, track, TELEMETRY_EVENT } from '../../../../../common/lib/telemetry'; +import type { BulkActionEditPayloadDeleteAlertSuppression } from '../../../../../../common/api/detection_engine/rule_management'; +import { BulkActionEditTypeEnum } from '../../../../../../common/api/detection_engine/rule_management'; +import { bulkAlertSuppression as i18n } from './translations'; + +interface Props { + rulesCount: number; + onCancel: () => void; + onConfirm: (bulkActionEditPayload: BulkActionEditPayloadDeleteAlertSuppression) => void; +} + +export const BulkEditDeleteAlertSuppressionConfirmation: React.FC = ({ + rulesCount, + onCancel, + onConfirm, +}) => ( + { + onConfirm({ + type: BulkActionEditTypeEnum.delete_alert_suppression, + }); + track(METRIC_TYPE.CLICK, TELEMETRY_EVENT.SET_ALERT_SUPPRESSION_FOR_THRESHOLD); + }} + confirmButtonText={i18n.DELETE_CONFIRMATION_CONFIRM} + cancelButtonText={i18n.DELETE_CONFIRMATION_CANCEL} + buttonColor="danger" + defaultFocusedButton="confirm" + data-test-subj="deleteRulesConfirmationModal" + > + {rulesCount}, + deleteStrong: ( + + + + ), + }} + /> + +); + +BulkEditDeleteAlertSuppressionConfirmation.displayName = + 'BulkEditDeleteAlertSuppressionConfirmation'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/bulk_edit_flyout.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/bulk_edit_flyout.tsx index d4b01000233df..59ffd2fcd9834 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/bulk_edit_flyout.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/bulk_edit_flyout.tsx @@ -19,6 +19,8 @@ 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'; +import { SetAlertSuppressionForm } from './forms/set_alert_suppression_form'; +import { SetAlertSuppressionForThresholdForm } from './forms/set_alert_suppression_for_threshold_form'; interface BulkEditFlyoutProps { onClose: () => void; @@ -44,6 +46,12 @@ const BulkEditFlyoutComponent = ({ editAction, ...props }: BulkEditFlyoutProps) case BulkActionEditTypeEnum.set_investigation_fields: return ; + case BulkActionEditTypeEnum.set_alert_suppression: + return ; + + case BulkActionEditTypeEnum.set_alert_suppression_for_threshold: + return ; + case BulkActionEditTypeEnum.set_timeline: return ; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/forms/set_alert_suppression_for_threshold_form.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/forms/set_alert_suppression_for_threshold_form.tsx new file mode 100644 index 0000000000000..326efece8f662 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/forms/set_alert_suppression_for_threshold_form.tsx @@ -0,0 +1,105 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, EuiIcon, EuiFormRow } from '@elastic/eui'; + +import { METRIC_TYPE, track, TELEMETRY_EVENT } from '../../../../../../common/lib/telemetry'; +import { BulkActionEditTypeEnum } from '../../../../../../../common/api/detection_engine/rule_management'; +import type { BulkActionEditPayload } from '../../../../../../../common/api/detection_engine/rule_management'; +import type { AlertSuppressionDuration } from '../../../../../../../common/api/detection_engine/model/rule_schema/common_attributes.gen'; +import { useForm, UseMultiFields } from '../../../../../../shared_imports'; +import { BulkEditFormWrapper } from './bulk_edit_form_wrapper'; +import { ALERT_SUPPRESSION_DEFAULT_DURATION } from '../../../../../rule_creation/components/alert_suppression_edit'; +import { bulkAlertSuppression as i18n } from '../translations'; +import { DurationInput } from '../../../../../rule_creation/components/duration_input'; + +type FormData = AlertSuppressionDuration; + +const initialFormData: FormData = ALERT_SUPPRESSION_DEFAULT_DURATION; + +interface AlertSuppressionFormProps { + editAction: BulkActionEditTypeEnum['set_alert_suppression_for_threshold']; + rulesCount: number; + onClose: () => void; + onConfirm: (bulkActionEditPayload: BulkActionEditPayload) => void; +} + +export const SetAlertSuppressionForThresholdForm = React.memo(function SetAlertSuppressionForm({ + editAction, + rulesCount, + onClose, + onConfirm, +}: AlertSuppressionFormProps) { + const { form } = useForm({ + defaultValue: initialFormData, + }); + + const handleSubmit = async () => { + const { data, isValid } = await form.submit(); + if (!isValid) { + return; + } + + onConfirm({ + value: { + duration: data, + }, + type: BulkActionEditTypeEnum.set_alert_suppression_for_threshold, + }); + + track(METRIC_TYPE.CLICK, TELEMETRY_EVENT.SET_ALERT_SUPPRESSION_FOR_THRESHOLD); + }; + + return ( + + + + + + + {i18n.SUPPRESSION_FOR_THRESHOLD_INFO_TEXT} + + + + + + fields={{ + suppressionDurationValue: { + path: `value`, + }, + suppressionDurationUnit: { + path: `unit`, + }, + }} + > + {({ suppressionDurationValue, suppressionDurationUnit }) => ( + + )} + + + + ); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/forms/set_alert_suppression_form.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/forms/set_alert_suppression_form.tsx new file mode 100644 index 0000000000000..5b87bad00072e --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/forms/set_alert_suppression_form.tsx @@ -0,0 +1,135 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, EuiIcon } from '@elastic/eui'; + +import { useKibana } from '../../../../../../common/lib/kibana'; +import { DEFAULT_INDEX_KEY } from '../../../../../../../common/constants'; +import { METRIC_TYPE, track, TELEMETRY_EVENT } from '../../../../../../common/lib/telemetry'; +import { DEFAULT_SUPPRESSION_MISSING_FIELDS_STRATEGY } from '../../../../../../../common/detection_engine/constants'; +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 { + AlertSuppressionMissingFieldsStrategy, + AlertSuppressionDuration, +} from '../../../../../../../common/api/detection_engine/model/rule_schema/common_attributes.gen'; +import { useForm, fieldValidators } from '../../../../../../shared_imports'; +import type { FormSchema } from '../../../../../../shared_imports'; +import { BulkEditFormWrapper } from './bulk_edit_form_wrapper'; +import { + AlertSuppressionEdit, + ALERT_SUPPRESSION_MISSING_FIELDS_FIELD_NAME, + ALERT_SUPPRESSION_FIELDS_FIELD_NAME, + ALERT_SUPPRESSION_DURATION_TYPE_FIELD_NAME, + ALERT_SUPPRESSION_DURATION_FIELD_NAME, + ALERT_SUPPRESSION_DEFAULT_DURATION, +} from '../../../../../rule_creation/components/alert_suppression_edit'; +import { AlertSuppressionDurationType } from '../../../../../common/types'; +import { bulkAlertSuppression as i18n } from '../translations'; +import { useTermsAggregationFields } from '../../../../../../common/hooks/use_terms_aggregation_fields'; + +interface AlertSuppressionFormData { + alertSuppressionFields: string[]; + alertSuppressionDurationType: AlertSuppressionDurationType; + alertSuppressionDuration: AlertSuppressionDuration; + alertSuppressionMissingFields?: AlertSuppressionMissingFieldsStrategy; +} + +const formSchema: FormSchema = { + [ALERT_SUPPRESSION_FIELDS_FIELD_NAME]: { + validations: [ + { + validator: fieldValidators.emptyField(i18n.SUPPRESSION_REQUIRED_ERROR), + }, + { + validator: fieldValidators.maxLengthField({ + message: i18n.SUPPRESSION_MAX_LENGTH_ERROR, + length: 3, + }), + }, + ], + }, +}; + +const initialFormData: AlertSuppressionFormData = { + [ALERT_SUPPRESSION_FIELDS_FIELD_NAME]: [], + [ALERT_SUPPRESSION_DURATION_TYPE_FIELD_NAME]: AlertSuppressionDurationType.PerRuleExecution, + [ALERT_SUPPRESSION_DURATION_FIELD_NAME]: ALERT_SUPPRESSION_DEFAULT_DURATION, + [ALERT_SUPPRESSION_MISSING_FIELDS_FIELD_NAME]: DEFAULT_SUPPRESSION_MISSING_FIELDS_STRATEGY, +}; + +interface AlertSuppressionFormProps { + editAction: BulkActionEditTypeEnum['set_alert_suppression']; + rulesCount: number; + onClose: () => void; + onConfirm: (bulkActionEditPayload: BulkActionEditPayload) => void; +} + +export const SetAlertSuppressionForm = React.memo(function SetAlertSuppressionForm({ + editAction, + rulesCount, + onClose, + onConfirm, +}: AlertSuppressionFormProps) { + const { form } = useForm({ + defaultValue: initialFormData, + schema: formSchema, + }); + const { uiSettings } = useKibana().services; + const defaultPatterns = uiSettings.get(DEFAULT_INDEX_KEY); + + const [_, { indexPatterns }] = useFetchIndex(defaultPatterns, false); + const suppressibleFields = useTermsAggregationFields(indexPatterns?.fields); + + const handleSubmit = async () => { + const { data, isValid } = await form.submit(); + if (!isValid) { + return; + } + + const durationValue = + data[ALERT_SUPPRESSION_DURATION_TYPE_FIELD_NAME] === + AlertSuppressionDurationType.PerTimePeriod + ? data[ALERT_SUPPRESSION_DURATION_FIELD_NAME] + : undefined; + + const suppressionPayload = { + value: { + group_by: data.alertSuppressionFields, + missing_fields_strategy: data[ALERT_SUPPRESSION_MISSING_FIELDS_FIELD_NAME], + duration: durationValue, + }, + type: BulkActionEditTypeEnum.set_alert_suppression, + }; + + onConfirm(suppressionPayload); + track(METRIC_TYPE.CLICK, TELEMETRY_EVENT.SET_ALERT_SUPPRESSION); + }; + + return ( + + + + + + + {i18n.SUPPRESSION_INFO_TEXT} + + + + + + + ); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/translations.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/translations.tsx index f9af83408995d..8c5eed829a9f6 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/translations.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/translations.tsx @@ -190,3 +190,81 @@ export const ML_RULES_UNAVAILABLE = (totalRules: number) => defaultMessage: '{totalRules} {totalRules, plural, =1 {rule requires} other {rules require}} Machine Learning to enable.', }); + +export const bulkAlertSuppression = { + SUPPRESSION_MAX_LENGTH_ERROR: i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.alertSuppression.alertSuppressionMaxLengthErrorMessage', + { + defaultMessage: 'Number of suppress by fields must be at most 3.', + } + ), + SET_TITLE: i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.alertSuppression.addTitle', + { + defaultMessage: 'Apply alert suppression', + } + ), + SET_FOR_THRESHOLD_TITLE: i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.alertSuppression.addTitle', + { + defaultMessage: 'Apply alert suppression to threshold rules', + } + ), + SUPPRESSION_REQUIRED_ERROR: i18n.translate( + 'xpack.securitySolution.detectionEngine.components.allRules.bulkActions.edit.alertSuppressionRequiredErrorMessage', + { + defaultMessage: 'A minimum of one suppression field is required.', + } + ), + SUPPRESSION_INFO_TEXT: i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.alertSuppression.infoText', + { + defaultMessage: + 'Existing alert suppression settings will be overwritten for all of the selected rules, except for threshold rules.', + } + ), + SUPPRESSION_FOR_THRESHOLD_INFO_TEXT: i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.alertSuppressionForThreshold.infoText', + { + defaultMessage: + 'Existing alert suppression settings will be overwritten for all of the selected threshold rules.', + } + ), + DELETE_CONFIRMATION_TITLE: i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.alertSuppression.bulkDeleteConfirmationTitle', + { + defaultMessage: 'Confirm bulk removal of alert suppression', + } + ), + DELETE_CONFIRMATION_CONFIRM: i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.alertSuppression.deleteConfirmationConfirm', + { + defaultMessage: 'Delete', + } + ), + DELETE_CONFIRMATION_CANCEL: i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.alertSuppression.deleteConfirmationCancel', + { + defaultMessage: 'Cancel', + } + ), + DURATION_PER_TIME_PERIOD_INPUT: i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.alertSuppression.perTimePeriodInput', + { + defaultMessage: 'Per time period', + } + ), + DURATION_PER_TIME_PERIOD_LABEL: i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.alertSuppression.perTimePeriodLabel', + { + defaultMessage: 'Suppression interval', + } + ), + DURATION_PER_TIME_PERIOD_HELP_TEXT: i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.alertSuppression.perTimePeriodHelpText', + { + defaultMessage: + 'Suppress alerts for the selected rules within a repeating time interval. To ensure suppression is appropriately applied, avoid choosing an interval that’s shorter than the rule’s run schedule.', + } + ), +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/use_bulk_actions.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/use_bulk_actions.tsx index dc8b903f179f3..cab42b5f26921 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/use_bulk_actions.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/use_bulk_actions.tsx @@ -49,6 +49,10 @@ import { computeDryRunEditPayload } from './utils/compute_dry_run_edit_payload'; import { transformExportDetailsToDryRunResult } from './utils/dry_run_result'; import { prepareSearchParams } from './utils/prepare_search_params'; import { ManualRuleRunEventTypes } from '../../../../../common/lib/telemetry'; +import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; +import { useUpsellingMessage } from '../../../../../common/hooks/use_upselling'; +import { useLicense } from '../../../../../common/hooks/use_license'; +import { MINIMUM_LICENSE_FOR_SUPPRESSION } from '../../../../../../common/detection_engine/constants'; interface UseBulkActionsArgs { filterOptions: FilterOptions; @@ -105,6 +109,13 @@ export const useBulkActions = ({ }; }, [kql, filterOptions]); + const isBulkEditAlertSuppressionFeatureEnabled = useIsExperimentalFeatureEnabled( + 'bulkEditAlertSuppressionEnabled' + ); + const alertSuppressionUpsellingMessage = useUpsellingMessage('alert_suppression_rule_form'); + const license = useLicense(); + const isAlertSuppressionLicenseValid = license.isAtLeast(MINIMUM_LICENSE_FOR_SUPPRESSION); + const getBulkItemsPopoverContent = useCallback( (closePopover: () => void): EuiContextMenuPanelDescriptor[] => { const selectedRules = rules.filter(({ id }) => selectedRuleIds.includes(id)); @@ -366,6 +377,7 @@ export const useBulkActions = ({ const isDeleteDisabled = containsLoading || selectedRuleIds.length === 0; const isEditDisabled = missingActionPrivileges || containsLoading || selectedRuleIds.length === 0; + const isAlertSuppressionDisabled = isEditDisabled || !isAlertSuppressionLicenseValid; return [ { @@ -418,6 +430,20 @@ export const useBulkActions = ({ disabled: isEditDisabled, panel: 3, }, + ...(isBulkEditAlertSuppressionFeatureEnabled + ? [ + { + key: i18n.BULK_ACTION_ALERT_SUPPRESSION, + name: i18n.BULK_ACTION_ALERT_SUPPRESSION, + 'data-test-subj': 'alertSuppressionBulkEditRule', + disabled: isAlertSuppressionDisabled, + toolTipContent: isAlertSuppressionLicenseValid + ? undefined + : alertSuppressionUpsellingMessage, + panel: 4, + }, + ] + : []), { key: i18n.BULK_ACTION_ADD_RULE_ACTIONS, name: i18n.BULK_ACTION_ADD_RULE_ACTIONS, @@ -584,6 +610,36 @@ export const useBulkActions = ({ }, ], }, + { + id: 4, + title: i18n.BULK_ACTION_MENU_TITLE, + items: [ + { + key: i18n.BULK_ACTION_SET_ALERT_SUPPRESSION, + name: i18n.BULK_ACTION_SET_ALERT_SUPPRESSION, + 'data-test-subj': 'setAlertSuppressionBulkEditRule', + onClick: handleBulkEdit(BulkActionEditTypeEnum.set_alert_suppression), + disabled: isAlertSuppressionDisabled, + toolTipProps: { position: 'right' }, + }, + { + key: i18n.BULK_ACTION_SET_ALERT_SUPPRESSION_FOR_THRESHOLD, + name: i18n.BULK_ACTION_SET_ALERT_SUPPRESSION_FOR_THRESHOLD, + 'data-test-subj': 'setAlertSuppressionForThresholdBulkEditRule', + onClick: handleBulkEdit(BulkActionEditTypeEnum.set_alert_suppression_for_threshold), + disabled: isAlertSuppressionDisabled, + toolTipProps: { position: 'right' }, + }, + { + key: i18n.BULK_ACTION_DELETE_ALERT_SUPPRESSION, + name: i18n.BULK_ACTION_DELETE_ALERT_SUPPRESSION, + 'data-test-subj': 'deleteAlertSuppressionBulkEditRule', + onClick: handleBulkEdit(BulkActionEditTypeEnum.delete_alert_suppression), + disabled: isAlertSuppressionDisabled, + toolTipProps: { position: 'right' }, + }, + ], + }, ]; }, [ @@ -608,8 +664,11 @@ export const useBulkActions = ({ executeBulkActionsDryRun, filterOptions, completeBulkEditForm, + isBulkEditAlertSuppressionFeatureEnabled, startServices, canCreateTimelines, + isAlertSuppressionLicenseValid, + alertSuppressionUpsellingMessage, globalQuery, ] ); diff --git a/x-pack/solutions/security/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/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/utils/compute_dry_run_edit_payload.test.ts index 29d1c03a1b0e4..6c33627ecc5a6 100644 --- a/x-pack/solutions/security/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/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/utils/compute_dry_run_edit_payload.test.ts @@ -18,6 +18,12 @@ describe('computeDryRunEditPayload', () => { [BulkActionEditTypeEnum.set_index_patterns, []], [BulkActionEditTypeEnum.delete_index_patterns, []], [BulkActionEditTypeEnum.add_index_patterns, []], + [BulkActionEditTypeEnum.delete_alert_suppression, undefined], + [BulkActionEditTypeEnum.set_alert_suppression, { group_by: ['test_field'] }], + [ + BulkActionEditTypeEnum.set_alert_suppression_for_threshold, + { duration: { unit: 'm', value: 4 } }, + ], [BulkActionEditTypeEnum.add_tags, []], [BulkActionEditTypeEnum.delete_index_patterns, []], [BulkActionEditTypeEnum.set_tags, []], @@ -25,7 +31,6 @@ describe('computeDryRunEditPayload', () => { ])('should return correct payload for bulk edit action %s', (editAction, value) => { const payload = computeDryRunEditPayload(editAction); expect(payload).toHaveLength(1); - expect(payload?.[0].type).toEqual(editAction); - expect(payload?.[0].value).toEqual(value); + expect(payload?.[0]).toEqual({ type: editAction, value }); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/utils/compute_dry_run_edit_payload.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/utils/compute_dry_run_edit_payload.ts index 340e2345b33db..54ec515f8841e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/utils/compute_dry_run_edit_payload.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/utils/compute_dry_run_edit_payload.ts @@ -30,6 +30,27 @@ export function computeDryRunEditPayload(editAction: BulkActionEditType): BulkAc }, ]; + case BulkActionEditTypeEnum.set_alert_suppression_for_threshold: + return [ + { + type: editAction, + value: { duration: { unit: 'm', value: 4 } }, + }, + ]; + case BulkActionEditTypeEnum.delete_alert_suppression: + return [ + { + type: editAction, + }, + ]; + case BulkActionEditTypeEnum.set_alert_suppression: + return [ + { + type: editAction, + value: { group_by: ['test_field'] }, + }, + ]; + case BulkActionEditTypeEnum.add_index_patterns: case BulkActionEditTypeEnum.delete_index_patterns: case BulkActionEditTypeEnum.set_index_patterns: diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/utils/prepare_search_params.test.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/utils/prepare_search_params.test.ts index ac074cd162893..9a14b717ccfac 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/utils/prepare_search_params.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/utils/prepare_search_params.test.ts @@ -82,6 +82,26 @@ describe('prepareSearchParams', () => { showElasticRules: false, }, ], + [ + BulkActionsDryRunErrCodeEnum.THRESHOLD_RULE_TYPE_IN_SUPPRESSION, + { + filter: '', + tags: [], + showCustomRules: false, + showElasticRules: false, + excludeRuleTypes: ['threshold'], + }, + ], + [ + BulkActionsDryRunErrCodeEnum.UNSUPPORTED_RULE_IN_SUPPRESSION_FOR_THRESHOLD, + { + filter: '', + tags: [], + showCustomRules: false, + showElasticRules: false, + includeRuleTypes: ['threshold'], + }, + ], [ undefined, { diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/utils/prepare_search_params.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/utils/prepare_search_params.ts index 658d020261962..525dcccccc935 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/utils/prepare_search_params.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/utils/prepare_search_params.ts @@ -60,6 +60,18 @@ export const prepareSearchParams = ({ excludeRuleTypes: [...(modifiedFilterOptions.excludeRuleTypes ?? []), 'esql'], }; break; + case BulkActionsDryRunErrCodeEnum.THRESHOLD_RULE_TYPE_IN_SUPPRESSION: + modifiedFilterOptions = { + ...modifiedFilterOptions, + excludeRuleTypes: [...(modifiedFilterOptions.excludeRuleTypes ?? []), 'threshold'], + }; + break; + case BulkActionsDryRunErrCodeEnum.UNSUPPORTED_RULE_IN_SUPPRESSION_FOR_THRESHOLD: + modifiedFilterOptions = { + ...modifiedFilterOptions, + includeRuleTypes: [...(modifiedFilterOptions.includeRuleTypes ?? []), 'threshold'], + }; + break; } }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_tables.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_tables.tsx index 5078c89630907..07d2ac00e637f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_tables.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_tables.tsx @@ -46,6 +46,8 @@ import { ManualRuleRunModal } from '../../../rule_gaps/components/manual_rule_ru import { BulkManualRuleRunLimitErrorModal } from './bulk_actions/bulk_manual_rule_run_limit_error_modal'; import { RulesWithGapsOverviewPanel } from '../../../rule_gaps/components/rules_with_gaps_overview_panel'; import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; +import { BulkEditDeleteAlertSuppressionConfirmation } from './bulk_actions/bulk_edit_delete_alert_suprression_confirmation'; +import { BulkActionEditTypeEnum } from '../../../../../common/api/detection_engine/rule_management'; const INITIAL_SORT_FIELD = 'enabled'; @@ -313,14 +315,23 @@ export const RulesTables = React.memo(({ selectedTab }) => { rulesCount={rulesCount} /> )} - {isBulkEditFlyoutVisible && bulkEditActionType !== undefined && ( - - )} + {isBulkEditFlyoutVisible && + bulkEditActionType && + (bulkEditActionType === BulkActionEditTypeEnum.delete_alert_suppression ? ( + + ) : ( + + ))} + {shouldShowRulesTable && ( <> {selectedTab === AllRulesTabs.monitoring && storeGapsInEventLogEnabled && ( diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts index 4ed9b44976f2f..6c7810cb101c1 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -41,6 +41,7 @@ import { import { getBulkDisableRuleActionSchemaMock, getPerformBulkActionEditSchemaMock, + getPerformBulkActionEditAlertSuppressionSchemaMock, } from '../../../../../common/api/detection_engine/rule_management/mocks'; import { getCreateRulesSchemaMock } from '../../../../../common/api/detection_engine/model/rule_schema/mocks'; @@ -180,6 +181,13 @@ export const getDeleteAsPostBulkRequest = () => body: [{ rule_id: 'rule-1' }], }); +export const getBulkActionEditAlertSuppressionRequest = () => + requestMock.create({ + method: 'patch', + path: DETECTION_ENGINE_RULES_BULK_ACTION, + body: getPerformBulkActionEditAlertSuppressionSchemaMock(), + }); + export const getPrivilegeRequest = (options: { auth?: { isAuthenticated: boolean } } = {}) => requestMock.create({ method: 'get', diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/api/register_routes.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/api/register_routes.ts index d4b7c09ad2e63..1876a081c5140 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/api/register_routes.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/api/register_routes.ts @@ -48,7 +48,7 @@ export const registerRuleManagementRoutes = ( bulkDeleteRulesRoute(router, logger, docLinks); // Rules bulk actions - performBulkActionRoute(router, ml); + performBulkActionRoute(router, ml, config); // Rules export/import exportRulesRoute(router, config, logger); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.test.ts index 2795956fc791b..7dbb17022d559 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.test.ts @@ -14,6 +14,7 @@ import { getBulkActionEditRequest, getFindResultWithSingleHit, getFindResultWithMultiHits, + getBulkActionEditAlertSuppressionRequest, } from '../../../../routes/__mocks__/request_responses'; import { requestContextMock, serverMock, requestMock } from '../../../../routes/__mocks__'; import { performBulkActionRoute } from './route'; @@ -23,6 +24,7 @@ import { } from '../../../../../../../common/api/detection_engine/rule_management/mocks'; import { readRules } from '../../../logic/detection_rules_client/read_rules'; import { BulkActionsDryRunErrCodeEnum } from '../../../../../../../common/api/detection_engine'; +import type { ConfigType } from '../../../../../../config'; jest.mock('../../../../../machine_learning/authz'); jest.mock('../../../logic/detection_rules_client/read_rules', () => ({ readRules: jest.fn() })); @@ -33,6 +35,9 @@ describe('Perform bulk action route', () => { let { clients, context } = requestContextMock.createTools(); let ml: ReturnType; const mockRule = getFindResultWithSingleHit().data[0]; + const experimentalFeatures = { + bulkEditAlertSuppressionEnabled: true, + } as ConfigType['experimentalFeatures']; beforeEach(() => { server = serverMock.create(); @@ -45,7 +50,9 @@ describe('Perform bulk action route', () => { errors: [], total: 1, }); - performBulkActionRoute(server.router, ml); + performBulkActionRoute(server.router, ml, { + experimentalFeatures, + } as ConfigType); }); describe('status codes', () => { @@ -106,6 +113,32 @@ describe('Perform bulk action route', () => { status_code: 400, }); }); + + it('returns 403 if alert suppression license is not sufficient', async () => { + (context.licensing.license.hasAtLeast as jest.Mock).mockReturnValue(false); + const response = await server.inject( + getBulkActionEditAlertSuppressionRequest(), + requestContextMock.convertContext(context) + ); + + expect(response.body).toEqual({ + message: 'Alert suppression is enabled with platinum license or above.', + status_code: 403, + }); + }); + + it('returns 403 for dry run mode if alert suppression license is not sufficient', async () => { + (context.licensing.license.hasAtLeast as jest.Mock).mockReturnValue(false); + const response = await server.inject( + { ...getBulkActionEditAlertSuppressionRequest(), query: { dry_run: 'true' } }, + requestContextMock.convertContext(context) + ); + + expect(response.body).toEqual({ + message: 'Alert suppression is enabled with platinum license or above.', + status_code: 403, + }); + }); }); describe('rules execution failures', () => { @@ -752,6 +785,52 @@ describe('Perform bulk action route', () => { }); }); +describe('Perform bulk action route, experimental feature bulkEditAlertSuppressionEnabled is disabled', () => { + let server: ReturnType; + let { clients, context } = requestContextMock.createTools(); + let ml: ReturnType; + const experimentalFeatures = {} as ConfigType['experimentalFeatures']; + + beforeEach(() => { + server = serverMock.create(); + ({ clients, context } = requestContextMock.createTools()); + ml = mlServicesMock.createSetupContract(); + clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); + + performBulkActionRoute(server.router, ml, { + experimentalFeatures, + } as ConfigType); + }); + + it('returns error if experimental feature bulkEditAlertSuppressionEnabled is not enabled for alert suppression bulk action', async () => { + const response = await server.inject( + getBulkActionEditAlertSuppressionRequest(), + requestContextMock.convertContext(context) + ); + + expect(response.status).toEqual(400); + expect(response.body).toEqual({ + message: + 'Bulk alert suppression actions are not supported. Use "experimentalFeatures.bulkEditAlertSuppressionEnabled" config field to enable it.', + status_code: 400, + }); + }); + + it('returns error for dry run mode if experimental feature bulkEditAlertSuppressionEnabled is not enabled for alert suppression bulk action', async () => { + const response = await server.inject( + { ...getBulkActionEditAlertSuppressionRequest(), query: { dry_run: 'true' } }, + requestContextMock.convertContext(context) + ); + + expect(response.status).toEqual(400); + expect(response.body).toEqual({ + message: + 'Bulk alert suppression actions are not supported. Use "experimentalFeatures.bulkEditAlertSuppressionEnabled" config field to enable it.', + status_code: 400, + }); + }); +}); + function someBulkActionResults() { return { created: expect.any(Array), diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.ts index 6a685c0f7874a..6e607cdbd6518 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.ts @@ -43,6 +43,8 @@ import { bulkEnableDisableRules } from './bulk_enable_disable_rules'; import { fetchRulesByQueryOrIds } from './fetch_rules_by_query_or_ids'; import { bulkScheduleBackfill } from './bulk_schedule_rule_run'; import { createPrebuiltRuleAssetsClient } from '../../../../prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client'; +import type { ConfigType } from '../../../../../../config'; +import { checkAlertSuppressionBulkEditSupport } from '../../../logic/bulk_actions/check_alert_suppression_bulk_edit_support'; const MAX_RULES_TO_PROCESS_TOTAL = 10000; // Set a lower limit for bulk edit as the rules client might fail with a "Query @@ -96,7 +98,8 @@ const validateBulkAction = ( export const performBulkActionRoute = ( router: SecuritySolutionPluginRouter, - ml: SetupPlugins['ml'] + ml: SetupPlugins['ml'], + config: ConfigType ) => { router.versioned .post({ @@ -343,6 +346,16 @@ export const performBulkActionRoute = ( } case BulkActionTypeEnum.edit: { + const suppressionSupportError = await checkAlertSuppressionBulkEditSupport({ + editActions: body.edit, + licensing: ctx.licensing, + experimentalFeatures: config.experimentalFeatures, + }); + + if (suppressionSupportError) { + return siemResponse.error(suppressionSupportError); + } + if (isDryRun) { // during dry run only validation is getting performed and rule is not saved in ES const bulkActionOutcome = await initPromisePool({ diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/check_alert_suppression_bulk_edit_support.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/check_alert_suppression_bulk_edit_support.ts new file mode 100644 index 0000000000000..257307e2b794e --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/check_alert_suppression_bulk_edit_support.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { LicensingApiRequestHandlerContext } from '@kbn/licensing-plugin/server'; + +import { MINIMUM_LICENSE_FOR_SUPPRESSION } from '../../../../../../common/detection_engine/constants'; +import type { ConfigType } from '../../../../../config'; +import type { BulkActionEditPayload } from '../../../../../../common/api/detection_engine/rule_management'; + +import { hasAlertSuppressionBulkEditAction } from './utils'; + +export const checkAlertSuppressionBulkEditSupport = async ({ + editActions, + experimentalFeatures, + licensing, +}: { + editActions: BulkActionEditPayload[]; + experimentalFeatures: ConfigType['experimentalFeatures']; + licensing: LicensingApiRequestHandlerContext; +}) => { + const hasAlertSuppressionActions = hasAlertSuppressionBulkEditAction(editActions); + const isAlertSuppressionEnabled = experimentalFeatures.bulkEditAlertSuppressionEnabled; + + if (hasAlertSuppressionActions) { + if (!isAlertSuppressionEnabled) { + return { + body: `Bulk alert suppression actions are not supported. Use "experimentalFeatures.bulkEditAlertSuppressionEnabled" config field to enable it.`, + statusCode: 400, + }; + } + + const isAlertSuppressionLicenseValid = await licensing.license.hasAtLeast( + MINIMUM_LICENSE_FOR_SUPPRESSION + ); + if (!isAlertSuppressionLicenseValid) { + return { + body: `Alert suppression is enabled with ${MINIMUM_LICENSE_FOR_SUPPRESSION} license or above.`, + statusCode: 403, + }; + } + } + return undefined; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/rule_params_modifier.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/rule_params_modifier.test.ts index 2f7bef6d9a12d..5c6375a7225d2 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/rule_params_modifier.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/rule_params_modifier.test.ts @@ -7,6 +7,7 @@ import { addItemsToArray, deleteItemsFromArray, ruleParamsModifier } from './rule_params_modifier'; import { BulkActionEditTypeEnum } from '../../../../../../common/api/detection_engine/rule_management'; +import { AlertSuppressionMissingFieldsStrategyEnum } from '../../../../../../common/api/detection_engine/model/rule_schema/common_attributes.gen'; import type { RuleAlertType } from '../../../rule_schema'; describe('addItemsToArray', () => { @@ -685,6 +686,311 @@ describe('ruleParamsModifier', () => { }); }); + describe('alert_suppression', () => { + describe('delete_alert_suppression action', () => { + test.each([ + [ + 'removes alert suppression', + { + existingAlertSuppression: { + groupBy: ['field-1', 'field-2', 'field-3'], + missingFieldsStrategy: AlertSuppressionMissingFieldsStrategyEnum.suppress, + }, + resultingAlertSuppression: undefined, + isParamsUpdateSkipped: false, + ruleType: 'query', + }, + ], + [ + 'skips updates if suppression is not configured', + { + existingAlertSuppression: undefined, + resultingAlertSuppression: undefined, + isParamsUpdateSkipped: true, + ruleType: 'query', + }, + ], + [ + 'removes alert suppression in threshold rule', + { + existingAlertSuppression: { + duration: { value: 5, unit: 'h' }, + }, + resultingAlertSuppression: undefined, + isParamsUpdateSkipped: false, + ruleType: 'threshold', + }, + ], + [ + 'skips updates if suppression is not configured in threshold rule', + { + existingAlertSuppression: undefined, + resultingAlertSuppression: undefined, + isParamsUpdateSkipped: true, + ruleType: 'query', + }, + ], + ])( + 'should delete alert suppression, case:"%s"', + ( + caseName, + { existingAlertSuppression, resultingAlertSuppression, isParamsUpdateSkipped, ruleType } + ) => { + const { modifiedParams, isParamsUpdateSkipped: isUpdateSkipped } = ruleParamsModifier( + { + ...ruleParamsMock, + alertSuppression: existingAlertSuppression, + type: ruleType, + } as RuleAlertType['params'], + [ + { + type: BulkActionEditTypeEnum.delete_alert_suppression, + }, + ] + ); + expect(modifiedParams).toHaveProperty('alertSuppression', resultingAlertSuppression); + expect(isParamsUpdateSkipped).toBe(isUpdateSkipped); + } + ); + }); + + describe('set_alert_suppression action', () => { + test.each([ + [ + '3 existing groupBy fields overwritten with 2 of them = 2 groupBy fields', + { + existingAlertSuppression: { + groupBy: ['field-1', 'field-2', 'field-3'], + duration: { value: 1, unit: 'h' }, + missingFieldsStrategy: AlertSuppressionMissingFieldsStrategyEnum.suppress, + }, + alertSuppressionToSet: { + group_by: ['field-2', 'field-3'], + missingFieldsStrategy: AlertSuppressionMissingFieldsStrategyEnum.suppress, + }, + resultingAlertSuppression: { + groupBy: ['field-2', 'field-3'], + missingFieldsStrategy: AlertSuppressionMissingFieldsStrategyEnum.suppress, + }, + isParamsUpdateSkipped: false, + ruleType: 'query', + }, + ], + [ + '`undefined` existing alert suppression overwritten with 2 groupBy fields = 2 groupBy fields', + { + existingAlertSuppression: undefined, + alertSuppressionToSet: { + group_by: ['field-1', 'field-2'], + duration: { value: 5, unit: 'h' as const }, + missingFieldsStrategy: AlertSuppressionMissingFieldsStrategyEnum.suppress, + }, + resultingAlertSuppression: { + groupBy: ['field-1', 'field-2'], + duration: { value: 5, unit: 'h' }, + missingFieldsStrategy: AlertSuppressionMissingFieldsStrategyEnum.suppress, + }, + isParamsUpdateSkipped: false, + ruleType: 'query', + }, + ], + [ + 'sets missingFieldsStrategy to default when it is not set in action', + { + existingAlertSuppression: { + groupBy: ['field-1', 'field-2', 'field-3'], + missingFieldsStrategy: AlertSuppressionMissingFieldsStrategyEnum.doNotSuppress, + }, + alertSuppressionToSet: { + group_by: ['field-x'], + }, + resultingAlertSuppression: { + groupBy: ['field-x'], + missingFieldsStrategy: AlertSuppressionMissingFieldsStrategyEnum.suppress, + }, + isParamsUpdateSkipped: false, + ruleType: 'query', + }, + ], + [ + 'skips update when existing alert suppression is the same as action', + { + existingAlertSuppression: { + groupBy: ['field-1', 'field-2'], + duration: { value: 5, unit: 'h' }, + missingFieldsStrategy: AlertSuppressionMissingFieldsStrategyEnum.suppress, + }, + alertSuppressionToSet: { + group_by: ['field-1', 'field-2'], + missing_fields_strategy: AlertSuppressionMissingFieldsStrategyEnum.suppress, + duration: { value: 5, unit: 'h' as const }, + }, + resultingAlertSuppression: { + groupBy: ['field-1', 'field-2'], + duration: { value: 5, unit: 'h' }, + missingFieldsStrategy: AlertSuppressionMissingFieldsStrategyEnum.suppress, + }, + isParamsUpdateSkipped: true, + ruleType: 'query', + }, + ], + [ + 'skips update when existing alert suppression is the same as action for absent duration', + { + existingAlertSuppression: { + groupBy: ['field-1', 'field-2', 'field-3'], + missingFieldsStrategy: AlertSuppressionMissingFieldsStrategyEnum.suppress, + }, + alertSuppressionToSet: { + group_by: ['field-1', 'field-2', 'field-3'], + missing_fields_strategy: AlertSuppressionMissingFieldsStrategyEnum.suppress, + }, + resultingAlertSuppression: { + groupBy: ['field-1', 'field-2', 'field-3'], + missingFieldsStrategy: AlertSuppressionMissingFieldsStrategyEnum.suppress, + }, + isParamsUpdateSkipped: true, + ruleType: 'query', + }, + ], + ])( + 'should set alert suppression, case:"%s"', + ( + caseName, + { + existingAlertSuppression, + alertSuppressionToSet, + resultingAlertSuppression, + isParamsUpdateSkipped, + ruleType, + } + ) => { + const { modifiedParams, isParamsUpdateSkipped: isUpdateSkipped } = ruleParamsModifier( + { + ...ruleParamsMock, + alertSuppression: existingAlertSuppression, + type: ruleType, + } as RuleAlertType['params'], + [ + { + type: BulkActionEditTypeEnum.set_alert_suppression, + value: alertSuppressionToSet, + }, + ] + ); + expect(modifiedParams).toHaveProperty('alertSuppression', resultingAlertSuppression); + expect(isParamsUpdateSkipped).toBe(isUpdateSkipped); + } + ); + + test('should throw error when applied to threshold rule', () => { + expect(() => + ruleParamsModifier({ type: 'threshold' } as RuleAlertType['params'], [ + { + type: BulkActionEditTypeEnum.set_alert_suppression, + value: { + group_by: ['field-1', 'field-2', 'field-3'], + missing_fields_strategy: AlertSuppressionMissingFieldsStrategyEnum.suppress, + }, + }, + ]) + ).toThrow( + "Threshold rule doesn't support this action. Use 'set_alert_suppression_for_threshold' action instead" + ); + }); + }); + + describe('set_alert_suppression_for_threshold action', () => { + test.each([ + [ + 'overwrites existing alert suppression with new duration', + { + existingAlertSuppression: { + duration: { value: 1, unit: 'h' }, + }, + alertSuppressionToSet: { + duration: { value: 30, unit: 'm' as const }, + }, + resultingAlertSuppression: { + duration: { value: 30, unit: 'm' }, + }, + isParamsUpdateSkipped: false, + }, + ], + [ + 'set new duration when existing suppression is undefined', + { + existingAlertSuppression: undefined, + alertSuppressionToSet: { + duration: { value: 5, unit: 'h' as const }, + }, + resultingAlertSuppression: { + duration: { value: 5, unit: 'h' }, + }, + isParamsUpdateSkipped: false, + }, + ], + [ + 'skips update when existing alert suppression is the same as action', + { + existingAlertSuppression: { + duration: { value: 5, unit: 'h' }, + }, + alertSuppressionToSet: { + duration: { value: 5, unit: 'h' as const }, + }, + resultingAlertSuppression: { + duration: { value: 5, unit: 'h' }, + }, + isParamsUpdateSkipped: true, + ruleType: 'query', + }, + ], + ])( + 'should set alert suppression, case:"%s"', + ( + caseName, + { + existingAlertSuppression, + alertSuppressionToSet, + resultingAlertSuppression, + isParamsUpdateSkipped, + } + ) => { + const { modifiedParams, isParamsUpdateSkipped: isUpdateSkipped } = ruleParamsModifier( + { + ...ruleParamsMock, + alertSuppression: existingAlertSuppression, + type: 'threshold', + } as RuleAlertType['params'], + [ + { + type: BulkActionEditTypeEnum.set_alert_suppression_for_threshold, + value: alertSuppressionToSet, + }, + ] + ); + expect(modifiedParams).toHaveProperty('alertSuppression', resultingAlertSuppression); + expect(isParamsUpdateSkipped).toBe(isUpdateSkipped); + } + ); + + test('should throw error when applied not to threshold rule', () => { + expect(() => + ruleParamsModifier({ type: 'new_terms' } as RuleAlertType['params'], [ + { + type: BulkActionEditTypeEnum.set_alert_suppression_for_threshold, + value: { + duration: { value: 30, unit: 'm' as const }, + }, + }, + ]) + ).toThrow( + "new_terms rule type doesn't support this action. Use 'set_alert_suppression' action instead." + ); + }); + }); + }); describe('timeline', () => { test('should set timeline', () => { const { modifiedParams, isParamsUpdateSkipped } = ruleParamsModifier(ruleParamsMock, [ diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/rule_params_modifier.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/rule_params_modifier.ts index 16b0e365480d9..dbd83d7add646 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/rule_params_modifier.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/rule_params_modifier.ts @@ -11,10 +11,16 @@ import type { BulkActionEditForRuleParams, BulkActionEditPayloadIndexPatterns, BulkActionEditPayloadInvestigationFields, + BulkActionEditPayloadSetAlertSuppression, } from '../../../../../../common/api/detection_engine/rule_management'; import { BulkActionEditTypeEnum } from '../../../../../../common/api/detection_engine/rule_management'; import { invariant } from '../../../../../../common/utils/invariant'; import { calculateFromValue } from '../../../rule_types/utils/utils'; +import type { + AlertSuppressionCamel, + AlertSuppressionDuration, +} from '../../../../../../common/api/detection_engine/model/rule_schema/common_attributes.gen'; +import { DEFAULT_SUPPRESSION_MISSING_FIELDS_STRATEGY } from '../../../../../../common/detection_engine/constants'; export const addItemsToArray = (arr: T[], items: T[]): T[] => Array.from(new Set([...arr, ...items])); @@ -104,6 +110,26 @@ const shouldSkipInvestigationFieldsBulkAction = ( return false; }; +const hasMatchingDuration = ( + duration: AlertSuppressionDuration | undefined, + actionDuration: AlertSuppressionDuration | undefined +) => duration?.value === actionDuration?.value && duration?.unit === actionDuration?.unit; + +const shouldSkipAddAlertSuppressionBulkAction = ( + alertSuppression: AlertSuppressionCamel | undefined, + action: BulkActionEditPayloadSetAlertSuppression +) => { + if (!hasMatchingDuration(alertSuppression?.duration, action.value.duration)) { + return false; + } + + if (alertSuppression?.missingFieldsStrategy !== action.value.missing_fields_strategy) { + return false; + } + + return action.value.group_by.every((field) => alertSuppression?.groupBy?.includes(field)); +}; + // eslint-disable-next-line complexity const applyBulkActionEditToRuleParams = ( existingRuleParams: RuleAlertType['params'], @@ -113,7 +139,7 @@ const applyBulkActionEditToRuleParams = ( isActionSkipped: boolean; } => { let ruleParams = { ...existingRuleParams }; - // If the action is succesfully applied and the rule params are modified, + // If the action is successfully applied and the rule params are modified, // we update the following flag to false. As soon as the current function // returns this flag as false, at least once, for any action, we know that // the rule needs to be marked as having its params updated. @@ -241,6 +267,53 @@ const applyBulkActionEditToRuleParams = ( ruleParams.investigationFields = action.value; break; } + // alert suppression actions + case BulkActionEditTypeEnum.delete_alert_suppression: { + if (!ruleParams?.alertSuppression) { + isActionSkipped = true; + break; + } + + ruleParams.alertSuppression = undefined; + break; + } + case BulkActionEditTypeEnum.set_alert_suppression: { + invariant( + ruleParams.type !== 'threshold', + "Threshold rule doesn't support this action. Use 'set_alert_suppression_for_threshold' action instead" + ); + + if (shouldSkipAddAlertSuppressionBulkAction(ruleParams?.alertSuppression, action)) { + isActionSkipped = true; + break; + } + + ruleParams.alertSuppression = { + groupBy: action.value.group_by, + missingFieldsStrategy: + action.value.missing_fields_strategy ?? DEFAULT_SUPPRESSION_MISSING_FIELDS_STRATEGY, + duration: action.value.duration, + }; + + break; + } + case BulkActionEditTypeEnum.set_alert_suppression_for_threshold: { + invariant( + ruleParams.type === 'threshold', + `${ruleParams.type} rule type doesn't support this action. Use 'set_alert_suppression' action instead.` + ); + + if (hasMatchingDuration(ruleParams?.alertSuppression?.duration, action.value.duration)) { + isActionSkipped = true; + break; + } + + ruleParams.alertSuppression = { + duration: action.value.duration, + }; + + break; + } // timeline actions case BulkActionEditTypeEnum.set_timeline: { ruleParams = { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/utils.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/utils.test.ts new file mode 100644 index 0000000000000..7785eda8d78b8 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/utils.test.ts @@ -0,0 +1,56 @@ +/* + * 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 { BulkActionEditTypeEnum } from '../../../../../../common/api/detection_engine/rule_management'; +import type { BulkActionEditPayload } from '../../../../../../common/api/detection_engine/rule_management'; +import { hasAlertSuppressionBulkEditAction } from './utils'; + +describe('hasAlertSuppressionBulkEditAction', () => { + it('returns true if actions include set_alert_suppression_for_threshold', () => { + const actions: BulkActionEditPayload[] = [ + { + type: BulkActionEditTypeEnum.set_alert_suppression_for_threshold, + value: { duration: { unit: 'm', value: 4 } }, + }, + ]; + expect(hasAlertSuppressionBulkEditAction(actions)).toBe(true); + }); + + it('returns true if actions include delete_alert_suppression', () => { + const actions: BulkActionEditPayload[] = [ + { type: BulkActionEditTypeEnum.delete_alert_suppression }, + ]; + expect(hasAlertSuppressionBulkEditAction(actions)).toBe(true); + }); + + it('returns true if actions include set_alert_suppression', () => { + const actions: BulkActionEditPayload[] = [ + { type: BulkActionEditTypeEnum.set_alert_suppression, value: { group_by: ['test-'] } }, + ]; + expect(hasAlertSuppressionBulkEditAction(actions)).toBe(true); + }); + + it('returns false if actions do not include any suppression actions', () => { + const actions: BulkActionEditPayload[] = [ + { type: BulkActionEditTypeEnum.add_tags, value: ['tag1'] }, + { type: BulkActionEditTypeEnum.set_index_patterns, value: [] }, + ]; + expect(hasAlertSuppressionBulkEditAction(actions)).toBe(false); + }); + + it('returns true if at least one action is a suppression action among others', () => { + const actions: BulkActionEditPayload[] = [ + { type: BulkActionEditTypeEnum.add_tags, value: ['tag1'] }, + { type: BulkActionEditTypeEnum.set_alert_suppression, value: { group_by: ['test-'] } }, + ]; + expect(hasAlertSuppressionBulkEditAction(actions)).toBe(true); + }); + + it('returns false for empty actions array', () => { + expect(hasAlertSuppressionBulkEditAction([])).toBe(false); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/utils.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/utils.ts index 34e35f4baf49f..25cfda1acb2a1 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/utils.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/utils.ts @@ -11,6 +11,7 @@ import type { BulkActionEditType, NormalizedRuleAction, ThrottleForBulkActions, + BulkActionEditPayload, } from '../../../../../../common/api/detection_engine/rule_management'; import { BulkActionEditTypeEnum } from '../../../../../../common/api/detection_engine/rule_management'; import { transformToActionFrequency } from '../../normalization/rule_actions'; @@ -31,6 +32,31 @@ export const isIndexPatternsBulkEditAction = (editAction: BulkActionEditType) => return indexPatternsActions.includes(editAction); }; +/** + * helper utility that defines whether bulk edit action is related to alert suppression, i.e. one of: + * 'set_alert_suppression_for_threshold', 'delete_alert_suppression', 'set_alert_suppression' + * @param editAction {@link BulkActionEditType} + * @returns {boolean} + */ +const isAlertSuppressionBulkEditAction = (editAction: BulkActionEditType) => { + const bulkActions: BulkActionEditType[] = [ + BulkActionEditTypeEnum.set_alert_suppression_for_threshold, + BulkActionEditTypeEnum.delete_alert_suppression, + BulkActionEditTypeEnum.set_alert_suppression, + ]; + return bulkActions.includes(editAction); +}; + +/** + * Checks if any of the actions is related to alert suppression, i.e. one of: + * 'set_alert_suppression_for_threshold', 'delete_alert_suppression', 'set_alert_suppression' + * @param actions {@link BulkActionEditPayload[][]} + * @returns {boolean} + */ +export const hasAlertSuppressionBulkEditAction = (actions: BulkActionEditPayload[]): boolean => { + return actions.some((action) => isAlertSuppressionBulkEditAction(action.type)); +}; + /** * Separates system actions from actions and performs necessary transformations for * alerting rules client bulk edit operations. diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/validations.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/validations.ts index 0d6178196aea9..7d6ed4f7244c0 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/validations.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/validations.ts @@ -15,7 +15,7 @@ import { BulkActionsDryRunErrCodeEnum, } from '../../../../../../common/api/detection_engine/rule_management'; import type { PrebuiltRulesCustomizationStatus } from '../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status'; -import { isEsqlRule } from '../../../../../../common/detection_engine/utils'; +import { isEsqlRule, isThresholdRule } from '../../../../../../common/detection_engine/utils'; import { isMlRule } from '../../../../../../common/machine_learning/helpers'; import { invariant } from '../../../../../../common/utils/invariant'; import type { MlAuthz } from '../../../../machine_learning/authz'; @@ -169,4 +169,28 @@ export const dryRunValidateBulkEditRule = async ({ ), BulkActionsDryRunErrCodeEnum.ESQL_INDEX_PATTERN ); + + // if rule is threshold, set_alert_suppression action can't be applied to it + await throwDryRunError( + () => + invariant( + !isThresholdRule(rule.params.type) || + !edit.some((action) => action.type === BulkActionEditTypeEnum.set_alert_suppression), + "Threshold rule doesn't support this action. Use 'set_alert_suppression_for_threshold' action instead" + ), + BulkActionsDryRunErrCodeEnum.THRESHOLD_RULE_TYPE_IN_SUPPRESSION + ); + + // if rule noy threshold, set_alert_suppression_for_threshold action can't be applied to it + await throwDryRunError( + () => + invariant( + isThresholdRule(rule.params.type) || + !edit.some( + (action) => action.type === BulkActionEditTypeEnum.set_alert_suppression_for_threshold + ), + "Rule type doesn't support this action. Use 'set_alert_suppression' action instead." + ), + BulkActionsDryRunErrCodeEnum.UNSUPPORTED_RULE_IN_SUPPRESSION_FOR_THRESHOLD + ); }; 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 aa8da28f45aff..1d50234728f57 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 @@ -108,6 +108,7 @@ export function createTestConfig(options: CreateTestConfigOptions, testFiles?: s 'previewTelemetryUrlEnabled', 'riskScoringPersistence', 'riskScoringRoutesEnabled', + 'bulkEditAlertSuppressionEnabled', ])}`, `--plugin-path=${path.resolve( __dirname, diff --git a/x-pack/test/security_solution_api_integration/config/serverless/config.base.ts b/x-pack/test/security_solution_api_integration/config/serverless/config.base.ts index e490d90eac491..fd9ecd86e6566 100644 --- a/x-pack/test/security_solution_api_integration/config/serverless/config.base.ts +++ b/x-pack/test/security_solution_api_integration/config/serverless/config.base.ts @@ -37,6 +37,9 @@ export function createTestConfig(options: CreateTestConfigOptions) { ...svlSharedConfig.get('kbnTestServer.serverArgs'), '--serverless=security', `--xpack.actions.preconfigured=${JSON.stringify(PRECONFIGURED_ACTION_CONNECTORS)}`, + `--xpack.securitySolution.enableExperimental=${JSON.stringify([ + 'bulkEditAlertSuppressionEnabled', + ])}`, ...(options.kbnTestServerArgs || []), `--plugin-path=${path.resolve( __dirname, diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/index.ts index 01bd3e9460430..58553d7d49f96 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/index.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/index.ts @@ -12,6 +12,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./perform_bulk_action_dry_run')); loadTestFile(require.resolve('./perform_bulk_action_dry_run_ess')); loadTestFile(require.resolve('./perform_bulk_action')); + loadTestFile(require.resolve('./perform_bulk_action_suppression')); loadTestFile(require.resolve('./perform_bulk_action_ess')); loadTestFile(require.resolve('./perform_bulk_enable_disable.ts')); }); 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_dry_run.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_dry_run.ts index 2933c585c0861..ad47c2f7bfe76 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_dry_run.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_dry_run.ts @@ -10,7 +10,12 @@ import { BulkActionEditTypeEnum, } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management'; import moment from 'moment'; -import { getCustomQueryRuleParams, getSimpleMlRule, getSimpleRule } from '../../../utils'; +import { + getCustomQueryRuleParams, + getSimpleMlRule, + getSimpleRule, + getThresholdRuleForAlertTesting, +} from '../../../utils'; import { createRule, createAlertsIndex, @@ -406,5 +411,99 @@ export default ({ getService }: FtrProviderContext): void => { }); }); }); + // skips serverless MKI due to feature flag + describe('@skipInServerlessMKI alert suppression', () => { + it('should return error when attempting to apply set_alert_suppression bulk action to a threshold rule', async () => { + const createdRule = await createRule( + supertest, + log, + getThresholdRuleForAlertTesting(['*'], 'ruleId') + ); + + const { body } = await securitySolutionApi + .performRulesBulkAction({ + query: { dry_run: true }, + body: { + ids: [createdRule.id], + action: BulkActionTypeEnum.edit, + [BulkActionTypeEnum.edit]: [ + { + type: BulkActionEditTypeEnum.set_alert_suppression, + value: { group_by: ['host.name'], duration: { value: 5, unit: 'm' as const } }, + }, + ], + }, + }) + .expect(500); + + expect(body.attributes.summary).toEqual({ + failed: 1, + skipped: 0, + succeeded: 0, + total: 1, + }); + + expect(body.attributes.errors).toHaveLength(1); + expect(body.attributes.errors[0]).toEqual({ + err_code: 'THRESHOLD_RULE_TYPE_IN_SUPPRESSION', + message: + "Threshold rule doesn't support this action. Use 'set_alert_suppression_for_threshold' action instead", + rules: [ + { + id: createdRule.id, + name: createdRule.name, + }, + ], + status_code: 500, + }); + }); + + it('should return error when attempting to apply set_alert_suppression_for_threshold bulk action to a non-threshold rule', async () => { + const createdRule = await createRule( + supertest, + log, + getCustomQueryRuleParams({ + rule_id: 'rule-1', + }) + ); + + const { body } = await securitySolutionApi + .performRulesBulkAction({ + query: { dry_run: true }, + body: { + ids: [createdRule.id], + action: BulkActionTypeEnum.edit, + [BulkActionTypeEnum.edit]: [ + { + type: BulkActionEditTypeEnum.set_alert_suppression_for_threshold, + value: { duration: { value: 5, unit: 'm' as const } }, + }, + ], + }, + }) + .expect(500); + + expect(body.attributes.summary).toEqual({ + failed: 1, + skipped: 0, + succeeded: 0, + total: 1, + }); + + expect(body.attributes.errors).toHaveLength(1); + expect(body.attributes.errors[0]).toEqual({ + err_code: 'UNSUPPORTED_RULE_IN_SUPPRESSION_FOR_THRESHOLD', + message: + "Rule type doesn't support this action. Use 'set_alert_suppression' action instead.", + rules: [ + { + id: createdRule.id, + name: createdRule.name, + }, + ], + status_code: 500, + }); + }); + }); }); }; 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_suppression.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_suppression.ts new file mode 100644 index 0000000000000..5a3aa7ee82166 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action_suppression.ts @@ -0,0 +1,432 @@ +/* + * 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 expect from 'expect'; +import { + BulkActionTypeEnum, + BulkActionEditTypeEnum, +} from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management'; +import { AlertSuppressionMissingFieldsStrategyEnum } from '@kbn/security-solution-plugin/common/api/detection_engine/model/rule_schema/common_attributes.gen'; +import { getThresholdRuleForAlertTesting, getCustomQueryRuleParams } from '../../../utils'; +import { createRule, deleteAllRules } from '../../../../../../common/utils/security_solution'; + +import { FtrProviderContext } from '../../../../../ftr_provider_context'; + +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const log = getService('log'); + const securitySolutionApi = getService('securitySolutionApi'); + + // skips serverless MKI due to feature flag + describe('@ess @serverless @skipInServerlessMKI perform_bulk_action suppression', () => { + beforeEach(async () => { + await deleteAllRules(supertest, log); + }); + + describe('set_alert_suppression action', () => { + it('should overwrite suppression in a rule', async () => { + const ruleId = 'ruleId'; + const existingSuppression = { + group_by: ['field1'], + duration: { value: 5, unit: 'm' as const }, + missing_fields_strategy: AlertSuppressionMissingFieldsStrategyEnum.suppress, + }; + const suppressionToSet = { + group_by: ['field2'], + duration: { value: 10, unit: 'm' as const }, + missing_fields_strategy: AlertSuppressionMissingFieldsStrategyEnum.suppress, + }; + const resultingSuppression = { + group_by: ['field2'], + duration: { value: 10, unit: 'm' }, + missing_fields_strategy: AlertSuppressionMissingFieldsStrategyEnum.suppress, + }; + + await createRule( + supertest, + log, + getCustomQueryRuleParams({ rule_id: ruleId, alert_suppression: existingSuppression }) + ); + + const { body: bulkEditResponse } = await securitySolutionApi + .performRulesBulkAction({ + query: { dry_run: false }, + body: { + action: BulkActionTypeEnum.edit, + [BulkActionTypeEnum.edit]: [ + { + type: BulkActionEditTypeEnum.set_alert_suppression, + value: suppressionToSet, + }, + ], + }, + }) + .expect(200); + + expect(bulkEditResponse.attributes.summary).toEqual({ + failed: 0, + skipped: 0, + succeeded: 1, + total: 1, + }); + + // Check that the updated rule is returned with the response + expect(bulkEditResponse.attributes.results.updated[0].alert_suppression).toEqual( + resultingSuppression + ); + + // Check that the updates have been persisted + const { body: updatedRule } = await securitySolutionApi + .readRule({ + query: { rule_id: ruleId }, + }) + .expect(200); + + expect(updatedRule.alert_suppression).toEqual(resultingSuppression); + }); + + it('should set suppression to rules without configured suppression', async () => { + const suppressionToSet = { + group_by: ['field2'], + }; + const resultingSuppression = { + group_by: ['field2'], + missing_fields_strategy: AlertSuppressionMissingFieldsStrategyEnum.suppress, + }; + + await Promise.all([ + createRule(supertest, log, getCustomQueryRuleParams({ rule_id: 'id_1' })), + createRule(supertest, log, getCustomQueryRuleParams({ rule_id: 'id_2' })), + ]); + + const { body: bulkEditResponse } = await securitySolutionApi + .performRulesBulkAction({ + query: { dry_run: false }, + body: { + action: BulkActionTypeEnum.edit, + [BulkActionTypeEnum.edit]: [ + { + type: BulkActionEditTypeEnum.set_alert_suppression, + value: suppressionToSet, + }, + ], + }, + }) + .expect(200); + + expect(bulkEditResponse.attributes.summary).toEqual({ + failed: 0, + skipped: 0, + succeeded: 2, + total: 2, + }); + + // Check that the updated rule is returned with the response + expect(bulkEditResponse.attributes.results.updated[0].alert_suppression).toEqual( + resultingSuppression + ); + + // Check that the updates have been persisted + const { body: updatedRule } = await securitySolutionApi + .readRule({ + query: { rule_id: 'id_1' }, + }) + .expect(200); + + expect(updatedRule.alert_suppression).toEqual(resultingSuppression); + }); + + it('should return error when trying to set suppression to threshold rule', async () => { + const ruleId = 'ruleId'; + const existingSuppression = { duration: { value: 5, unit: 'm' as const } }; + const suppressionToSet = { + group_by: ['field2'], + }; + + await createRule(supertest, log, { + ...getThresholdRuleForAlertTesting(['*'], ruleId), + alert_suppression: existingSuppression, + }); + + const { body: bulkEditResponse } = await securitySolutionApi + .performRulesBulkAction({ + query: { dry_run: false }, + body: { + action: BulkActionTypeEnum.edit, + [BulkActionTypeEnum.edit]: [ + { + type: BulkActionEditTypeEnum.set_alert_suppression, + value: suppressionToSet, + }, + ], + }, + }) + .expect(500); + + expect(bulkEditResponse.attributes.summary).toEqual({ + failed: 1, + skipped: 0, + succeeded: 0, + total: 1, + }); + + expect(bulkEditResponse.attributes.errors).toHaveLength(1); + expect(bulkEditResponse.attributes.errors[0].message).toBe( + "Threshold rule doesn't support this action. Use 'set_alert_suppression_for_threshold' action instead" + ); + // Check that the updates did not apply to the rule + const { body: updatedRule } = await securitySolutionApi + .readRule({ + query: { rule_id: ruleId }, + }) + .expect(200); + + expect(updatedRule.alert_suppression).toEqual(existingSuppression); + }); + + it('should set suppression to rules without configured suppression and throw error on threshold one', async () => { + const suppressionToSet = { + group_by: ['field2'], + }; + + await Promise.all([ + createRule(supertest, log, getCustomQueryRuleParams({ rule_id: 'id_1' })), + createRule(supertest, log, getThresholdRuleForAlertTesting(['*'], 'id_2')), + ]); + + const { body: bulkEditResponse } = await securitySolutionApi + .performRulesBulkAction({ + query: { dry_run: false }, + body: { + action: BulkActionTypeEnum.edit, + [BulkActionTypeEnum.edit]: [ + { + type: BulkActionEditTypeEnum.set_alert_suppression, + value: suppressionToSet, + }, + ], + }, + }) + .expect(500); + + expect(bulkEditResponse.attributes.summary).toEqual({ + failed: 1, + skipped: 0, + succeeded: 1, + total: 2, + }); + }); + }); + + describe('delete_alert_suppression action', () => { + it('should delete suppression from rules', async () => { + await Promise.all([ + createRule( + supertest, + log, + getCustomQueryRuleParams({ + rule_id: 'id_1', + alert_suppression: { + group_by: ['field2'], + duration: { value: 10, unit: 'm' }, + missing_fields_strategy: AlertSuppressionMissingFieldsStrategyEnum.suppress, + }, + }) + ), + createRule(supertest, log, { + ...getThresholdRuleForAlertTesting(['*'], 'id_2'), + alert_suppression: { + duration: { value: 10, unit: 'm' }, + }, + }), + ]); + + const { body: bulkEditResponse } = await securitySolutionApi + .performRulesBulkAction({ + query: { dry_run: false }, + body: { + query: '', + action: BulkActionTypeEnum.edit, + [BulkActionTypeEnum.edit]: [ + { + type: BulkActionEditTypeEnum.delete_alert_suppression, + }, + ], + }, + }) + .expect(200); + + expect(bulkEditResponse.attributes.summary).toEqual({ + failed: 0, + skipped: 0, + succeeded: 2, + total: 2, + }); + + // Check that the updated rule is returned with the response + expect(bulkEditResponse.attributes.results.updated[0].alert_suppression).toEqual(undefined); + + // Check that the updates have been persisted + const updatedRules = await Promise.all([ + securitySolutionApi.readRule({ + query: { rule_id: 'id_1' }, + }), + await securitySolutionApi.readRule({ + query: { rule_id: 'id_2' }, + }), + ]); + expect(updatedRules[0].body.alert_suppression).toEqual(undefined); + expect(updatedRules[1].body.alert_suppression).toEqual(undefined); + }); + }); + + describe('set_alert_suppression_for_threshold action', () => { + it('should overwrite suppression in a rule', async () => { + const ruleId = 'ruleId'; + const existingSuppression = { + duration: { value: 5, unit: 'm' as const }, + }; + const suppressionToSet = { + duration: { value: 10, unit: 'm' as const }, + }; + const resultingSuppression = { + duration: { value: 10, unit: 'm' }, + }; + + await createRule(supertest, log, { + ...getThresholdRuleForAlertTesting(['*'], ruleId), + alert_suppression: existingSuppression, + }); + + const { body: bulkEditResponse } = await securitySolutionApi + .performRulesBulkAction({ + query: { dry_run: false }, + body: { + action: BulkActionTypeEnum.edit, + [BulkActionTypeEnum.edit]: [ + { + type: BulkActionEditTypeEnum.set_alert_suppression_for_threshold, + value: suppressionToSet, + }, + ], + }, + }) + .expect(200); + + expect(bulkEditResponse.attributes.summary).toEqual({ + failed: 0, + skipped: 0, + succeeded: 1, + total: 1, + }); + + // Check that the updated rule is returned with the response + expect(bulkEditResponse.attributes.results.updated[0].alert_suppression).toEqual( + resultingSuppression + ); + + // Check that the updates have been persisted + const { body: updatedRule } = await securitySolutionApi + .readRule({ + query: { rule_id: ruleId }, + }) + .expect(200); + + expect(updatedRule.alert_suppression).toEqual(resultingSuppression); + }); + + it('should set suppression to rule without configured suppression', async () => { + const ruleId = 'ruleId'; + const suppressionToSet = { + duration: { value: 10, unit: 'm' as const }, + }; + const resultingSuppression = { + duration: { value: 10, unit: 'm' }, + }; + + await createRule(supertest, log, getThresholdRuleForAlertTesting(['*'], ruleId)); + + const { body: bulkEditResponse } = await securitySolutionApi + .performRulesBulkAction({ + query: { dry_run: false }, + body: { + action: BulkActionTypeEnum.edit, + [BulkActionTypeEnum.edit]: [ + { + type: BulkActionEditTypeEnum.set_alert_suppression_for_threshold, + value: suppressionToSet, + }, + ], + }, + }) + .expect(200); + + expect(bulkEditResponse.attributes.summary).toEqual({ + failed: 0, + skipped: 0, + succeeded: 1, + total: 1, + }); + + // Check that the updated rule is returned with the response + expect(bulkEditResponse.attributes.results.updated[0].alert_suppression).toEqual( + resultingSuppression + ); + + // Check that the updates have been persisted + const { body: updatedRule } = await securitySolutionApi + .readRule({ + query: { rule_id: ruleId }, + }) + .expect(200); + + expect(updatedRule.alert_suppression).toEqual(resultingSuppression); + }); + + it('should return error when trying to set suppression not to threshold rule', async () => { + const ruleId = 'ruleId'; + + await createRule(supertest, log, getCustomQueryRuleParams({ rule_id: ruleId })); + + const { body: bulkEditResponse } = await securitySolutionApi + .performRulesBulkAction({ + query: { dry_run: false }, + body: { + action: BulkActionTypeEnum.edit, + [BulkActionTypeEnum.edit]: [ + { + type: BulkActionEditTypeEnum.set_alert_suppression_for_threshold, + value: { duration: { value: 10, unit: 'm' } }, + }, + ], + }, + }) + .expect(500); + + expect(bulkEditResponse.attributes.summary).toEqual({ + failed: 1, + skipped: 0, + succeeded: 0, + total: 1, + }); + + expect(bulkEditResponse.attributes.errors).toHaveLength(1); + expect(bulkEditResponse.attributes.errors[0].message).toBe( + "query rule type doesn't support this action. Use 'set_alert_suppression' action instead." + ); + // Check that the updates did not apply to the rule + const { body: updatedRule } = await securitySolutionApi + .readRule({ + query: { rule_id: ruleId }, + }) + .expect(200); + + expect(updatedRule.alert_suppression).toEqual(undefined); + }); + }); + }); +}; diff --git a/x-pack/test/security_solution_cypress/config.ts b/x-pack/test/security_solution_cypress/config.ts index f02968945087d..c0dc71482d20e 100644 --- a/x-pack/test/security_solution_cypress/config.ts +++ b/x-pack/test/security_solution_cypress/config.ts @@ -54,6 +54,9 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { // packages listed in fleet_packages.json // See: https://elastic.slack.com/archives/CNMNXV4RG/p1683033379063079 `--xpack.fleet.developer.bundledPackageLocation=./inexistentDir`, + `--xpack.securitySolution.enableExperimental=${JSON.stringify([ + 'bulkEditAlertSuppressionEnabled', + ])}`, '--csp.strict=false', '--csp.warnLegacyBrowsers=false', ], diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_edit_rules_suppression.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_edit_rules_suppression.cy.ts new file mode 100644 index 0000000000000..8f113081e69d3 --- /dev/null +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_edit_rules_suppression.cy.ts @@ -0,0 +1,230 @@ +/* + * 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 { deleteAlertsAndRules } from '../../../../../tasks/api_calls/common'; +import { MODAL_CONFIRMATION_BODY } from '../../../../../screens/alerts_detection_rules'; +import { RULES_BULK_EDIT_FORM_TITLE } from '../../../../../screens/rules_bulk_actions'; + +import { + DEFINITION_DETAILS, + SUPPRESS_BY_DETAILS, + SUPPRESS_FOR_DETAILS, + SUPPRESS_MISSING_FIELD, +} from '../../../../../screens/rule_details'; + +import { + selectAllRules, + goToRuleDetailsOf, + getRulesManagementTableRows, + disableAutoRefresh, +} from '../../../../../tasks/alerts_detection_rules'; + +import { + waitForBulkEditActionToFinish, + submitBulkEditForm, + clickSetAlertSuppressionMenuItem, + confirmBulkEditAction, + clickSetAlertSuppressionForThresholdMenuItem, + clickDeleteAlertSuppressionMenuItem, +} from '../../../../../tasks/rules_bulk_actions'; + +import { + fillAlertSuppressionFields, + selectAlertSuppressionPerInterval, + setAlertSuppressionDuration, + selectDoNotSuppressForMissingFields, +} from '../../../../../tasks/create_new_rule'; + +import { getDetails, assertDetailsNotExist } from '../../../../../tasks/rule_details'; +import { login } from '../../../../../tasks/login'; +import { visitRulesManagementTable } from '../../../../../tasks/rules_management'; +import { createRule } from '../../../../../tasks/api_calls/rules'; + +import { + getEqlRule, + getNewThreatIndicatorRule as getNewIMRule, + getNewRule, + getNewThresholdRule, + getMachineLearningRule, + getNewTermsRule, + getEsqlRule, +} from '../../../../../objects/rule'; + +const queryRule = getNewRule({ rule_id: '1', name: 'Query rule', enabled: false }); +const eqlRule = getEqlRule({ rule_id: '2', name: 'EQL Rule', enabled: false }); +const mlRule = getMachineLearningRule({ rule_id: '3', name: 'ML Rule', enabled: false }); +const imRule = getNewIMRule({ rule_id: '4', name: 'IM Rule', enabled: false }); +const newTermsRule = getNewTermsRule({ rule_id: '5', name: 'New Terms Rule', enabled: false }); +const esqlRule = getEsqlRule({ rule_id: '6', name: 'ES|QL Rule', enabled: false }); +const thresholdRule = getNewThresholdRule({ rule_id: '7', name: 'Threshold Rule', enabled: false }); + +// skipInServerlessMKI because of experiment feature flag +describe( + 'Bulk Edit - Alert Suppression', + { tags: ['@ess', '@serverless', '@skipInServerlessMKI'] }, + () => { + beforeEach(() => { + login(); + deleteAlertsAndRules(); + }); + + describe('Rules without suppression', () => { + beforeEach(() => { + createRule(queryRule); + createRule(eqlRule); + createRule(mlRule); + createRule(imRule); + createRule(newTermsRule); + createRule(esqlRule); + createRule(thresholdRule); + + visitRulesManagementTable(); + disableAutoRefresh(); + }); + + it('Set alert suppression', () => { + const skippedCount = 1; // Threshold rule is skipped + getRulesManagementTableRows().then((rows) => { + selectAllRules(); + clickSetAlertSuppressionMenuItem(); + + cy.get(MODAL_CONFIRMATION_BODY).contains( + `${skippedCount} threshold rule can't be edited. To bulk-apply alert suppression to this rule, use the Apply alert suppression to threshold rules option.` + ); + + confirmBulkEditAction(); + + cy.get(RULES_BULK_EDIT_FORM_TITLE).should('have.text', 'Apply alert suppression'); + + fillAlertSuppressionFields(['source.ip']); + selectAlertSuppressionPerInterval(); + setAlertSuppressionDuration(2, 'h'); + selectDoNotSuppressForMissingFields(); + + submitBulkEditForm(); + waitForBulkEditActionToFinish({ updatedCount: rows.length - skippedCount }); + + // check if one of the rules has been updated + goToRuleDetailsOf(eqlRule.name); + cy.get(DEFINITION_DETAILS).within(() => { + getDetails(SUPPRESS_BY_DETAILS).should('have.text', 'source.ip'); + getDetails(SUPPRESS_FOR_DETAILS).should('have.text', '2h'); + getDetails(SUPPRESS_MISSING_FIELD).should( + 'have.text', + 'Do not suppress alerts for events with missing fields' + ); + }); + }); + }); + + it('Set alert suppression for threshold rules', () => { + const skippedCount = 6; // Non threshold rules are skipped + getRulesManagementTableRows().then((rows) => { + selectAllRules(); + clickSetAlertSuppressionForThresholdMenuItem(); + + cy.get(MODAL_CONFIRMATION_BODY).contains( + `${skippedCount} rules can't be edited. To bulk-apply alert suppression to these rules, use the Apply alert suppression option.` + ); + + confirmBulkEditAction(); + + cy.get(RULES_BULK_EDIT_FORM_TITLE).should( + 'have.text', + 'Apply alert suppression to threshold rules' + ); + + setAlertSuppressionDuration(50, 'm'); + + submitBulkEditForm(); + waitForBulkEditActionToFinish({ updatedCount: rows.length - skippedCount }); + + // check if one of the rules has been updated + goToRuleDetailsOf(thresholdRule.name); + cy.get(DEFINITION_DETAILS).within(() => { + getDetails(SUPPRESS_FOR_DETAILS).should('have.text', '50m'); + }); + }); + }); + }); + + describe('Rules with suppression', () => { + beforeEach(() => { + const commonOverrides = { + alert_suppression: { + group_by: ['destination.ip'], + duration: { value: 30, unit: 'm' as const }, + missing_fields_strategy: 'suppress' as const, + }, + enabled: false, + }; + + createRule({ ...queryRule, ...commonOverrides }); + createRule({ ...eqlRule, ...commonOverrides }); + createRule({ ...mlRule, ...commonOverrides }); + createRule({ ...imRule, ...commonOverrides }); + createRule({ ...newTermsRule, ...commonOverrides }); + createRule({ ...esqlRule, ...commonOverrides }); + createRule({ + ...thresholdRule, + enabled: false, + alert_suppression: { duration: { value: 1, unit: 'h' as const } }, + }); + + visitRulesManagementTable(); + disableAutoRefresh(); + }); + + it('Delete alert suppression', () => { + getRulesManagementTableRows().then((rows) => { + selectAllRules(); + clickDeleteAlertSuppressionMenuItem(); + + cy.get(MODAL_CONFIRMATION_BODY).contains( + `This action will remove alert suppression from 7 rules. Click Delete to continue.` + ); + + confirmBulkEditAction(); + + waitForBulkEditActionToFinish({ updatedCount: rows.length }); + + // check if one of the rules has been updated and suppression is removed + goToRuleDetailsOf(newTermsRule.name); + cy.get(DEFINITION_DETAILS).within(() => { + assertDetailsNotExist(SUPPRESS_BY_DETAILS); + assertDetailsNotExist(SUPPRESS_FOR_DETAILS); + assertDetailsNotExist(SUPPRESS_MISSING_FIELD); + }); + }); + }); + + it('Overwrites existing alert suppression', () => { + const skippedCount = 1; // Threshold rule is skipped + getRulesManagementTableRows().then((rows) => { + selectAllRules(); + clickSetAlertSuppressionMenuItem(); + confirmBulkEditAction(); + + fillAlertSuppressionFields(['agent.name']); + + submitBulkEditForm(); + waitForBulkEditActionToFinish({ updatedCount: rows.length - skippedCount }); + + // check if one of the rules has been updated + goToRuleDetailsOf(esqlRule.name); + cy.get(DEFINITION_DETAILS).within(() => { + getDetails(SUPPRESS_BY_DETAILS).should('have.text', 'agent.name'); + getDetails(SUPPRESS_FOR_DETAILS).should('have.text', 'One rule execution'); + getDetails(SUPPRESS_MISSING_FIELD).should( + 'have.text', + 'Suppress and group alerts for events with missing fields' + ); + }); + }); + }); + }); + } +); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_edit_rules_suppression_basic_ess.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_edit_rules_suppression_basic_ess.cy.ts new file mode 100644 index 0000000000000..3906e8460f899 --- /dev/null +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_edit_rules_suppression_basic_ess.cy.ts @@ -0,0 +1,51 @@ +/* + * 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 { deleteAlertsAndRules } from '../../../../../tasks/api_calls/common'; +import { ALERT_SUPPRESSION_RULE_BULK_MENU_ITEM } from '../../../../../screens/rules_bulk_actions'; +import { TOOLTIP } from '../../../../../screens/common'; + +import { + selectAllRules, + getRulesManagementTableRows, + disableAutoRefresh, +} from '../../../../../tasks/alerts_detection_rules'; +import { clickBulkActionsButton } from '../../../../../tasks/rules_bulk_actions'; +import { login } from '../../../../../tasks/login'; +import { visitRulesManagementTable } from '../../../../../tasks/rules_management'; +import { startBasicLicense } from '../../../../../tasks/api_calls/licensing'; +import { createRule } from '../../../../../tasks/api_calls/rules'; + +import { getNewRule } from '../../../../../objects/rule'; + +const queryRule = getNewRule({ rule_id: '1', name: 'Query rule', enabled: false }); + +describe('Bulk Edit - Alert Suppression, Basic License', { tags: ['@ess'] }, () => { + beforeEach(() => { + login(); + deleteAlertsAndRules(); + startBasicLicense(); + }); + + beforeEach(() => { + createRule(queryRule); + + visitRulesManagementTable(); + disableAutoRefresh(); + }); + + it('bulk suppression is disabled and and upselling message is shown on hover', () => { + getRulesManagementTableRows().then((rows) => { + selectAllRules(); + clickBulkActionsButton(); + + cy.get(ALERT_SUPPRESSION_RULE_BULK_MENU_ITEM).should('be.disabled'); + cy.get(`${ALERT_SUPPRESSION_RULE_BULK_MENU_ITEM}`).parent().trigger('mouseover'); + // Platinum license is required for this option to be enabled + cy.get(TOOLTIP).contains('Platinum license'); + }); + }); +}); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_edit_rules_suppression_essentials_serverless.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_edit_rules_suppression_essentials_serverless.cy.ts new file mode 100644 index 0000000000000..213a4f72c62bd --- /dev/null +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_edit_rules_suppression_essentials_serverless.cy.ts @@ -0,0 +1,76 @@ +/* + * 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 { deleteAlertsAndRules } from '../../../../../tasks/api_calls/common'; +import { DEFINITION_DETAILS, SUPPRESS_BY_DETAILS } from '../../../../../screens/rule_details'; + +import { + selectAllRules, + goToRuleDetailsOf, + getRulesManagementTableRows, + disableAutoRefresh, +} from '../../../../../tasks/alerts_detection_rules'; + +import { + waitForBulkEditActionToFinish, + submitBulkEditForm, + clickSetAlertSuppressionMenuItem, +} from '../../../../../tasks/rules_bulk_actions'; + +import { fillAlertSuppressionFields } from '../../../../../tasks/create_new_rule'; + +import { getDetails } from '../../../../../tasks/rule_details'; +import { login } from '../../../../../tasks/login'; +import { visitRulesManagementTable } from '../../../../../tasks/rules_management'; +import { createRule } from '../../../../../tasks/api_calls/rules'; + +import { getNewRule } from '../../../../../objects/rule'; + +const queryRule = getNewRule({ rule_id: '1', name: 'Query rule', enabled: false }); + +// skipInServerlessMKI because of experiment feature flag +describe( + 'Bulk Edit - Alert Suppression, Essentials Serverless tier', + { + tags: ['@serverless', '@skipInServerlessMKI'], + env: { + ftrConfig: { + productTypes: [{ product_line: 'security', product_tier: 'essentials' }], + }, + }, + }, + () => { + beforeEach(() => { + login(); + deleteAlertsAndRules(); + }); + + beforeEach(() => { + createRule(queryRule); + + visitRulesManagementTable(); + disableAutoRefresh(); + }); + + it('Set alert suppression', () => { + getRulesManagementTableRows().then((rows) => { + selectAllRules(); + clickSetAlertSuppressionMenuItem(); + + fillAlertSuppressionFields(['source.ip']); + + submitBulkEditForm(); + waitForBulkEditActionToFinish({ updatedCount: rows.length }); + + // check if one of the rules has been updated + goToRuleDetailsOf(queryRule.name); + cy.get(DEFINITION_DETAILS).within(() => { + getDetails(SUPPRESS_BY_DETAILS).should('have.text', 'source.ip'); + }); + }); + }); + } +); 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 46a265f0632fe..65cc0f4831e54 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 @@ -102,6 +102,19 @@ export const RULES_BULK_EDIT_OVERWRITE_INVESTIGATION_FIELDS_CHECKBOX = export const RULES_BULK_EDIT_INVESTIGATION_FIELDS_WARNING = '[data-test-subj="bulkEditRulesInvestigationFieldsWarning"]'; +// ALERT SUPPRESSION +export const ALERT_SUPPRESSION_RULE_BULK_MENU_ITEM = + '[data-test-subj="alertSuppressionBulkEditRule"]'; + +export const SET_ALERT_SUPPRESSION_RULE_BULK_MENU_ITEM = + '[data-test-subj="setAlertSuppressionBulkEditRule"]'; + +export const SET_ALERT_SUPPRESSION_FOR_THRESHOLD_BULK_MENU_ITEM = + '[data-test-subj="setAlertSuppressionForThresholdBulkEditRule"]'; + +export const DELETE_ALERT_SUPPRESSION_RULE_BULK_MENU_ITEM = + '[data-test-subj="deleteAlertSuppressionBulkEditRule"]'; + // ENABLE/DISABLE export const ENABLE_RULE_BULK_BTN = '[data-test-subj="enableRuleBulk"]'; 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 01c5bc391f91c..aaa32a526621e 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 @@ -58,6 +58,10 @@ import { UPDATE_SCHEDULE_LOOKBACK_INPUT, UPDATE_SCHEDULE_MENU_ITEM, UPDATE_SCHEDULE_TIME_UNIT_SELECT, + ALERT_SUPPRESSION_RULE_BULK_MENU_ITEM, + SET_ALERT_SUPPRESSION_RULE_BULK_MENU_ITEM, + DELETE_ALERT_SUPPRESSION_RULE_BULK_MENU_ITEM, + SET_ALERT_SUPPRESSION_FOR_THRESHOLD_BULK_MENU_ITEM, } from '../screens/rules_bulk_actions'; import { SCHEDULE_DETAILS } from '../screens/rule_details'; @@ -281,6 +285,32 @@ export const checkOverwriteInvestigationFieldsCheckbox = () => { .should('be.checked'); }; +// edit alert suppression + +export const clickBulkActionsButton = () => { + cy.get(BULK_ACTIONS_BTN).click(); +}; + +const clickAlertSuppressionMenuItem = () => { + clickBulkActionsButton(); + cy.get(ALERT_SUPPRESSION_RULE_BULK_MENU_ITEM).click(); +}; + +export const clickSetAlertSuppressionMenuItem = () => { + clickAlertSuppressionMenuItem(); + cy.get(SET_ALERT_SUPPRESSION_RULE_BULK_MENU_ITEM).click(); +}; + +export const clickSetAlertSuppressionForThresholdMenuItem = () => { + clickAlertSuppressionMenuItem(); + cy.get(SET_ALERT_SUPPRESSION_FOR_THRESHOLD_BULK_MENU_ITEM).click(); +}; + +export const clickDeleteAlertSuppressionMenuItem = () => { + clickAlertSuppressionMenuItem(); + cy.get(DELETE_ALERT_SUPPRESSION_RULE_BULK_MENU_ITEM).click(); +}; + // EDIT-SCHEDULE export const clickUpdateScheduleMenuItem = () => { cy.get(BULK_ACTIONS_BTN).click(); @@ -445,3 +475,9 @@ export const scheduleManualRuleRunForSelectedRules = ( } cy.get(MODAL_CONFIRMATION_BTN).click(); }; + +// Confirmation modal + +export const confirmBulkEditAction = () => { + cy.get(MODAL_CONFIRMATION_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 f3f04dda79dbb..31bc6f0c8f6a9 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([ + 'bulkEditAlertSuppressionEnabled', + ])}`, '--csp.strict=false', '--csp.warnLegacyBrowsers=false', ], From a995f2643dfde3c149e2188b93ecae8fa0c6e3f2 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 19 Jun 2025 15:19:34 +0000 Subject: [PATCH 2/4] [CI] Auto-commit changed files from 'yarn openapi:bundle' --- ...ections_api_2023_10_31.bundled.schema.yaml | 90 +++++++++++++++++++ ...ections_api_2023_10_31.bundled.schema.yaml | 90 +++++++++++++++++++ 2 files changed, 180 insertions(+) diff --git a/x-pack/solutions/security/plugins/security_solution/docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml b/x-pack/solutions/security/plugins/security_solution/docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml index 7f4d3096106a5..ed4d3ed6a65a4 100644 --- a/x-pack/solutions/security/plugins/security_solution/docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/solutions/security/plugins/security_solution/docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml @@ -2015,6 +2015,54 @@ paths: timestamp: 2023-10-31T00:00:00.000Z ids: - 9e946bfc-3118-4c77-bb25-67d781191921 + example27: + description: >- + The following request set alert suppression to the rules with + the specified IDs. + summary: Edit - Set alert suppression to rules (idempotent) + value: + action: edit + edit: + - type: set_alert_suppression + value: + duration: + unit: h + value: 1 + group_by: + - source.ip + missing_fields_strategy: suppress + ids: + - 12345678-1234-1234-1234-1234567890ab + - 87654321-4321-4321-4321-0987654321ba + example28: + description: >- + The following request set alert suppression to threshold rules + with the specified IDs. + summary: Edit - Set alert suppression to threshold rules (idempotent) + value: + action: edit + edit: + - type: set_alert_suppression_for_threshold + value: + duration: + unit: h + value: 1 + ids: + - 12345678-1234-1234-1234-1234567890ab + - 87654321-4321-4321-4321-0987654321ba + example29: + description: >- + The following request removes alert suppression from the rules + with the specified IDs. If the rules do not have alert + suppression, no changes are made. + summary: Edit - Removes alert suppression from rules (idempotent) + value: + action: edit + edit: + - type: delete_alert_suppression + ids: + - 12345678-1234-1234-1234-1234567890ab + - 87654321-4321-4321-4321-0987654321ba schema: oneOf: - $ref: '#/components/schemas/BulkDeleteRules' @@ -4481,6 +4529,22 @@ components: - $ref: '#/components/schemas/BulkActionEditPayloadTimeline' - $ref: '#/components/schemas/BulkActionEditPayloadRuleActions' - $ref: '#/components/schemas/BulkActionEditPayloadSchedule' + - $ref: '#/components/schemas/BulkActionEditPayloadAlertSuppression' + BulkActionEditPayloadAlertSuppression: + anyOf: + - $ref: '#/components/schemas/BulkActionEditPayloadSetAlertSuppression' + - $ref: >- + #/components/schemas/BulkActionEditPayloadSetAlertSuppressionForThreshold + - $ref: '#/components/schemas/BulkActionEditPayloadDeleteAlertSuppression' + BulkActionEditPayloadDeleteAlertSuppression: + type: object + properties: + type: + enum: + - delete_alert_suppression + type: string + required: + - type BulkActionEditPayloadIndexPatterns: description: > Edits index patterns of rulesClient. @@ -4619,6 +4683,30 @@ components: required: - type - value + BulkActionEditPayloadSetAlertSuppression: + type: object + properties: + type: + enum: + - set_alert_suppression + type: string + value: + $ref: '#/components/schemas/AlertSuppression' + required: + - type + - value + BulkActionEditPayloadSetAlertSuppressionForThreshold: + type: object + properties: + type: + enum: + - set_alert_suppression_for_threshold + type: string + value: + $ref: '#/components/schemas/ThresholdAlertSuppression' + required: + - type + - value BulkActionEditPayloadTags: description: > Edits tags of rules. @@ -4680,6 +4768,8 @@ components: - ESQL_INDEX_PATTERN - MANUAL_RULE_RUN_FEATURE - MANUAL_RULE_RUN_DISABLED_RULE + - THRESHOLD_RULE_TYPE_IN_SUPPRESSION + - UNSUPPORTED_RULE_IN_SUPPRESSION_FOR_THRESHOLD type: string BulkActionSkipResult: type: object diff --git a/x-pack/solutions/security/plugins/security_solution/docs/openapi/serverless/security_solution_detections_api_2023_10_31.bundled.schema.yaml b/x-pack/solutions/security/plugins/security_solution/docs/openapi/serverless/security_solution_detections_api_2023_10_31.bundled.schema.yaml index 849240dbe914e..75bcda37bd8a5 100644 --- a/x-pack/solutions/security/plugins/security_solution/docs/openapi/serverless/security_solution_detections_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/solutions/security/plugins/security_solution/docs/openapi/serverless/security_solution_detections_api_2023_10_31.bundled.schema.yaml @@ -1884,6 +1884,54 @@ paths: timestamp: 2023-10-31T00:00:00.000Z ids: - 9e946bfc-3118-4c77-bb25-67d781191921 + example27: + description: >- + The following request set alert suppression to the rules with + the specified IDs. + summary: Edit - Set alert suppression to rules (idempotent) + value: + action: edit + edit: + - type: set_alert_suppression + value: + duration: + unit: h + value: 1 + group_by: + - source.ip + missing_fields_strategy: suppress + ids: + - 12345678-1234-1234-1234-1234567890ab + - 87654321-4321-4321-4321-0987654321ba + example28: + description: >- + The following request set alert suppression to threshold rules + with the specified IDs. + summary: Edit - Set alert suppression to threshold rules (idempotent) + value: + action: edit + edit: + - type: set_alert_suppression_for_threshold + value: + duration: + unit: h + value: 1 + ids: + - 12345678-1234-1234-1234-1234567890ab + - 87654321-4321-4321-4321-0987654321ba + example29: + description: >- + The following request removes alert suppression from the rules + with the specified IDs. If the rules do not have alert + suppression, no changes are made. + summary: Edit - Removes alert suppression from rules (idempotent) + value: + action: edit + edit: + - type: delete_alert_suppression + ids: + - 12345678-1234-1234-1234-1234567890ab + - 87654321-4321-4321-4321-0987654321ba schema: oneOf: - $ref: '#/components/schemas/BulkDeleteRules' @@ -3552,6 +3600,22 @@ components: - $ref: '#/components/schemas/BulkActionEditPayloadTimeline' - $ref: '#/components/schemas/BulkActionEditPayloadRuleActions' - $ref: '#/components/schemas/BulkActionEditPayloadSchedule' + - $ref: '#/components/schemas/BulkActionEditPayloadAlertSuppression' + BulkActionEditPayloadAlertSuppression: + anyOf: + - $ref: '#/components/schemas/BulkActionEditPayloadSetAlertSuppression' + - $ref: >- + #/components/schemas/BulkActionEditPayloadSetAlertSuppressionForThreshold + - $ref: '#/components/schemas/BulkActionEditPayloadDeleteAlertSuppression' + BulkActionEditPayloadDeleteAlertSuppression: + type: object + properties: + type: + enum: + - delete_alert_suppression + type: string + required: + - type BulkActionEditPayloadIndexPatterns: description: > Edits index patterns of rulesClient. @@ -3690,6 +3754,30 @@ components: required: - type - value + BulkActionEditPayloadSetAlertSuppression: + type: object + properties: + type: + enum: + - set_alert_suppression + type: string + value: + $ref: '#/components/schemas/AlertSuppression' + required: + - type + - value + BulkActionEditPayloadSetAlertSuppressionForThreshold: + type: object + properties: + type: + enum: + - set_alert_suppression_for_threshold + type: string + value: + $ref: '#/components/schemas/ThresholdAlertSuppression' + required: + - type + - value BulkActionEditPayloadTags: description: > Edits tags of rules. @@ -3751,6 +3839,8 @@ components: - ESQL_INDEX_PATTERN - MANUAL_RULE_RUN_FEATURE - MANUAL_RULE_RUN_DISABLED_RULE + - THRESHOLD_RULE_TYPE_IN_SUPPRESSION + - UNSUPPORTED_RULE_IN_SUPPRESSION_FOR_THRESHOLD type: string BulkActionSkipResult: type: object From 3322a08b7be0ccc027c9d7ed417bcb1cfa582103 Mon Sep 17 00:00:00 2001 From: Vitalii Dmyterko <92328789+vitaliidm@users.noreply.github.com> Date: Thu, 19 Jun 2025 16:39:31 +0100 Subject: [PATCH 3/4] update test --- .../bulk_actions/bulk_actions_route.test.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.test.ts b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.test.ts index 8529d286905a0..3c2fed5c255ee 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.test.ts @@ -217,7 +217,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 13 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 18 more"` ); }); @@ -279,7 +279,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 13 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 18 more"` ); }); @@ -397,7 +397,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 16 more"` ); }); @@ -419,7 +419,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 14 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 19 more"` ); }); @@ -457,7 +457,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 16 more"` ); }); @@ -502,7 +502,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 14 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 19 more"` ); }); @@ -524,7 +524,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 14 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 19 more"` ); }); @@ -562,7 +562,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 16 more"` ); }); @@ -584,7 +584,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 15 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 20 more"` ); }); From 5cdd6f0002a4432c363d067a24994677f367d86f Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 19 Jun 2025 15:55:46 +0000 Subject: [PATCH 4/4] [CI] Auto-commit changed files from 'make api-docs' --- oas_docs/output/kibana.yaml | 82 +++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index f0c78897188e9..20fcc68da92a9 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -10325,6 +10325,47 @@ paths: timestamp: '2023-10-31T00:00:00.000Z' ids: - 9e946bfc-3118-4c77-bb25-67d781191921 + example27: + description: The following request set alert suppression to the rules with the specified IDs. + summary: Edit - Set alert suppression to rules (idempotent) + value: + action: edit + edit: + - type: set_alert_suppression + value: + duration: + unit: h + value: 1 + group_by: + - source.ip + missing_fields_strategy: suppress + ids: + - 12345678-1234-1234-1234-1234567890ab + - 87654321-4321-4321-4321-0987654321ba + example28: + description: The following request set alert suppression to threshold rules with the specified IDs. + summary: Edit - Set alert suppression to threshold rules (idempotent) + value: + action: edit + edit: + - type: set_alert_suppression_for_threshold + value: + duration: + unit: h + value: 1 + ids: + - 12345678-1234-1234-1234-1234567890ab + - 87654321-4321-4321-4321-0987654321ba + example29: + description: The following request removes alert suppression from the rules with the specified IDs. If the rules do not have alert suppression, no changes are made. + summary: Edit - Removes alert suppression from rules (idempotent) + value: + action: edit + edit: + - type: delete_alert_suppression + ids: + - 12345678-1234-1234-1234-1234567890ab + - 87654321-4321-4321-4321-0987654321ba schema: oneOf: - $ref: '#/components/schemas/Security_Detections_API_BulkDeleteRules' @@ -35429,6 +35470,21 @@ components: - $ref: '#/components/schemas/Security_Detections_API_BulkActionEditPayloadTimeline' - $ref: '#/components/schemas/Security_Detections_API_BulkActionEditPayloadRuleActions' - $ref: '#/components/schemas/Security_Detections_API_BulkActionEditPayloadSchedule' + - $ref: '#/components/schemas/Security_Detections_API_BulkActionEditPayloadAlertSuppression' + Security_Detections_API_BulkActionEditPayloadAlertSuppression: + anyOf: + - $ref: '#/components/schemas/Security_Detections_API_BulkActionEditPayloadSetAlertSuppression' + - $ref: '#/components/schemas/Security_Detections_API_BulkActionEditPayloadSetAlertSuppressionForThreshold' + - $ref: '#/components/schemas/Security_Detections_API_BulkActionEditPayloadDeleteAlertSuppression' + Security_Detections_API_BulkActionEditPayloadDeleteAlertSuppression: + type: object + properties: + type: + enum: + - delete_alert_suppression + type: string + required: + - type Security_Detections_API_BulkActionEditPayloadIndexPatterns: description: | Edits index patterns of rulesClient. @@ -35534,6 +35590,30 @@ components: required: - type - value + Security_Detections_API_BulkActionEditPayloadSetAlertSuppression: + type: object + properties: + type: + enum: + - set_alert_suppression + type: string + value: + $ref: '#/components/schemas/Security_Detections_API_AlertSuppression' + required: + - type + - value + Security_Detections_API_BulkActionEditPayloadSetAlertSuppressionForThreshold: + type: object + properties: + type: + enum: + - set_alert_suppression_for_threshold + type: string + value: + $ref: '#/components/schemas/Security_Detections_API_ThresholdAlertSuppression' + required: + - type + - value Security_Detections_API_BulkActionEditPayloadTags: description: | Edits tags of rules. @@ -35587,6 +35667,8 @@ components: - ESQL_INDEX_PATTERN - MANUAL_RULE_RUN_FEATURE - MANUAL_RULE_RUN_DISABLED_RULE + - THRESHOLD_RULE_TYPE_IN_SUPPRESSION + - UNSUPPORTED_RULE_IN_SUPPRESSION_FOR_THRESHOLD type: string Security_Detections_API_BulkActionSkipResult: type: object