mirror of
https://github.com/elastic/kibana.git
synced 2025-06-28 03:01:21 -04:00
[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 <maxim.palenov@elastic.co>
This commit is contained in:
parent
884e51ae49
commit
40dccf51a2
52 changed files with 2857 additions and 27 deletions
|
@ -8395,6 +8395,47 @@ paths:
|
|||
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
|
||||
example30:
|
||||
description: The following request triggers the filling of gaps for the specified rule ids and time range
|
||||
summary: Fill Gaps - Manually trigger the filling of gaps for specified rules
|
||||
value:
|
||||
|
@ -58896,6 +58937,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.
|
||||
|
@ -59001,6 +59057,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.
|
||||
|
@ -59054,6 +59134,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
|
||||
- RULE_FILL_GAPS_DISABLED_RULE
|
||||
type: string
|
||||
Security_Detections_API_BulkActionSkipResult:
|
||||
|
|
|
@ -10070,6 +10070,47 @@ paths:
|
|||
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
|
||||
example30:
|
||||
description: The following request triggers the filling of gaps for the specified rule ids and time range
|
||||
summary: Fill Gaps - Manually trigger the filling of gaps for specified rules
|
||||
value:
|
||||
|
@ -68268,6 +68309,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.
|
||||
|
@ -68373,6 +68429,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.
|
||||
|
@ -68426,6 +68506,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
|
||||
- RULE_FILL_GAPS_DISABLED_RULE
|
||||
type: string
|
||||
Security_Detections_API_BulkActionSkipResult:
|
||||
|
|
|
@ -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<typeof BulkEditSkipReason>;
|
||||
export const BulkEditSkipReason = z.literal('RULE_NOT_MODIFIED');
|
||||
|
@ -59,6 +61,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',
|
||||
'RULE_FILL_GAPS_DISABLED_RULE',
|
||||
]);
|
||||
export type BulkActionsDryRunErrCodeEnum = typeof BulkActionsDryRunErrCode.enum;
|
||||
|
@ -258,6 +262,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;
|
||||
|
@ -382,6 +389,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<BulkActionEditPayloadAlertSuppression>;
|
||||
|
||||
export const BulkActionEditPayloadInternal = z.union([
|
||||
BulkActionEditPayloadTags,
|
||||
BulkActionEditPayloadIndexPatterns,
|
||||
|
@ -389,6 +431,7 @@ export const BulkActionEditPayloadInternal = z.union([
|
|||
BulkActionEditPayloadTimeline,
|
||||
BulkActionEditPayloadRuleActions,
|
||||
BulkActionEditPayloadSchedule,
|
||||
BulkActionEditPayloadAlertSuppression,
|
||||
]);
|
||||
|
||||
export type BulkActionEditPayload = z.infer<typeof BulkActionEditPayloadInternal>;
|
||||
|
|
|
@ -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'] } },
|
||||
],
|
||||
});
|
||||
|
|
|
@ -360,8 +360,48 @@ 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'
|
||||
example30:
|
||||
summary: Fill Gaps - Manually trigger the filling of gaps for specified rules
|
||||
description: The following request triggers the filling of gaps for the specified rule ids and time range
|
||||
value:
|
||||
|
@ -1060,6 +1100,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
|
||||
- RULE_FILL_GAPS_DISABLED_RULE
|
||||
|
||||
NormalizedRuleError:
|
||||
|
@ -1333,6 +1375,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:
|
||||
|
@ -1505,6 +1550,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'
|
||||
|
@ -1513,6 +1600,8 @@ components:
|
|||
- $ref: '#/components/schemas/BulkActionEditPayloadTimeline'
|
||||
- $ref: '#/components/schemas/BulkActionEditPayloadRuleActions'
|
||||
- $ref: '#/components/schemas/BulkActionEditPayloadSchedule'
|
||||
- $ref: '#/components/schemas/BulkActionEditPayloadAlertSuppression'
|
||||
|
||||
|
||||
BulkEditRules:
|
||||
allOf:
|
||||
|
|
|
@ -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 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"`
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -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 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"`
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -385,6 +385,136 @@ describe('Perform bulk action request schema', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('alert suppression', () => {
|
||||
test('valid request: delete_alert_suppression edit action', () => {
|
||||
const payload: PerformRulesBulkActionRequestBody = {
|
||||
query: 'name: test',
|
||||
action: BulkActionTypeEnum.edit,
|
||||
[BulkActionTypeEnum.edit]: [
|
||||
{
|
||||
type: BulkActionEditTypeEnum.delete_alert_suppression,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = PerformRulesBulkActionRequestBody.safeParse(payload);
|
||||
|
||||
expectParseSuccess(result);
|
||||
expect(result.data).toEqual(payload);
|
||||
});
|
||||
|
||||
test('valid request: set_alert_suppression edit action', () => {
|
||||
const payload: PerformRulesBulkActionRequestBody = {
|
||||
query: 'name: test',
|
||||
action: BulkActionTypeEnum.edit,
|
||||
[BulkActionTypeEnum.edit]: [
|
||||
{
|
||||
type: BulkActionEditTypeEnum.set_alert_suppression,
|
||||
value: {
|
||||
group_by: ['field-1', 'field-2', 'field-3'],
|
||||
duration: { value: 10, unit: 'h' },
|
||||
missing_fields_strategy: 'doNotSuppress',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = PerformRulesBulkActionRequestBody.safeParse(payload);
|
||||
|
||||
expectParseSuccess(result);
|
||||
expect(result.data).toEqual(payload);
|
||||
});
|
||||
|
||||
test('valid request: set_alert_suppression_for_threshold edit action', () => {
|
||||
const payload: PerformRulesBulkActionRequestBody = {
|
||||
query: 'name: test',
|
||||
action: BulkActionTypeEnum.edit,
|
||||
[BulkActionTypeEnum.edit]: [
|
||||
{
|
||||
type: BulkActionEditTypeEnum.set_alert_suppression,
|
||||
value: {
|
||||
group_by: ['field-1', 'field-2', 'field-3'],
|
||||
duration: { value: 10, unit: 'h' },
|
||||
missing_fields_strategy: 'doNotSuppress',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = PerformRulesBulkActionRequestBody.safeParse(payload);
|
||||
|
||||
expectParseSuccess(result);
|
||||
expect(result.data).toEqual(payload);
|
||||
});
|
||||
|
||||
test('invalid request: set_alert_suppression with too many group_by fields', () => {
|
||||
const payload = {
|
||||
query: 'name: test',
|
||||
action: BulkActionTypeEnum.edit,
|
||||
[BulkActionTypeEnum.edit]: [
|
||||
{
|
||||
type: BulkActionEditTypeEnum.set_alert_suppression,
|
||||
value: {
|
||||
group_by: ['field-1', 'field-2', 'field-3', 'field-4'],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = PerformRulesBulkActionRequestBody.safeParse(payload);
|
||||
|
||||
expectParseError(result);
|
||||
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
|
||||
`"edit.0.value.group_by: Array must contain at most 3 element(s)"`
|
||||
);
|
||||
});
|
||||
|
||||
test('invalid request: set_alert_suppression with empty group_by fields', () => {
|
||||
const payload = {
|
||||
query: 'name: test',
|
||||
action: BulkActionTypeEnum.edit,
|
||||
[BulkActionTypeEnum.edit]: [
|
||||
{
|
||||
type: BulkActionEditTypeEnum.set_alert_suppression,
|
||||
value: {
|
||||
group_by: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = PerformRulesBulkActionRequestBody.safeParse(payload);
|
||||
|
||||
expectParseError(result);
|
||||
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
|
||||
`"edit.0.value.group_by: Array must contain at least 1 element(s)"`
|
||||
);
|
||||
});
|
||||
|
||||
test('invalid request: set_alert_suppression with negative duration', () => {
|
||||
const payload = {
|
||||
query: 'name: test',
|
||||
action: BulkActionTypeEnum.edit,
|
||||
[BulkActionTypeEnum.edit]: [
|
||||
{
|
||||
type: BulkActionEditTypeEnum.set_alert_suppression,
|
||||
value: {
|
||||
group_by: ['field-1'],
|
||||
duration: { value: -5, unit: 'm' },
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = PerformRulesBulkActionRequestBody.safeParse(payload);
|
||||
|
||||
expectParseError(result);
|
||||
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
|
||||
`"edit.0.value.duration.value: Number must be greater than or equal to 1"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('timeline', () => {
|
||||
test('invalid request: wrong timeline payload type', () => {
|
||||
const payload = {
|
||||
|
@ -397,7 +527,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"`
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -419,7 +549,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 16 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 21 more"`
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -457,7 +587,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"`
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -502,7 +632,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 16 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 21 more"`
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -524,7 +654,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 16 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 21 more"`
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -562,7 +692,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"`
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -584,7 +714,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 17 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 22 more"`
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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")');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<RulesFilterOptions>): 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']) {
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -2057,6 +2057,54 @@ paths:
|
|||
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
|
||||
example30:
|
||||
description: >-
|
||||
The following request triggers the filling of gaps for the
|
||||
specified rule ids and time range
|
||||
|
@ -4493,6 +4541,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.
|
||||
|
@ -4631,6 +4695,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.
|
||||
|
@ -4692,6 +4780,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
|
||||
- RULE_FILL_GAPS_DISABLED_RULE
|
||||
type: string
|
||||
BulkActionSkipResult:
|
||||
|
|
|
@ -1921,6 +1921,54 @@ paths:
|
|||
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
|
||||
example30:
|
||||
description: >-
|
||||
The following request triggers the filling of gaps for the
|
||||
specified rule ids and time range
|
||||
|
@ -3823,6 +3871,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.
|
||||
|
@ -3961,6 +4025,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.
|
||||
|
@ -4022,6 +4110,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
|
||||
- RULE_FILL_GAPS_DISABLED_RULE
|
||||
type: string
|
||||
BulkActionSkipResult:
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
{
|
||||
|
|
|
@ -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 && (
|
||||
<EuiText size="xs" color="warning" data-test-subj="alertSuppressionWarning">
|
||||
|
|
|
@ -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 (
|
||||
<EuiFormRow
|
||||
|
@ -38,6 +40,7 @@ export function SuppressionFieldsSelector({
|
|||
componentProps={{
|
||||
browserFields: suppressibleFields,
|
||||
isDisabled: disabled,
|
||||
fullWidth,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(
|
||||
<BulkActionRuleErrorsList bulkAction={BulkActionTypeEnum.edit} ruleErrors={ruleErrors} />,
|
||||
{
|
||||
wrapper: TestProviders,
|
||||
}
|
||||
);
|
||||
|
||||
expect(screen.getByText(new RegExp(value, 'i'))).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 = ({
|
|||
/>
|
||||
</li>
|
||||
);
|
||||
case BulkActionsDryRunErrCodeEnum.THRESHOLD_RULE_TYPE_IN_SUPPRESSION:
|
||||
return (
|
||||
<li key={message}>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.thresholdRuleInSuppressionDescription"
|
||||
defaultMessage="{rulesCount, plural, =1 {# threshold rule} other {# threshold rules}} can't be edited. To bulk-apply alert suppression {rulesCount, plural, =1 {to this rule} other {to these rules}}, use the {actionStrong} option."
|
||||
values={{
|
||||
rulesCount,
|
||||
actionStrong: <strong>{BULK_ACTION_SET_ALERT_SUPPRESSION_FOR_THRESHOLD}</strong>,
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
case BulkActionsDryRunErrCodeEnum.UNSUPPORTED_RULE_IN_SUPPRESSION_FOR_THRESHOLD:
|
||||
return (
|
||||
<li key={message}>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.unsupportedRulesInThresholdSuppressionDescription"
|
||||
defaultMessage="{rulesCount, plural, =1 {# rule} other {# rules}} can't be edited. To bulk-apply alert suppression {rulesCount, plural, =1 {to this rule} other {to these rules}}, use the {actionStrong} option."
|
||||
values={{
|
||||
rulesCount,
|
||||
actionStrong: <strong>{BULK_ACTION_SET_ALERT_SUPPRESSION}</strong>,
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<li key={message}>
|
||||
|
|
|
@ -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<Props> = ({
|
||||
rulesCount,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
}) => (
|
||||
<EuiConfirmModal
|
||||
title={i18n.DELETE_CONFIRMATION_TITLE}
|
||||
onCancel={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"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.alertSuppression.deleteAlertSuppressionConfirmationModalBody"
|
||||
defaultMessage="This action will remove alert suppression from {rulesCount, plural, one {the chosen rule} other {{rulesCountStrong} rules}}. Click {deleteStrong} to continue."
|
||||
values={{
|
||||
rulesCount,
|
||||
rulesCountStrong: <strong>{rulesCount}</strong>,
|
||||
deleteStrong: (
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.alertSuppression.deleteAlertSuppressionConfirmationModalBodyDeleteBtnLabel"
|
||||
defaultMessage="Delete"
|
||||
/>
|
||||
</strong>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EuiConfirmModal>
|
||||
);
|
||||
|
||||
BulkEditDeleteAlertSuppressionConfirmation.displayName =
|
||||
'BulkEditDeleteAlertSuppressionConfirmation';
|
|
@ -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 <InvestigationFieldsForm {...props} editAction={editAction} />;
|
||||
|
||||
case BulkActionEditTypeEnum.set_alert_suppression:
|
||||
return <SetAlertSuppressionForm {...props} editAction={editAction} />;
|
||||
|
||||
case BulkActionEditTypeEnum.set_alert_suppression_for_threshold:
|
||||
return <SetAlertSuppressionForThresholdForm {...props} editAction={editAction} />;
|
||||
|
||||
case BulkActionEditTypeEnum.set_timeline:
|
||||
return <TimelineTemplateForm {...props} />;
|
||||
|
||||
|
|
|
@ -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<FormData>({
|
||||
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 (
|
||||
<BulkEditFormWrapper
|
||||
form={form}
|
||||
onClose={onClose}
|
||||
onSubmit={handleSubmit}
|
||||
title={i18n.SET_FOR_THRESHOLD_TITLE}
|
||||
>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="iInCircle" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="xs">{i18n.SUPPRESSION_FOR_THRESHOLD_INFO_TEXT}</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiFormRow
|
||||
data-test-subj="alertSuppressionDuration"
|
||||
label={i18n.DURATION_PER_TIME_PERIOD_LABEL}
|
||||
helpText={i18n.DURATION_PER_TIME_PERIOD_HELP_TEXT}
|
||||
>
|
||||
<UseMultiFields<{
|
||||
suppressionDurationValue: number | undefined;
|
||||
suppressionDurationUnit: string;
|
||||
}>
|
||||
fields={{
|
||||
suppressionDurationValue: {
|
||||
path: `value`,
|
||||
},
|
||||
suppressionDurationUnit: {
|
||||
path: `unit`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{({ suppressionDurationValue, suppressionDurationUnit }) => (
|
||||
<DurationInput
|
||||
isDisabled={false}
|
||||
durationValueField={suppressionDurationValue}
|
||||
durationUnitField={suppressionDurationUnit}
|
||||
aria-label={i18n.DURATION_PER_TIME_PERIOD_INPUT}
|
||||
minimumValue={1}
|
||||
/>
|
||||
)}
|
||||
</UseMultiFields>
|
||||
</EuiFormRow>
|
||||
</BulkEditFormWrapper>
|
||||
);
|
||||
});
|
|
@ -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<AlertSuppressionFormData> = {
|
||||
[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<string[]>(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 (
|
||||
<BulkEditFormWrapper
|
||||
form={form}
|
||||
onClose={onClose}
|
||||
onSubmit={handleSubmit}
|
||||
title={i18n.SET_TITLE}
|
||||
>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="iInCircle" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="xs">{i18n.SUPPRESSION_INFO_TEXT}</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
<AlertSuppressionEdit suppressibleFields={suppressibleFields} fullWidth />
|
||||
</BulkEditFormWrapper>
|
||||
);
|
||||
});
|
|
@ -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.',
|
||||
}
|
||||
),
|
||||
};
|
||||
|
|
|
@ -48,6 +48,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;
|
||||
|
@ -104,6 +108,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));
|
||||
|
@ -365,6 +376,7 @@ export const useBulkActions = ({
|
|||
const isDeleteDisabled = containsLoading || selectedRuleIds.length === 0;
|
||||
const isEditDisabled =
|
||||
missingActionPrivileges || containsLoading || selectedRuleIds.length === 0;
|
||||
const isAlertSuppressionDisabled = isEditDisabled || !isAlertSuppressionLicenseValid;
|
||||
|
||||
return [
|
||||
{
|
||||
|
@ -417,6 +429,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,
|
||||
|
@ -581,6 +607,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' },
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
},
|
||||
[
|
||||
|
@ -605,8 +661,11 @@ export const useBulkActions = ({
|
|||
executeBulkActionsDryRun,
|
||||
filterOptions,
|
||||
completeBulkEditForm,
|
||||
isBulkEditAlertSuppressionFeatureEnabled,
|
||||
startServices,
|
||||
canCreateTimelines,
|
||||
isAlertSuppressionLicenseValid,
|
||||
alertSuppressionUpsellingMessage,
|
||||
globalQuery,
|
||||
]
|
||||
);
|
||||
|
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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<RulesTableProps>(({ selectedTab }) => {
|
|||
rulesCount={rulesCount}
|
||||
/>
|
||||
)}
|
||||
{isBulkEditFlyoutVisible && bulkEditActionType !== undefined && (
|
||||
{isBulkEditFlyoutVisible &&
|
||||
bulkEditActionType &&
|
||||
(bulkEditActionType === BulkActionEditTypeEnum.delete_alert_suppression ? (
|
||||
<BulkEditDeleteAlertSuppressionConfirmation
|
||||
rulesCount={bulkActionsDryRunResult?.succeededRulesCount ?? 0}
|
||||
onCancel={handleBulkEditFormCancel}
|
||||
onConfirm={handleBulkEditFormConfirm}
|
||||
/>
|
||||
) : (
|
||||
<BulkEditFlyout
|
||||
rulesCount={bulkActionsDryRunResult?.succeededRulesCount ?? 0}
|
||||
editAction={bulkEditActionType}
|
||||
onClose={handleBulkEditFormCancel}
|
||||
onConfirm={handleBulkEditFormConfirm}
|
||||
/>
|
||||
)}
|
||||
))}
|
||||
|
||||
{shouldShowRulesTable && (
|
||||
<>
|
||||
{selectedTab === AllRulesTabs.monitoring && storeGapsInEventLogEnabled && (
|
||||
|
|
|
@ -38,6 +38,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';
|
||||
|
@ -128,6 +129,13 @@ export const getBulkActionEditRequest = () =>
|
|||
body: getPerformBulkActionEditSchemaMock(),
|
||||
});
|
||||
|
||||
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',
|
||||
|
|
|
@ -37,7 +37,7 @@ export const registerRuleManagementRoutes = (
|
|||
deleteRuleRoute(router);
|
||||
|
||||
// Rules bulk actions
|
||||
performBulkActionRoute(router, ml);
|
||||
performBulkActionRoute(router, ml, config);
|
||||
|
||||
// Rules export/import
|
||||
exportRulesRoute(router, config, logger);
|
||||
|
|
|
@ -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';
|
||||
|
@ -22,6 +23,7 @@ import {
|
|||
getBulkDisableRuleActionSchemaMock,
|
||||
} from '../../../../../../../common/api/detection_engine/rule_management/mocks';
|
||||
import { BulkActionsDryRunErrCodeEnum } from '../../../../../../../common/api/detection_engine';
|
||||
import type { ConfigType } from '../../../../../../config';
|
||||
|
||||
jest.mock('../../../../../machine_learning/authz');
|
||||
|
||||
|
@ -32,6 +34,9 @@ describe('Perform bulk action route', () => {
|
|||
let { clients, context } = requestContextMock.createTools();
|
||||
let ml: ReturnType<typeof mlServicesMock.createSetupContract>;
|
||||
const mockRule = getFindResultWithSingleHit().data[0];
|
||||
const experimentalFeatures = {
|
||||
bulkEditAlertSuppressionEnabled: true,
|
||||
} as ConfigType['experimentalFeatures'];
|
||||
|
||||
beforeEach(async () => {
|
||||
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', () => {
|
||||
|
@ -753,6 +786,52 @@ describe('Perform bulk action route', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Perform bulk action route, experimental feature bulkEditAlertSuppressionEnabled is disabled', () => {
|
||||
let server: ReturnType<typeof serverMock.create>;
|
||||
let { clients, context } = requestContextMock.createTools();
|
||||
let ml: ReturnType<typeof mlServicesMock.createSetupContract>;
|
||||
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),
|
||||
|
|
|
@ -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';
|
||||
import { bulkScheduleRuleGapFilling } from './bulk_schedule_rule_gap_filling';
|
||||
|
||||
const MAX_RULES_TO_PROCESS_TOTAL = 10000;
|
||||
|
@ -97,7 +99,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({
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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, [
|
||||
|
|
|
@ -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 = <T>(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 = {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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.
|
||||
|
|
|
@ -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';
|
||||
|
@ -180,4 +180,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
|
||||
);
|
||||
};
|
||||
|
|
|
@ -108,6 +108,7 @@ export function createTestConfig(options: CreateTestConfigOptions, testFiles?: s
|
|||
'previewTelemetryUrlEnabled',
|
||||
'riskScoringPersistence',
|
||||
'riskScoringRoutesEnabled',
|
||||
'bulkEditAlertSuppressionEnabled',
|
||||
])}`,
|
||||
`--plugin-path=${path.resolve(
|
||||
__dirname,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'));
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
@ -565,5 +570,100 @@ 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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -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',
|
||||
],
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
|
@ -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"]';
|
||||
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
],
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue