Skip to content

Commit

Permalink
[Security Solution][Detections] Add bulk rule action for manual rule …
Browse files Browse the repository at this point in the history
…run (#9653) (#186293)

Main ticket elastic/security-team#9653

With this changes we introduce a new bulk action which allows to
schedule backfill for multiple rules.

**NOTES**:
- To be able to test these changes, you need to enable feature flag
`manualRuleRunEnabled` first

**RECORDING**:


https://github.com/elastic/kibana/assets/2700761/742083e7-090e-4805-8c3d-abcba04554b1

### Checklist

Delete any items that are not applicable to this PR.

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
    - [x] elastic/security-docs#5264
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [x] [Cypress RM (100 ESS & 100
Serverless)](https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/6410)
- [x] [Cypress DE (100 ESS & 100
Serverless)](https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/6411)
- [x] [Integration Rule Gaps (100 ESS & 100
Serverless)](https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/6412)
- [x] [Integration Bulk Actions (100 ESS & 100
Serverless)](https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/6413)

---------

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
e40pud and kibanamachine authored Jun 27, 2024
1 parent 47e0111 commit 66f36af
Show file tree
Hide file tree
Showing 30 changed files with 1,270 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ export const BulkActionsDryRunErrCode = z.enum([
'MACHINE_LEARNING_INDEX_PATTERN',
'ESQL_INDEX_PATTERN',
'INVESTIGATION_FIELDS_FEATURE',
'MANUAL_RULE_RUN_FEATURE',
'MANUAL_RULE_RUN_DISABLED_RULE',
]);
export type BulkActionsDryRunErrCodeEnum = typeof BulkActionsDryRunErrCode.enum;
export const BulkActionsDryRunErrCodeEnum = BulkActionsDryRunErrCode.enum;
Expand Down Expand Up @@ -157,6 +159,23 @@ export const BulkDuplicateRules = BulkActionBase.merge(
})
);

export type BulkManualRuleRun = z.infer<typeof BulkManualRuleRun>;
export const BulkManualRuleRun = BulkActionBase.merge(
z.object({
action: z.literal('run'),
run: z.object({
/**
* Start date of the manual rule run
*/
start_date: z.string(),
/**
* End date of the manual rule run
*/
end_date: z.string().optional(),
}),
})
);

/**
* The condition for throttling the notification: 'rule', 'no_actions', or time duration
*/
Expand All @@ -173,6 +192,7 @@ export const BulkActionType = z.enum([
'delete',
'duplicate',
'edit',
'run',
]);
export type BulkActionTypeEnum = typeof BulkActionType.enum;
export const BulkActionTypeEnum = BulkActionType.enum;
Expand Down Expand Up @@ -302,6 +322,7 @@ export const PerformBulkActionRequestBody = z.union([
BulkEnableRules,
BulkExportRules,
BulkDuplicateRules,
BulkManualRuleRun,
BulkEditRules,
]);
export type PerformBulkActionRequestBodyInput = z.input<typeof PerformBulkActionRequestBody>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ paths:
- $ref: '#/components/schemas/BulkEnableRules'
- $ref: '#/components/schemas/BulkExportRules'
- $ref: '#/components/schemas/BulkDuplicateRules'
- $ref: '#/components/schemas/BulkManualRuleRun'
- $ref: '#/components/schemas/BulkEditRules'
responses:
200:
Expand Down Expand Up @@ -78,6 +79,8 @@ components:
- MACHINE_LEARNING_INDEX_PATTERN
- ESQL_INDEX_PATTERN
- INVESTIGATION_FIELDS_FEATURE
- MANUAL_RULE_RUN_FEATURE
- MANUAL_RULE_RUN_DISABLED_RULE

NormalizedRuleError:
type: object
Expand Down Expand Up @@ -251,6 +254,29 @@ components:
required:
- action

BulkManualRuleRun:
allOf:
- $ref: '#/components/schemas/BulkActionBase'
- type: object
properties:
action:
type: string
enum: [run]
run:
type: object
properties:
start_date:
type: string
description: Start date of the manual rule run
end_date:
type: string
description: End date of the manual rule run
required:
- start_date
required:
- action
- run

ThrottleForBulkActions:
type: string
description: "The condition for throttling the notification: 'rule', 'no_actions', or time duration"
Expand All @@ -269,6 +295,7 @@ components:
- delete
- duplicate
- edit
- run

BulkActionEditType:
type: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,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 2 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 4 more"`
);
});

Expand All @@ -46,7 +46,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 2 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 4 more"`
);
});

Expand Down Expand Up @@ -74,7 +74,7 @@ describe('Perform bulk action request schema', () => {
expectParseError(result);

expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"ids: Expected array, received string, action: Invalid literal value, expected \\"delete\\", ids: Expected array, received string, action: Invalid literal value, expected \\"disable\\", ids: Expected array, received string, and 7 more"`
`"ids: Expected array, received string, action: Invalid literal value, expected \\"delete\\", ids: Expected array, received string, action: Invalid literal value, expected \\"disable\\", ids: Expected array, received string, and 10 more"`
);
});
});
Expand Down Expand Up @@ -143,6 +143,36 @@ describe('Perform bulk action request schema', () => {
});
});

describe('bulk manual rule run', () => {
test('invalid request: missing manual rule run payload', () => {
const payload = {
query: 'name: test',
action: BulkActionTypeEnum.run,
};

const result = PerformBulkActionRequestBody.safeParse(payload);
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 3 more"`
);
});

test('valid request', () => {
const payload: PerformBulkActionRequestBody = {
query: 'name: test',
action: BulkActionTypeEnum.run,
[BulkActionTypeEnum.run]: {
start_date: new Date().toISOString(),
end_date: new Date().toISOString(),
},
};
const result = PerformBulkActionRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
});

describe('bulk edit', () => {
describe('cases common to every type of editing', () => {
test('invalid request: missing edit payload', () => {
Expand All @@ -155,7 +185,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 1 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 3 more"`
);
});

Expand All @@ -170,7 +200,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 1 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 3 more"`
);
});
});
Expand All @@ -187,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 11 more"`
`"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 13 more"`
);
});

Expand Down Expand Up @@ -249,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 11 more"`
`"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 13 more"`
);
});

Expand Down Expand Up @@ -367,7 +397,7 @@ describe('Perform bulk action request schema', () => {

expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 9 more"`
`"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 11 more"`
);
});

Expand All @@ -389,7 +419,7 @@ describe('Perform bulk action request schema', () => {

expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 12 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 14 more"`
);
});

Expand Down Expand Up @@ -427,7 +457,7 @@ describe('Perform bulk action request schema', () => {

expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 9 more"`
`"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 11 more"`
);
});

Expand Down Expand Up @@ -472,7 +502,7 @@ describe('Perform bulk action request schema', () => {

expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 12 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 14 more"`
);
});

Expand All @@ -494,7 +524,7 @@ describe('Perform bulk action request schema', () => {

expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 12 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 14 more"`
);
});

Expand Down Expand Up @@ -532,7 +562,7 @@ describe('Perform bulk action request schema', () => {

expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 9 more"`
`"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 11 more"`
);
});

Expand All @@ -554,7 +584,7 @@ describe('Perform bulk action request schema', () => {

expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 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 15 more"`
);
});

Expand Down
8 changes: 8 additions & 0 deletions x-pack/plugins/security_solution/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,8 @@ export enum BulkActionsDryRunErrCode {
MACHINE_LEARNING_INDEX_PATTERN = 'MACHINE_LEARNING_INDEX_PATTERN',
ESQL_INDEX_PATTERN = 'ESQL_INDEX_PATTERN',
INVESTIGATION_FIELDS_FEATURE = 'INVESTIGATION_FIELDS_FEATURE',
MANUAL_RULE_RUN_FEATURE = 'MANUAL_RULE_RUN_FEATURE',
MANUAL_RULE_RUN_DISABLED_RULE = 'MANUAL_RULE_RUN_DISABLED_RULE',
}

export const MAX_NUMBER_OF_NEW_TERMS_FIELDS = 3;
Expand Down Expand Up @@ -493,3 +495,9 @@ export const MAX_COMMENT_LENGTH = 30000 as const;
* Cases external attachment IDs
*/
export const CASE_ATTACHMENT_ENDPOINT_TYPE_ID = 'endpoint' as const;

/**
* Rule gaps
*/
export const MAX_MANUAL_RULE_RUN_LOOKBACK_WINDOW_DAYS = 90;
export const MAX_MANUAL_RULE_RUN_BULK_SIZE = 100;
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const SINGLE_RULE_ACTIONS = {
DUPLICATE: `${APP_UI_ID} singleRuleActions duplicate`,
EXPORT: `${APP_UI_ID} singleRuleActions export`,
DELETE: `${APP_UI_ID} singleRuleActions delete`,
MANUAL_RULE_RUN: `${APP_UI_ID} singleRuleActions manual run`,
MANUAL_RULE_RUN: `${APP_UI_ID} singleRuleActions manual rule run`,
PREVIEW: `${APP_UI_ID} singleRuleActions preview`,
SAVE: `${APP_UI_ID} singleRuleActions save`,
};
Expand All @@ -23,6 +23,7 @@ export const BULK_RULE_ACTIONS = {
DISABLE: `${APP_UI_ID} bulkRuleActions disable`,
DUPLICATE: `${APP_UI_ID} bulkRuleActions duplicate`,
EXPORT: `${APP_UI_ID} bulkRuleActions export`,
MANUAL_RULE_RUN: `${APP_UI_ID} bulkRuleActions manual rule run`,
DELETE: `${APP_UI_ID} bulkRuleActions delete`,
EDIT: `${APP_UI_ID} bulkRuleActions edit`,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
import React from 'react';
import moment from 'moment';
import { fireEvent, render, screen } from '@testing-library/react';
import { ManualRuleRunModal, MAX_SCHEDULE_BACKFILL_LOOKBACK_WINDOW_DAYS } from '.';
import { ManualRuleRunModal } from '.';
import { MAX_MANUAL_RULE_RUN_LOOKBACK_WINDOW_DAYS } from '../../../../../common/constants';

const convertToDatePickerFormat = (date: moment.Moment) => {
return `${date.format('L')} ${date.format('LT')}`;
Expand Down Expand Up @@ -67,7 +68,7 @@ describe('ManualRuleRunModal', () => {
expect(confirmModalConfirmButton).toBeEnabled();

const now = moment();
const startDate = now.clone().subtract(MAX_SCHEDULE_BACKFILL_LOOKBACK_WINDOW_DAYS, 'd');
const startDate = now.clone().subtract(MAX_MANUAL_RULE_RUN_LOOKBACK_WINDOW_DAYS, 'd');

fireEvent.change(startDatePicker, {
target: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,13 @@ import {
} from '@elastic/eui';
import moment from 'moment';
import React, { useCallback, useMemo, useState } from 'react';
import { MAX_MANUAL_RULE_RUN_LOOKBACK_WINDOW_DAYS } from '../../../../../common/constants';
import { TECHNICAL_PREVIEW, TECHNICAL_PREVIEW_TOOLTIP } from '../../../../common/translations';

import * as i18n from './translations';

const MANUAL_RULE_RUN_MODAL_WIDTH = 600;

export const MAX_SCHEDULE_BACKFILL_LOOKBACK_WINDOW_DAYS = 90;

interface ManualRuleRunModalProps {
onCancel: () => void;
onConfirm: (timeRange: { startDate: moment.Moment; endDate: moment.Moment }) => void;
Expand All @@ -43,15 +42,15 @@ const ManualRuleRunModalComponent = ({ onCancel, onConfirm }: ManualRuleRunModal

const isStartDateOutOfRange = now
.clone()
.subtract(MAX_SCHEDULE_BACKFILL_LOOKBACK_WINDOW_DAYS, 'd')
.subtract(MAX_MANUAL_RULE_RUN_LOOKBACK_WINDOW_DAYS, 'd')
.isAfter(startDate);
const isEndDateInFuture = endDate.isAfter(now);
const isInvalidTimeRange = startDate.isSameOrAfter(endDate);
const isInvalid = isStartDateOutOfRange || isEndDateInFuture || isInvalidTimeRange;
const errorMessage = useMemo(() => {
if (isStartDateOutOfRange) {
return i18n.MANUAL_RULE_RUN_START_DATE_OUT_OF_RANGE_ERROR(
MAX_SCHEDULE_BACKFILL_LOOKBACK_WINDOW_DAYS
MAX_MANUAL_RULE_RUN_LOOKBACK_WINDOW_DAYS
);
}
if (isEndDateInFuture) {
Expand Down
Loading

0 comments on commit 66f36af

Please sign in to comment.