Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution] Add active maintenance window callout to the Rules Management page #155386

Merged
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3b1199f
Fetch currently running MWs
nikitaindik Apr 20, 2023
9d3d956
Add tests
nikitaindik Apr 21, 2023
d48d9d0
Revert back to ENABLE_MAINTENANCE_WINDOWS=false flag value in Alertin…
nikitaindik Apr 21, 2023
f3be8fc
Adjust the wording
nikitaindik Apr 21, 2023
3e55fe6
Tweak wording in test to match the changes in code
nikitaindik Apr 21, 2023
8a80c9b
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine Apr 21, 2023
415a3a2
Merge branch 'main' into maintenance-windows-rule-management
nikitaindik Apr 24, 2023
1f70328
Move API path constant to Alerting plugin
nikitaindik Apr 24, 2023
5cc23bc
Make Jest test names shorter and more consistent
nikitaindik Apr 24, 2023
4d5cbd9
Rewrite MaintenanceWindowCallout to function expression, add return type
nikitaindik Apr 24, 2023
35c4de8
Cy test: Increase MW lifetime to 60 secs to make sure test doesn't be…
nikitaindik Apr 24, 2023
08b24a4
Remove redundant fields in Jest mocks
nikitaindik Apr 24, 2023
cf12b62
Accept error wording suggestion
nikitaindik Apr 24, 2023
afe38bf
Accept callout description wording suggestion
nikitaindik Apr 24, 2023
23d738e
Add "as const" to a constant for a better typechecking
nikitaindik Apr 24, 2023
be65dae
Merge branch 'maintenance-windows-rule-management' of github.com:niki…
nikitaindik Apr 24, 2023
bcf49b2
Types
nikitaindik Apr 24, 2023
197d88f
Accept test title wording suggestion
nikitaindik Apr 24, 2023
e26577e
Merge branch 'maintenance-windows-rule-management' of github.com:niki…
nikitaindik Apr 24, 2023
32089fe
Jest tests: fix wording, remove network error logging
nikitaindik Apr 24, 2023
fdd93c8
Add path constants on alerting plugin side, use them in tests
nikitaindik Apr 24, 2023
3fb0b77
Use .some() instead of .find() to check for 'running' maintenance win…
nikitaindik Apr 24, 2023
45b6683
Merge branch 'main' into maintenance-windows-rule-management
nikitaindik Apr 24, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* 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_BASE_ALERTING_API_PATH } from '@kbn/alerting-plugin/common';
import { cleanKibana } from '../../tasks/common';
import { login, visit } from '../../tasks/login';
import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../urls/navigation';

describe('Maintenance window callout on Rule Management page', () => {
let maintenanceWindowId = '';

before(() => {
cleanKibana();
login();

// Create a test maintenance window
cy.request({
method: 'POST',
url: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window`,
headers: { 'kbn-xsrf': 'cypress-creds' },
body: {
title: 'My maintenance window',
duration: 10000,
r_rule: {
dtstart: new Date().toISOString(),
tzid: 'Europe/Amsterdam',
freq: 0,
count: 1,
},
},
}).then((response) => {
maintenanceWindowId = response.body.id;
});
});

after(() => {
// Delete a test maintenance window
cy.request({
method: 'DELETE',
url: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window/${maintenanceWindowId}`,
headers: { 'kbn-xsrf': 'cypress-creds' },
});
});

it('Displays the callout when there are running maintenance windows', () => {
visit(DETECTIONS_RULE_MANAGEMENT_URL);

cy.contains('A maintenance window is currently running');
});
});
3 changes: 2 additions & 1 deletion x-pack/plugins/security_solution/cypress/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"force": true
},
"@kbn/rison",
"@kbn/datemath"
"@kbn/datemath",
"@kbn/alerting-plugin"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* 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 { MaintenanceWindow } from '@kbn/alerting-plugin/common/maintenance_window';
import { KibanaServices } from '../../../../common/lib/kibana';
import { GET_ACTIVE_MAINTENANCE_WINDOWS_URL } from './constants';

export const fetchActiveMaintenanceWindows = async (
signal?: AbortSignal
): Promise<MaintenanceWindow[]> =>
KibanaServices.get().http.fetch(GET_ACTIVE_MAINTENANCE_WINDOWS_URL, {
method: 'GET',
signal,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
* 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_BASE_ALERTING_API_PATH } from '@kbn/alerting-plugin/common';

export const GET_ACTIVE_MAINTENANCE_WINDOWS_URL = `${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window/_active`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
/*
* 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, waitFor, cleanup } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
import { useAppToastsMock } from '../../../../common/hooks/use_app_toasts.mock';
import { MaintenanceWindowCallout } from './maintenance_window_callout';
import { TestProviders } from '../../../../common/mock';
import { fetchActiveMaintenanceWindows } from './api';

jest.mock('../../../../common/hooks/use_app_toasts');

jest.mock('./api', () => ({
fetchActiveMaintenanceWindows: jest.fn(() => Promise.resolve([])),
}));

const RUNNING_MAINTENANCE_WINDOW_1 = {
title: 'Maintenance window 1',
enabled: true,
duration: 1800000,
events: [{ gte: '2023-04-20T16:27:30.753Z', lte: '2023-04-20T16:57:30.753Z' }],
id: '63057284-ac31-42ba-fe22-adfe9732e5ae',
status: 'running',
expiration_date: '2024-04-20T16:27:41.301Z',
r_rule: {
dtstart: '2023-04-20T16:27:30.753Z',
tzid: 'Europe/Amsterdam',
freq: 0,
count: 1,
},
created_by: 'elastic',
updated_by: 'elastic',
created_at: '2023-04-20T16:27:41.301Z',
updated_at: '2023-04-20T16:27:41.301Z',
event_start_time: '2023-04-20T16:27:30.753Z',
event_end_time: '2023-04-20T16:57:30.753Z',
};

const RUNNING_MAINTENANCE_WINDOW_2 = {
title: 'Maintenance window 2',
enabled: true,
duration: 1800000,
events: [{ gte: '2023-04-20T16:47:42.871Z', lte: '2023-04-20T17:11:32.192Z' }],
id: '45894340-df98-11ed-ac81-bfcb4982b4fd',
status: 'running',
expiration_date: '2023-04-20T16:47:42.871Z',
r_rule: {
dtstart: '2023-04-20T16:47:42.871Z',
tzid: 'Europe/Amsterdam',
freq: 0,
count: 1,
},
created_by: 'elastic',
updated_by: 'elastic',
created_at: '2023-04-20T16:30:51.208Z',
updated_at: '2023-04-20T16:30:51.208Z',
event_start_time: '2023-04-20T16:47:42.871Z',
event_end_time: '2023-04-20T17:11:32.192Z',
};

const UPCOMING_MAINTENANCE_WINDOW = {
title: 'Upcoming maintenance window',
enabled: true,
duration: 45972,
events: [
{ gte: '2023-04-21T10:36:14.028Z', lte: '2023-04-21T10:37:00.000Z' },
{ gte: '2023-04-28T10:36:14.028Z', lte: '2023-04-28T10:37:00.000Z' },
{ gte: '2023-05-05T10:36:14.028Z', lte: '2023-05-05T10:37:00.000Z' },
{ gte: '2023-05-12T10:36:14.028Z', lte: '2023-05-12T10:37:00.000Z' },
{ gte: '2023-05-19T10:36:14.028Z', lte: '2023-05-19T10:37:00.000Z' },
{ gte: '2023-05-26T10:36:14.028Z', lte: '2023-05-26T10:37:00.000Z' },
{ gte: '2023-06-02T10:36:14.028Z', lte: '2023-06-02T10:37:00.000Z' },
{ gte: '2023-06-09T10:36:14.028Z', lte: '2023-06-09T10:37:00.000Z' },
{ gte: '2023-06-16T10:36:14.028Z', lte: '2023-06-16T10:37:00.000Z' },
{ gte: '2023-06-23T10:36:14.028Z', lte: '2023-06-23T10:37:00.000Z' },
{ gte: '2023-06-30T10:36:14.028Z', lte: '2023-06-30T10:37:00.000Z' },
{ gte: '2023-07-07T10:36:14.028Z', lte: '2023-07-07T10:37:00.000Z' },
{ gte: '2023-07-14T10:36:14.028Z', lte: '2023-07-14T10:37:00.000Z' },
{ gte: '2023-07-21T10:36:14.028Z', lte: '2023-07-21T10:37:00.000Z' },
{ gte: '2023-07-28T10:36:14.028Z', lte: '2023-07-28T10:37:00.000Z' },
{ gte: '2023-08-04T10:36:14.028Z', lte: '2023-08-04T10:37:00.000Z' },
{ gte: '2023-08-11T10:36:14.028Z', lte: '2023-08-11T10:37:00.000Z' },
{ gte: '2023-08-18T10:36:14.028Z', lte: '2023-08-18T10:37:00.000Z' },
{ gte: '2023-08-25T10:36:14.028Z', lte: '2023-08-25T10:37:00.000Z' },
{ gte: '2023-09-01T10:36:14.028Z', lte: '2023-09-01T10:37:00.000Z' },
{ gte: '2023-09-08T10:36:14.028Z', lte: '2023-09-08T10:37:00.000Z' },
{ gte: '2023-09-15T10:36:14.028Z', lte: '2023-09-15T10:37:00.000Z' },
{ gte: '2023-09-22T10:36:14.028Z', lte: '2023-09-22T10:37:00.000Z' },
{ gte: '2023-09-29T10:36:14.028Z', lte: '2023-09-29T10:37:00.000Z' },
{ gte: '2023-10-06T10:36:14.028Z', lte: '2023-10-06T10:37:00.000Z' },
{ gte: '2023-10-13T10:36:14.028Z', lte: '2023-10-13T10:37:00.000Z' },
{ gte: '2023-10-20T10:36:14.028Z', lte: '2023-10-20T10:37:00.000Z' },
{ gte: '2023-10-27T10:36:14.028Z', lte: '2023-10-27T10:37:00.000Z' },
{ gte: '2023-11-03T11:36:14.028Z', lte: '2023-11-03T11:37:00.000Z' },
{ gte: '2023-11-10T11:36:14.028Z', lte: '2023-11-10T11:37:00.000Z' },
{ gte: '2023-11-17T11:36:14.028Z', lte: '2023-11-17T11:37:00.000Z' },
{ gte: '2023-11-24T11:36:14.028Z', lte: '2023-11-24T11:37:00.000Z' },
{ gte: '2023-12-01T11:36:14.028Z', lte: '2023-12-01T11:37:00.000Z' },
{ gte: '2023-12-08T11:36:14.028Z', lte: '2023-12-08T11:37:00.000Z' },
{ gte: '2023-12-15T11:36:14.028Z', lte: '2023-12-15T11:37:00.000Z' },
{ gte: '2023-12-22T11:36:14.028Z', lte: '2023-12-22T11:37:00.000Z' },
{ gte: '2023-12-29T11:36:14.028Z', lte: '2023-12-29T11:37:00.000Z' },
{ gte: '2024-01-05T11:36:14.028Z', lte: '2024-01-05T11:37:00.000Z' },
{ gte: '2024-01-12T11:36:14.028Z', lte: '2024-01-12T11:37:00.000Z' },
{ gte: '2024-01-19T11:36:14.028Z', lte: '2024-01-19T11:37:00.000Z' },
{ gte: '2024-01-26T11:36:14.028Z', lte: '2024-01-26T11:37:00.000Z' },
{ gte: '2024-02-02T11:36:14.028Z', lte: '2024-02-02T11:37:00.000Z' },
{ gte: '2024-02-09T11:36:14.028Z', lte: '2024-02-09T11:37:00.000Z' },
{ gte: '2024-02-16T11:36:14.028Z', lte: '2024-02-16T11:37:00.000Z' },
{ gte: '2024-02-23T11:36:14.028Z', lte: '2024-02-23T11:37:00.000Z' },
{ gte: '2024-03-01T11:36:14.028Z', lte: '2024-03-01T11:37:00.000Z' },
{ gte: '2024-03-08T11:36:14.028Z', lte: '2024-03-08T11:37:00.000Z' },
{ gte: '2024-03-15T11:36:14.028Z', lte: '2024-03-15T11:37:00.000Z' },
{ gte: '2024-03-22T11:36:14.028Z', lte: '2024-03-22T11:37:00.000Z' },
{ gte: '2024-03-29T11:36:14.028Z', lte: '2024-03-29T11:37:00.000Z' },
{ gte: '2024-04-05T10:36:14.028Z', lte: '2024-04-05T10:37:00.000Z' },
{ gte: '2024-04-12T10:36:14.028Z', lte: '2024-04-12T10:37:00.000Z' },
{ gte: '2024-04-19T10:36:14.028Z', lte: '2024-04-19T10:37:00.000Z' },
],
id: '5eafe070-e030-11ed-ac81-bfcb4982b4fd',
status: 'upcoming',
expiration_date: '2024-04-21T10:36:26.999Z',
r_rule: {
dtstart: '2023-04-21T10:36:14.028Z',
tzid: 'Europe/Amsterdam',
freq: 3,
interval: 1,
byweekday: ['FR'],
},
created_by: 'elastic',
updated_by: 'elastic',
created_at: '2023-04-21T10:36:26.999Z',
updated_at: '2023-04-21T10:36:26.999Z',
event_start_time: '2023-04-28T10:36:14.028Z',
event_end_time: '2023-04-28T10:37:00.000Z',
};

describe('MaintenanceWindowCallout', () => {
let appToastsMock: jest.Mocked<ReturnType<typeof useAppToastsMock.create>>;

beforeEach(() => {
jest.resetAllMocks();

appToastsMock = useAppToastsMock.create();
(useAppToasts as jest.Mock).mockReturnValue(appToastsMock);
});

afterEach(() => {
cleanup();
jest.restoreAllMocks();
});

it('MW callout should be visible if currently there is at least one "running" maintenance window', async () => {
(fetchActiveMaintenanceWindows as jest.Mock).mockResolvedValue([RUNNING_MAINTENANCE_WINDOW_1]);

const { findByText } = render(<MaintenanceWindowCallout />, { wrapper: TestProviders });

expect(await findByText('A maintenance window is currently running')).toBeInTheDocument();
});

it('A single MW callout should be visible if currently there are multiple "running" maintenance windows', async () => {
(fetchActiveMaintenanceWindows as jest.Mock).mockResolvedValue([
RUNNING_MAINTENANCE_WINDOW_1,
RUNNING_MAINTENANCE_WINDOW_2,
]);

const { findAllByText } = render(<MaintenanceWindowCallout />, { wrapper: TestProviders });

expect(await findAllByText('A maintenance window is currently running')).toHaveLength(1);
});

it('MW callout should NOT be visible if currently there are no active (running or upcoming) maintenance windows', async () => {
(fetchActiveMaintenanceWindows as jest.Mock).mockResolvedValue([]);

const { container } = render(<MaintenanceWindowCallout />, { wrapper: TestProviders });

expect(container).toBeEmptyDOMElement();
});

it('MW callout should NOT be visible if currently there are no "running" maintenance windows', async () => {
(fetchActiveMaintenanceWindows as jest.Mock).mockResolvedValue([UPCOMING_MAINTENANCE_WINDOW]);

const { container } = render(<MaintenanceWindowCallout />, { wrapper: TestProviders });

expect(container).toBeEmptyDOMElement();
});

it('User should see an error toast if there was an error while fetching maintenance windows', async () => {
const createReactQueryWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// Turn retries off, otherwise we won't be able to test errors
retry: false,
},
},
});
const wrapper: React.FC = ({ children }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
return wrapper;
};

const mockError = new Error('Network error');
(fetchActiveMaintenanceWindows as jest.Mock).mockRejectedValue(mockError);

render(<MaintenanceWindowCallout />, { wrapper: createReactQueryWrapper() });

await waitFor(() => {
expect(appToastsMock.addError).toHaveBeenCalledTimes(1);
expect(appToastsMock.addError).toHaveBeenCalledWith(mockError, {
title: 'Failed to check if maintenance window is running',
toastMessage: "Notification actions won't run while a maintenance window is running.",
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* 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 { EuiCallOut } from '@elastic/eui';
import { useFetchActiveMaintenanceWindows } from './use_fetch_active_maintenance_windows';
import * as i18n from './translations';

export const MaintenanceWindowCallout = () => {
const { data } = useFetchActiveMaintenanceWindows();
const activeMaintenanceWindows = data || [];

if (activeMaintenanceWindows.find(({ status }) => status === 'running')) {
return (
<EuiCallOut title={i18n.MAINTENANCE_WINDOW_RUNNING} color="warning" iconType="iInCircle">
{i18n.MAINTENANCE_WINDOW_RUNNING_DESCRIPTION}
</EuiCallOut>
);
}

return null;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* 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 { i18n } from '@kbn/i18n';

export const MAINTENANCE_WINDOW_RUNNING = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleManagementUi.maintenanceWindowCallout.maintenanceWindowActive',
{
defaultMessage: 'A maintenance window is currently running',
}
);

export const MAINTENANCE_WINDOW_RUNNING_DESCRIPTION = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleManagementUi.maintenanceWindowCallout.maintenanceWindowActiveDescription',
{
defaultMessage: "Notification actions won't run while a maintenance window is running",
}
);

export const FETCH_ERROR = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleManagementUi.maintenanceWindowCallout.fetchError',
{
defaultMessage: 'Failed to check if maintenance window is running',
}
);

export const FETCH_ERROR_DESCRIPTION = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleManagementUi.maintenanceWindowCallout.fetchErrorDescription',
{
defaultMessage: "Notification actions won't run while a maintenance window is running.",
}
);
Loading