Skip to content

Commit

Permalink
Manual rule run from rule details and rules table (#9327) (#184500)
Browse files Browse the repository at this point in the history
## Summary

Main ticket elastic/security-team#9327

With this changes we introduce the way to schedule rule run manually.
There are two ways to do that in UI:
1. Via "All actions" button on rules management page
2. Via "All actions" button on rule's details page

**NOTES**:
1. To be able to test these changes, you need to enable feature flag
`manualRuleRunEnabled` first
2. Bulk action will be part of a separate ticket/PR

**RECORDING**:


https://github.com/elastic/kibana/assets/2700761/d49bad53-026e-49c2-aeea-481203260b23

### 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
- [ ] [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/6263)
- [ ] [Cypress DE (100 ESS & 100
Serverless)](https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/6280)
- [x] [Integration Rule Gaps (100 ESS & 100
Serverless)](https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/6257)

---------

Co-authored-by: Kibana Machine <[email protected]>
Co-authored-by: Ryland Herrick <[email protected]>
  • Loading branch information
3 people authored Jun 11, 2024
1 parent a1ededc commit 4392ee8
Show file tree
Hide file tree
Showing 37 changed files with 1,445 additions and 2 deletions.
2 changes: 2 additions & 0 deletions .buildkite/ftr_configs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,8 @@ enabled:
- x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/exceptions/workflows/basic_license_essentials_tier/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_gaps/trial_license_complete_tier/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_gaps/trial_license_complete_tier/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_creation/basic_license_essentials_tier/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_creation/basic_license_essentials_tier/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_creation/trial_license_complete_tier/configs/ess.config.ts
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/alerting/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ export const INTERNAL_ALERTING_BACKFILL_API_PATH =
`${INTERNAL_BASE_ALERTING_API_PATH}/rules/backfill` as const;
export const INTERNAL_ALERTING_BACKFILL_FIND_API_PATH =
`${INTERNAL_ALERTING_BACKFILL_API_PATH}/_find` as const;
export const INTERNAL_ALERTING_BACKFILL_SCHEDULE_API_PATH =
`${INTERNAL_ALERTING_BACKFILL_API_PATH}/_schedule` as const;

export const ALERTING_FEATURE_ID = 'alerts';
export const MONITORING_HISTORY_LIMIT = 200;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +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`,
PREVIEW: `${APP_UI_ID} singleRuleActions preview`,
SAVE: `${APP_UI_ID} singleRuleActions save`,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ import { RuleSnoozeBadge } from '../../../rule_management/components/rule_snooze
import { useBoolState } from '../../../../common/hooks/use_bool_state';
import { RuleDefinitionSection } from '../../../rule_management/components/rule_details/rule_definition_section';
import { RuleScheduleSection } from '../../../rule_management/components/rule_details/rule_schedule_section';
import { ManualRuleRunModal } from '../../../rule_gaps/components/manual_rule_run';
import { useManualRuleRunConfirmation } from '../../../rule_gaps/components/manual_rule_run/use_manual_rule_run_confirmation';
// eslint-disable-next-line no-restricted-imports
import { useLegacyUrlRedirect } from './use_redirect_legacy_url';
import { RuleDetailTabs, useRuleDetailsTabs } from './use_rule_details_tabs';
Expand Down Expand Up @@ -516,6 +518,13 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
confirmRuleDuplication,
} = useBulkDuplicateExceptionsConfirmation();

const {
isManualRuleRunConfirmationVisible,
showManualRuleRunConfirmation,
cancelManualRuleRun,
confirmManualRuleRun,
} = useManualRuleRunConfirmation();

if (
redirectToDetections(
isSignalIndexExists,
Expand Down Expand Up @@ -563,6 +572,9 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
{i18n.DELETE_CONFIRMATION_BODY}
</EuiConfirmModal>
)}
{isManualRuleRunConfirmationVisible && (
<ManualRuleRunModal onCancel={cancelManualRuleRun} onConfirm={confirmManualRuleRun} />
)}
<StyledFullHeightContainer onKeyDown={onKeyDown} ref={containerElement}>
<EuiWindowEvent event="resize" handler={noop} />
<FiltersGlobal show={showGlobalFilters({ globalFullScreen, graphEventId })}>
Expand Down Expand Up @@ -650,6 +662,7 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
hasActionsPrivileges
)}
showBulkDuplicateExceptionsConfirmation={showBulkDuplicateConfirmation}
showManualRuleRunConfirmation={showManualRuleRunConfirmation}
confirmDeletion={confirmDeletion}
/>
</EuiFlexItem>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* 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 { ScheduleBackfillResponseBody } from '@kbn/alerting-plugin/common/routes/backfill/apis/schedule';
import { scheduleRuleRunMock } from '../../logic/__mocks__/mock';

import type { ScheduleBackfillProps } from '../../types';

export const scheduleRuleRun = async ({
ruleIds,
timeRange,
}: ScheduleBackfillProps): Promise<ScheduleBackfillResponseBody> =>
Promise.resolve(scheduleRuleRunMock);
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* 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 moment from 'moment';

import { INTERNAL_ALERTING_BACKFILL_SCHEDULE_API_PATH } from '@kbn/alerting-plugin/common';

import { KibanaServices } from '../../../common/lib/kibana';
import { scheduleRuleRunMock } from '../logic/__mocks__/mock';
import { scheduleRuleRun } from './api';

const mockKibanaServices = KibanaServices.get as jest.Mock;
jest.mock('../../../common/lib/kibana');

const fetchMock = jest.fn();
mockKibanaServices.mockReturnValue({ http: { fetch: fetchMock } });

describe('Detections Rule Gaps API', () => {
describe('scheduleRuleRun', () => {
beforeEach(() => {
fetchMock.mockClear();
fetchMock.mockResolvedValue(scheduleRuleRunMock);
});

test('schedules rule run', async () => {
const timeRange = { startDate: moment().subtract(1, 'd'), endDate: moment() };
await scheduleRuleRun({
ruleIds: ['rule-1'],
timeRange,
});
expect(fetchMock).toHaveBeenCalledWith(
INTERNAL_ALERTING_BACKFILL_SCHEDULE_API_PATH,
expect.objectContaining({
body: `[{"rule_id":"rule-1","start":"${timeRange.startDate.toISOString()}","end":"${timeRange.endDate.toISOString()}"}]`,
method: 'POST',
})
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,43 @@
*/

import {
INTERNAL_ALERTING_BACKFILL_FIND_API_PATH,
INTERNAL_ALERTING_BACKFILL_API_PATH,
INTERNAL_ALERTING_BACKFILL_FIND_API_PATH,
INTERNAL_ALERTING_BACKFILL_SCHEDULE_API_PATH,
} from '@kbn/alerting-plugin/common';
import type { FindBackfillResponseBody } from '@kbn/alerting-plugin/common/routes/backfill/apis/find';
import type { ScheduleBackfillResponseBody } from '@kbn/alerting-plugin/common/routes/backfill/apis/schedule';
import { KibanaServices } from '../../../common/lib/kibana';
import type { ScheduleBackfillProps } from '../types';

/**
* Schedule rules run over a specified time range
*
* @param ruleIds `rule_id`s of each rule to be backfilled
* @param timeRange the time range over which the backfill should apply
*
* @throws An error if response is not OK
*/
export const scheduleRuleRun = async ({
ruleIds,
timeRange,
}: ScheduleBackfillProps): Promise<ScheduleBackfillResponseBody> => {
const params = ruleIds.map((ruleId) => {
return {
rule_id: ruleId,
start: timeRange.startDate.toISOString(),
end: timeRange.endDate.toISOString(),
};
});
return KibanaServices.get().http.fetch<ScheduleBackfillResponseBody>(
INTERNAL_ALERTING_BACKFILL_SCHEDULE_API_PATH,
{
method: 'POST',
version: '2023-10-31',
body: JSON.stringify(params),
}
);
};

/**
* Find backfills for the given rule IDs
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* 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 moment from 'moment';

import { act } from '@testing-library/react-hooks';
import { useScheduleRuleRunMutation } from './use_schedule_rule_run_mutation';
import { renderMutation } from '../../../../management/hooks/test_utils';
import { scheduleRuleRunMock } from '../../logic/__mocks__/mock';
import { INTERNAL_ALERTING_BACKFILL_SCHEDULE_API_PATH } from '@kbn/alerting-plugin/common';

import { KibanaServices } from '../../../../common/lib/kibana';

const mockKibanaServices = KibanaServices.get as jest.Mock;
jest.mock('../../../../common/lib/kibana');

const fetchMock = jest.fn();
mockKibanaServices.mockReturnValue({ http: { fetch: fetchMock } });

const apiVersion = '2023-10-31';

describe('Schedule rule run hook', () => {
let result: ReturnType<typeof useScheduleRuleRunMutation>;

beforeEach(() => {
fetchMock.mockClear();
fetchMock.mockResolvedValue(scheduleRuleRunMock);
});

it('schedules a rule run by calling the backfill API', async () => {
result = await renderMutation(() => useScheduleRuleRunMutation());

expect(fetchMock).toHaveBeenCalledTimes(0);

const timeRange = { startDate: moment().subtract(1, 'd'), endDate: moment() };

await act(async () => {
const res = await result.mutateAsync({ ruleIds: ['rule-1'], timeRange });
expect(res).toEqual(scheduleRuleRunMock);
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(fetchMock).toHaveBeenCalledWith(INTERNAL_ALERTING_BACKFILL_SCHEDULE_API_PATH, {
body: `[{"rule_id":"rule-1","start":"${timeRange.startDate.toISOString()}","end":"${timeRange.endDate.toISOString()}"}]`,
method: 'POST',
version: apiVersion,
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* 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 { INTERNAL_ALERTING_BACKFILL_SCHEDULE_API_PATH } from '@kbn/alerting-plugin/common';
import type { UseMutationOptions } from '@tanstack/react-query';
import { useMutation } from '@tanstack/react-query';
import type { ScheduleBackfillProps } from '../../types';
import { scheduleRuleRun } from '../api';

export const SCHEDULE_RULE_RUN_MUTATION_KEY = [
'POST',
INTERNAL_ALERTING_BACKFILL_SCHEDULE_API_PATH,
];

export const useScheduleRuleRunMutation = (
options?: UseMutationOptions<unknown, Error, ScheduleBackfillProps>
) => {
return useMutation((scheduleOptions: ScheduleBackfillProps) => scheduleRuleRun(scheduleOptions), {
...options,
mutationKey: SCHEDULE_RULE_RUN_MUTATION_KEY,
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* 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 { render, within } from '@testing-library/react';
import { ManualRuleRunModal } from '.';

describe('ManualRuleRunModal', () => {
const onCancelMock = jest.fn();
const onConfirmMock = jest.fn();

afterEach(() => {
onCancelMock.mockReset();
onConfirmMock.mockReset();
});

it('should render modal', () => {
const wrapper = render(
<ManualRuleRunModal onCancel={onCancelMock} onConfirm={onConfirmMock} />
);

expect(wrapper.getByTestId('manual-rule-run-modal-form')).toBeInTheDocument();
expect(wrapper.getByTestId('confirmModalCancelButton')).toBeEnabled();
expect(wrapper.getByTestId('confirmModalConfirmButton')).toBeEnabled();
});

it('should render confirmation button disabled if invalid time range has been selected', () => {
const wrapper = render(
<ManualRuleRunModal onCancel={onCancelMock} onConfirm={onConfirmMock} />
);

expect(wrapper.getByTestId('confirmModalConfirmButton')).toBeEnabled();

within(wrapper.getByTestId('end-date-picker')).getByText('Previous Month').click();

expect(wrapper.getByTestId('confirmModalConfirmButton')).toBeDisabled();
expect(wrapper.getByTestId('manual-rule-run-time-range-form')).toHaveTextContent(
'Selected time range is invalid'
);
});

it('should render confirmation button disabled if selected start date is more than 90 days in the past', () => {
const wrapper = render(
<ManualRuleRunModal onCancel={onCancelMock} onConfirm={onConfirmMock} />
);

expect(wrapper.getByTestId('confirmModalConfirmButton')).toBeEnabled();

within(wrapper.getByTestId('start-date-picker')).getByText('Previous Month').click();
within(wrapper.getByTestId('start-date-picker')).getByText('Previous Month').click();
within(wrapper.getByTestId('start-date-picker')).getByText('Previous Month').click();
within(wrapper.getByTestId('start-date-picker')).getByText('Previous Month').click();

expect(wrapper.getByTestId('confirmModalConfirmButton')).toBeDisabled();
expect(wrapper.getByTestId('manual-rule-run-time-range-form')).toHaveTextContent(
'Manual rule run cannot be scheduled earlier than 90 days ago'
);
});

it('should render confirmation button disabled if selected end date is in future', () => {
const wrapper = render(
<ManualRuleRunModal onCancel={onCancelMock} onConfirm={onConfirmMock} />
);

expect(wrapper.getByTestId('confirmModalConfirmButton')).toBeEnabled();

within(wrapper.getByTestId('end-date-picker')).getByText('Next month').click();

expect(wrapper.getByTestId('confirmModalConfirmButton')).toBeDisabled();
expect(wrapper.getByTestId('manual-rule-run-time-range-form')).toHaveTextContent(
'Manual rule run cannot be scheduled for the future'
);
});
});
Loading

0 comments on commit 4392ee8

Please sign in to comment.