Fix performance issues affecting rules management (#135311)

This commit is contained in:
Dmitrii Shevchenko 2022-07-11 17:21:44 +02:00 committed by GitHub
parent d8553f5647
commit c7a5b1336d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 736 additions and 971 deletions

View file

@ -1,2 +1,3 @@
videos
screenshots
downloads

View file

@ -1 +0,0 @@
{"savedObjectId":"46cca0e0-2580-11ec-8e56-9dafa0b0343b","version":"WzIyNjIzNCwxXQ==","columns":[{"id":"@timestamp"},{"id":"user.name"},{"id":"event.category"},{"id":"event.action"},{"id":"host.name"}],"kqlMode":"filter","kqlQuery":{"filterQuery":{"kuery":{"expression":"host.name: *","kind":"kuery"}}},"dateRange":{"start":"1514809376000","end":"1577881376000"},"description":"This is the best timeline","title":"Security Timeline","created":1633399341550,"createdBy":"elastic","updated":1633399341550,"updatedBy":"elastic","savedQueryId":null,"dataViewId":null,"timelineType":"default","sort":[],"eventNotes":[],"globalNotes":[],"pinnedEventIds":[]}

View file

@ -163,7 +163,7 @@ export const goToTheRuleDetailsOf = (ruleName: string) => {
export const loadPrebuiltDetectionRules = () => {
cy.get(LOAD_PREBUILT_RULES_BTN)
.should('exist')
.should('be.enabled')
.pipe(($el) => $el.trigger('click'))
.should('be.disabled');
};

View file

@ -5,31 +5,31 @@
* 2.0.
*/
export const SECURITY_ACTIONS_PREFIX = 'securitySolution';
import { APP_UI_ID } from '../../../../common/constants';
export const SINGLE_RULE_ACTIONS = {
ENABLE: `${SECURITY_ACTIONS_PREFIX} singleRuleActions enable`,
DISABLE: `${SECURITY_ACTIONS_PREFIX} singleRuleActions disable`,
DUPLICATE: `${SECURITY_ACTIONS_PREFIX} singleRuleActions duplicate`,
EXPORT: `${SECURITY_ACTIONS_PREFIX} singleRuleActions export`,
DELETE: `${SECURITY_ACTIONS_PREFIX} singleRuleActions delete`,
PREVIEW: `${SECURITY_ACTIONS_PREFIX} singleRuleActions preview`,
SAVE: `${SECURITY_ACTIONS_PREFIX} singleRuleActions save`,
ENABLE: `${APP_UI_ID} singleRuleActions enable`,
DISABLE: `${APP_UI_ID} singleRuleActions disable`,
DUPLICATE: `${APP_UI_ID} singleRuleActions duplicate`,
EXPORT: `${APP_UI_ID} singleRuleActions export`,
DELETE: `${APP_UI_ID} singleRuleActions delete`,
PREVIEW: `${APP_UI_ID} singleRuleActions preview`,
SAVE: `${APP_UI_ID} singleRuleActions save`,
};
export const BULK_RULE_ACTIONS = {
ENABLE: `${SECURITY_ACTIONS_PREFIX} bulkRuleActions enable`,
DISABLE: `${SECURITY_ACTIONS_PREFIX} bulkRuleActions disable`,
DUPLICATE: `${SECURITY_ACTIONS_PREFIX} bulkRuleActions duplicate`,
EXPORT: `${SECURITY_ACTIONS_PREFIX} bulkRuleActions export`,
DELETE: `${SECURITY_ACTIONS_PREFIX} bulkRuleActions delete`,
EDIT: `${SECURITY_ACTIONS_PREFIX} bulkRuleActions edit`,
ENABLE: `${APP_UI_ID} bulkRuleActions enable`,
DISABLE: `${APP_UI_ID} bulkRuleActions disable`,
DUPLICATE: `${APP_UI_ID} bulkRuleActions duplicate`,
EXPORT: `${APP_UI_ID} bulkRuleActions export`,
DELETE: `${APP_UI_ID} bulkRuleActions delete`,
EDIT: `${APP_UI_ID} bulkRuleActions edit`,
};
export const RULES_TABLE_ACTIONS = {
REFRESH: `${SECURITY_ACTIONS_PREFIX} rulesTable refresh`,
FILTER: `${SECURITY_ACTIONS_PREFIX} rulesTable filter`,
LOAD_PREBUILT: `${SECURITY_ACTIONS_PREFIX} rulesTable loadPrebuilt`,
PREVIEW_ON: `${SECURITY_ACTIONS_PREFIX} rulesTable technicalPreview on`,
PREVIEW_OFF: `${SECURITY_ACTIONS_PREFIX} rulesTable technicalPreview off`,
REFRESH: `${APP_UI_ID} rulesTable refresh`,
FILTER: `${APP_UI_ID} rulesTable filter`,
LOAD_PREBUILT: `${APP_UI_ID} rulesTable loadPrebuilt`,
PREVIEW_ON: `${APP_UI_ID} rulesTable technicalPreview on`,
PREVIEW_OFF: `${APP_UI_ID} rulesTable technicalPreview off`,
};

View file

@ -5,16 +5,16 @@
* 2.0.
*/
import React from 'react';
import { waitFor } from '@testing-library/react';
import type { ReactWrapper } from 'enzyme';
import { shallow, mount } from 'enzyme';
import '../../../../common/mock/match_media';
import { PrePackagedRulesPrompt } from './load_empty_prompt';
import { getPrePackagedRulesStatus } from '../../../containers/detection_engine/rules/api';
import { useAppToastsMock } from '../../../../common/hooks/use_app_toasts.mock';
import { mount } from 'enzyme';
import React from 'react';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
import { useAppToastsMock } from '../../../../common/hooks/use_app_toasts.mock';
import { TestProviders } from '../../../../common/mock';
import '../../../../common/mock/match_media';
import { getPrePackagedRulesStatus } from '../../../containers/detection_engine/rules/api';
import { PrePackagedRulesPrompt } from './load_empty_prompt';
jest.mock('react-router-dom', () => {
const original = jest.requireActual('react-router-dom');
@ -76,7 +76,9 @@ describe('PrePackagedRulesPrompt', () => {
});
it('renders correctly', () => {
const wrapper = shallow(<PrePackagedRulesPrompt {...props} />);
const wrapper = mount(<PrePackagedRulesPrompt {...props} />, {
wrappingComponent: TestProviders,
});
expect(wrapper.find('EmptyPrompt')).toHaveLength(1);
});
@ -93,7 +95,9 @@ describe('LoadPrebuiltRulesAndTemplatesButton', () => {
timelines_not_updated: 0,
});
const wrapper: ReactWrapper = mount(<PrePackagedRulesPrompt {...props} />);
const wrapper: ReactWrapper = mount(<PrePackagedRulesPrompt {...props} />, {
wrappingComponent: TestProviders,
});
await waitFor(() => {
wrapper.update();
@ -114,7 +118,9 @@ describe('LoadPrebuiltRulesAndTemplatesButton', () => {
timelines_not_updated: 0,
});
const wrapper: ReactWrapper = mount(<PrePackagedRulesPrompt {...props} />);
const wrapper: ReactWrapper = mount(<PrePackagedRulesPrompt {...props} />, {
wrappingComponent: TestProviders,
});
await waitFor(() => {
wrapper.update();
@ -135,7 +141,9 @@ describe('LoadPrebuiltRulesAndTemplatesButton', () => {
timelines_not_updated: 0,
});
const wrapper: ReactWrapper = mount(<PrePackagedRulesPrompt {...props} />);
const wrapper: ReactWrapper = mount(<PrePackagedRulesPrompt {...props} />, {
wrappingComponent: TestProviders,
});
await waitFor(() => {
wrapper.update();
@ -157,7 +165,10 @@ describe('LoadPrebuiltRulesAndTemplatesButton', () => {
});
const wrapper: ReactWrapper = mount(
<PrePackagedRulesPrompt {...{ ...props, loading: true }} />
<PrePackagedRulesPrompt {...{ ...props, loading: true }} />,
{
wrappingComponent: TestProviders,
}
);
await waitFor(() => {
wrapper.update();

View file

@ -11,6 +11,8 @@ import { fetchInstalledIntegrations } from '../../../containers/detection_engine
// import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
// import * as i18n from './translations';
const ONE_MINUTE = 60000;
export interface UseInstalledIntegrationsArgs {
packages?: string[];
}
@ -34,6 +36,7 @@ export const useInstalledIntegrations = ({ packages }: UseInstalledIntegrationsA
},
{
keepPreviousData: true,
staleTime: ONE_MINUTE * 5,
onError: (e) => {
// Suppressing for now to prevent excessive errors when fleet isn't configured
// addError(e, { title: i18n.INTEGRATIONS_FETCH_FAILURE });

View file

@ -434,14 +434,13 @@ describe('Detections Rules API', () => {
});
test('check parameter url when creating pre-packaged rules', async () => {
await createPrepackagedRules({ signal: abortCtrl.signal });
await createPrepackagedRules();
expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/prepackaged', {
signal: abortCtrl.signal,
method: 'PUT',
});
});
test('happy path', async () => {
const resp = await createPrepackagedRules({ signal: abortCtrl.signal });
const resp = await createPrepackagedRules();
expect(resp).toEqual({
rules_installed: 0,
rules_updated: 0,

View file

@ -42,7 +42,6 @@ import type {
FetchRulesResponse,
Rule,
FetchRuleProps,
BasicFetchProps,
ImportDataProps,
ExportDocumentsProps,
ImportDataResponse,
@ -237,9 +236,7 @@ export const performBulkAction = async <Action extends BulkAction>({
*
* @throws An error if response is not OK
*/
export const createPrepackagedRules = async ({
signal,
}: BasicFetchProps): Promise<{
export const createPrepackagedRules = async (): Promise<{
rules_installed: number;
rules_updated: number;
timelines_installed: number;
@ -252,7 +249,6 @@ export const createPrepackagedRules = async ({
timelines_updated: number;
}>(DETECTION_ENGINE_PREPACKAGED_URL, {
method: 'PUT',
signal,
});
return result;
@ -401,7 +397,7 @@ export const fetchTags = async ({ signal }: { signal: AbortSignal }): Promise<st
export const getPrePackagedRulesStatus = async ({
signal,
}: {
signal: AbortSignal;
signal: AbortSignal | undefined;
}): Promise<PrePackagedRulesStatusResponse> =>
KibanaServices.get().http.fetch<PrePackagedRulesStatusResponse>(
DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL,

View file

@ -15,6 +15,7 @@ import { createRule } from './api';
import * as i18n from './translations';
import { transformOutput } from './transforms';
import { useInvalidateRules } from './use_find_rules_query';
import { useInvalidatePrePackagedRulesStatus } from './use_pre_packaged_rules_status';
interface CreateRuleReturn {
isLoading: boolean;
@ -29,6 +30,7 @@ export const useCreateRule = (): ReturnCreateRule => {
const [isLoading, setIsLoading] = useState(false);
const { addError } = useAppToasts();
const invalidateRules = useInvalidateRules();
const invalidatePrePackagedRulesStatus = useInvalidatePrePackagedRulesStatus();
useEffect(() => {
let isSubscribed = true;
@ -43,6 +45,7 @@ export const useCreateRule = (): ReturnCreateRule => {
signal: abortCtrl.signal,
});
invalidateRules();
invalidatePrePackagedRulesStatus();
if (isSubscribed) {
setRuleId(createRuleResponse.id);
}
@ -62,7 +65,7 @@ export const useCreateRule = (): ReturnCreateRule => {
isSubscribed = false;
abortCtrl.abort();
};
}, [rule, addError, invalidateRules]);
}, [rule, addError, invalidateRules, invalidatePrePackagedRulesStatus]);
return [{ isLoading, ruleId }, setRule];
};

View file

@ -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 { useMutation } from 'react-query';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
import { createPrepackagedRules } from './api';
import * as i18n from './translations';
import { useInvalidateRules } from './use_find_rules_query';
import { useInvalidatePrePackagedRulesStatus } from './use_pre_packaged_rules_status';
export const useInstallPrePackagedRules = () => {
const { addError, addSuccess } = useAppToasts();
const invalidateRules = useInvalidateRules();
const invalidatePrePackagedRulesStatus = useInvalidatePrePackagedRulesStatus();
return useMutation(() => createPrepackagedRules(), {
onError: (err) => {
addError(err, { title: i18n.RULE_AND_TIMELINE_PREPACKAGED_FAILURE });
},
onSuccess: (result) => {
addSuccess(getSuccessToastMessage(result));
// Always invalidate all rules and the prepackaged rules status cache as
// the number of rules might change after the installation
invalidatePrePackagedRulesStatus();
invalidateRules();
},
});
};
const getSuccessToastMessage = (result: {
rules_installed: number;
rules_updated: number;
timelines_installed: number;
timelines_updated: number;
}) => {
const {
rules_installed: rulesInstalled,
rules_updated: rulesUpdated,
timelines_installed: timelinesInstalled,
timelines_updated: timelinesUpdated,
} = result;
if (rulesInstalled === 0 && (timelinesInstalled > 0 || timelinesUpdated > 0)) {
return i18n.TIMELINE_PREPACKAGED_SUCCESS;
} else if ((rulesInstalled > 0 || rulesUpdated > 0) && timelinesInstalled === 0) {
return i18n.RULE_PREPACKAGED_SUCCESS;
} else {
return i18n.RULE_AND_TIMELINE_PREPACKAGED_SUCCESS;
}
};

View file

@ -5,13 +5,15 @@
* 2.0.
*/
import { act, renderHook } from '@testing-library/react-hooks';
import { shallow } from 'enzyme';
import type { ReactElement } from 'react';
import { renderHook, act } from '@testing-library/react-hooks';
import { useToasts } from '../../../../common/lib/kibana';
import { TestProviders } from '../../../../common/mock';
import * as api from './api';
import * as i18n from './translations';
import type { ReturnPrePackagedRulesAndTimelines } from './use_pre_packaged_rules';
import { usePrePackagedRules } from './use_pre_packaged_rules';
import * as api from './api';
import { shallow } from 'enzyme';
import * as i18n from './translations';
jest.mock('../../../../common/lib/kibana', () => ({
useKibana: jest.fn(),
@ -30,10 +32,10 @@ jest.mock('./api', () => ({
describe('usePrePackagedRules', () => {
beforeEach(() => {
jest.restoreAllMocks();
jest.clearAllMocks();
});
test('init', async () => {
test('initial state', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<unknown, ReturnPrePackagedRulesAndTimelines>(
() =>
@ -43,27 +45,18 @@ describe('usePrePackagedRules', () => {
isAuthenticated: null,
hasEncryptionKey: null,
isSignalIndexExists: null,
})
}),
{ wrapper: TestProviders }
);
await waitForNextUpdate();
expect(result.current).toEqual({
getLoadPrebuiltRulesAndTemplatesButton:
result.current.getLoadPrebuiltRulesAndTemplatesButton,
getReloadPrebuiltRulesAndTemplatesButton:
result.current.getReloadPrebuiltRulesAndTemplatesButton,
createPrePackagedRules: null,
getLoadPrebuiltRulesAndTemplatesButton: expect.any(Function),
getReloadPrebuiltRulesAndTemplatesButton: expect.any(Function),
createPrePackagedRules: expect.any(Function),
loading: true,
loadingCreatePrePackagedRules: false,
refetchPrePackagedRulesStatus: null,
rulesCustomInstalled: null,
rulesInstalled: null,
rulesNotInstalled: null,
rulesNotUpdated: null,
timelinesInstalled: null,
timelinesNotInstalled: null,
timelinesNotUpdated: null,
});
});
});
@ -93,7 +86,8 @@ describe('usePrePackagedRules', () => {
isAuthenticated: null,
hasEncryptionKey: null,
isSignalIndexExists: null,
})
}),
{ wrapper: TestProviders }
);
await waitForNextUpdate();
await waitForNextUpdate();
@ -106,7 +100,6 @@ describe('usePrePackagedRules', () => {
createPrePackagedRules: result.current.createPrePackagedRules,
loading: false,
loadingCreatePrePackagedRules: false,
refetchPrePackagedRulesStatus: result.current.refetchPrePackagedRulesStatus,
rulesCustomInstalled: 33,
rulesInstalled: 12,
rulesNotInstalled: 0,
@ -119,6 +112,22 @@ describe('usePrePackagedRules', () => {
});
test('happy path to createPrePackagedRules', async () => {
(api.getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({
rules_custom_installed: 33,
rules_installed: 12,
rules_not_installed: 0,
rules_not_updated: 0,
timelines_installed: 0,
timelines_not_installed: 0,
timelines_not_updated: 0,
});
(api.createPrepackagedRules as jest.Mock).mockResolvedValue({
rules_installed: 0,
rules_updated: 0,
timelines_installed: 0,
timelines_updated: 0,
});
await act(async () => {
const { result, waitForNextUpdate } = renderHook<unknown, ReturnPrePackagedRulesAndTimelines>(
() =>
@ -128,15 +137,12 @@ describe('usePrePackagedRules', () => {
isAuthenticated: true,
hasEncryptionKey: true,
isSignalIndexExists: true,
})
}),
{ wrapper: TestProviders }
);
await waitForNextUpdate();
result.current.createPrePackagedRules();
await waitForNextUpdate();
let resp = null;
if (result.current.createPrePackagedRules) {
resp = await result.current.createPrePackagedRules();
}
expect(resp).toEqual(true);
expect(api.createPrepackagedRules).toHaveBeenCalled();
expect(result.current).toEqual({
getLoadPrebuiltRulesAndTemplatesButton:
@ -146,7 +152,6 @@ describe('usePrePackagedRules', () => {
createPrePackagedRules: result.current.createPrePackagedRules,
loading: false,
loadingCreatePrePackagedRules: false,
refetchPrePackagedRulesStatus: result.current.refetchPrePackagedRulesStatus,
rulesCustomInstalled: 33,
rulesInstalled: 12,
rulesNotInstalled: 0,
@ -183,7 +188,8 @@ describe('usePrePackagedRules', () => {
isAuthenticated: true,
hasEncryptionKey: true,
isSignalIndexExists: true,
})
}),
{ wrapper: TestProviders }
);
await waitForNextUpdate();
await waitForNextUpdate();
@ -223,7 +229,8 @@ describe('usePrePackagedRules', () => {
isAuthenticated: true,
hasEncryptionKey: true,
isSignalIndexExists: true,
})
}),
{ wrapper: TestProviders }
);
await waitForNextUpdate();
await waitForNextUpdate();
@ -265,7 +272,8 @@ describe('usePrePackagedRules', () => {
isAuthenticated: true,
hasEncryptionKey: true,
isSignalIndexExists: true,
})
}),
{ wrapper: TestProviders }
);
await waitForNextUpdate();
await waitForNextUpdate();
@ -307,7 +315,8 @@ describe('usePrePackagedRules', () => {
isAuthenticated: true,
hasEncryptionKey: true,
isSignalIndexExists: true,
})
}),
{ wrapper: TestProviders }
);
await waitForNextUpdate();
await waitForNextUpdate();
@ -348,7 +357,8 @@ describe('usePrePackagedRules', () => {
isAuthenticated: true,
hasEncryptionKey: true,
isSignalIndexExists: true,
})
}),
{ wrapper: TestProviders }
);
await waitForNextUpdate();
await waitForNextUpdate();
@ -389,7 +399,8 @@ describe('usePrePackagedRules', () => {
isAuthenticated: true,
hasEncryptionKey: true,
isSignalIndexExists: true,
})
}),
{ wrapper: TestProviders }
);
await waitForNextUpdate();
await waitForNextUpdate();
@ -406,10 +417,7 @@ describe('usePrePackagedRules', () => {
});
test('unhappy path to createPrePackagedRules', async () => {
const spyOnCreatePrepackagedRules = jest.spyOn(api, 'createPrepackagedRules');
spyOnCreatePrepackagedRules.mockImplementation(() => {
throw new Error('Something went wrong');
});
(api.createPrepackagedRules as jest.Mock).mockRejectedValue(new Error('Something went wrong'));
await act(async () => {
const { result, waitForNextUpdate } = renderHook<unknown, ReturnPrePackagedRulesAndTimelines>(
() =>
@ -419,16 +427,14 @@ describe('usePrePackagedRules', () => {
isAuthenticated: true,
hasEncryptionKey: true,
isSignalIndexExists: true,
})
}),
{ wrapper: TestProviders }
);
await waitForNextUpdate();
result.current.createPrePackagedRules();
await waitForNextUpdate();
let resp = null;
if (result.current.createPrePackagedRules) {
resp = await result.current.createPrePackagedRules();
}
expect(resp).toEqual(false);
expect(spyOnCreatePrepackagedRules).toHaveBeenCalled();
expect(api.createPrepackagedRules).toHaveBeenCalled();
expect(useToasts().addError).toHaveBeenCalled();
});
});
@ -442,15 +448,13 @@ describe('usePrePackagedRules', () => {
isAuthenticated: true,
hasEncryptionKey: true,
isSignalIndexExists: true,
})
}),
{ wrapper: TestProviders }
);
await waitForNextUpdate();
result.current.createPrePackagedRules();
await waitForNextUpdate();
let resp = null;
if (result.current.createPrePackagedRules) {
resp = await result.current.createPrePackagedRules();
}
expect(resp).toEqual(false);
expect(api.createPrepackagedRules).not.toHaveBeenCalled();
});
});
@ -464,15 +468,13 @@ describe('usePrePackagedRules', () => {
isAuthenticated: true,
hasEncryptionKey: true,
isSignalIndexExists: true,
})
}),
{ wrapper: TestProviders }
);
await waitForNextUpdate();
result.current.createPrePackagedRules();
await waitForNextUpdate();
let resp = null;
if (result.current.createPrePackagedRules) {
resp = await result.current.createPrePackagedRules();
}
expect(resp).toEqual(false);
expect(api.createPrepackagedRules).not.toHaveBeenCalled();
});
});
@ -486,15 +488,13 @@ describe('usePrePackagedRules', () => {
isAuthenticated: false,
hasEncryptionKey: true,
isSignalIndexExists: true,
})
}),
{ wrapper: TestProviders }
);
await waitForNextUpdate();
result.current.createPrePackagedRules();
await waitForNextUpdate();
let resp = null;
if (result.current.createPrePackagedRules) {
resp = await result.current.createPrePackagedRules();
}
expect(resp).toEqual(false);
expect(api.createPrepackagedRules).not.toHaveBeenCalled();
});
});
@ -508,15 +508,13 @@ describe('usePrePackagedRules', () => {
isAuthenticated: true,
hasEncryptionKey: false,
isSignalIndexExists: true,
})
}),
{ wrapper: TestProviders }
);
await waitForNextUpdate();
result.current.createPrePackagedRules();
await waitForNextUpdate();
let resp = null;
if (result.current.createPrePackagedRules) {
resp = await result.current.createPrePackagedRules();
}
expect(resp).toEqual(false);
expect(api.createPrepackagedRules).not.toHaveBeenCalled();
});
});
@ -530,15 +528,13 @@ describe('usePrePackagedRules', () => {
isAuthenticated: true,
hasEncryptionKey: true,
isSignalIndexExists: false,
})
}),
{ wrapper: TestProviders }
);
await waitForNextUpdate();
result.current.createPrePackagedRules();
await waitForNextUpdate();
let resp = null;
if (result.current.createPrePackagedRules) {
resp = await result.current.createPrePackagedRules();
}
expect(resp).toEqual(false);
expect(api.createPrepackagedRules).not.toHaveBeenCalled();
});
});
});

View file

@ -5,26 +5,16 @@
* 2.0.
*/
import React, { useCallback, useMemo, useState, useEffect } from 'react';
import { EuiButton } from '@elastic/eui';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
import { getPrePackagedRulesStatus, createPrepackagedRules } from './api';
import * as i18n from './translations';
import React, { useCallback, useMemo } from 'react';
import {
getPrePackagedRuleStatus,
getPrePackagedTimelineStatus,
} from '../../../pages/detection_engine/rules/helpers';
type Func = () => Promise<void>;
export type CreatePreBuiltRules = () => Promise<boolean>;
interface ReturnPrePackagedTimelines {
timelinesInstalled: number | null;
timelinesNotInstalled: number | null;
timelinesNotUpdated: number | null;
}
import * as i18n from './translations';
import { useInstallPrePackagedRules } from './use_install_pre_packaged_rules';
import type { PrePackagedRulesStatusResponse } from './use_pre_packaged_rules_status';
import { usePrePackagedRulesStatus } from './use_pre_packaged_rules_status';
type GetLoadPrebuiltRulesAndTemplatesButton = (args: {
isDisabled: boolean;
@ -44,20 +34,15 @@ type GetReloadPrebuiltRulesAndTemplatesButton = ({
}) => React.ReactNode | null;
interface ReturnPrePackagedRules {
createPrePackagedRules: null | CreatePreBuiltRules;
createPrePackagedRules: () => void;
loading: boolean;
loadingCreatePrePackagedRules: boolean;
refetchPrePackagedRulesStatus: Func | null;
rulesCustomInstalled: number | null;
rulesInstalled: number | null;
rulesNotInstalled: number | null;
rulesNotUpdated: number | null;
getLoadPrebuiltRulesAndTemplatesButton: GetLoadPrebuiltRulesAndTemplatesButton;
getReloadPrebuiltRulesAndTemplatesButton: GetReloadPrebuiltRulesAndTemplatesButton;
}
export type ReturnPrePackagedRulesAndTimelines = ReturnPrePackagedRules &
ReturnPrePackagedTimelines;
Partial<PrePackagedRulesStatusResponse>;
interface UsePrePackagedRuleProps {
canUserCRUD: boolean | null;
@ -83,223 +68,61 @@ export const usePrePackagedRules = ({
hasEncryptionKey,
isSignalIndexExists,
}: UsePrePackagedRuleProps): ReturnPrePackagedRulesAndTimelines => {
const [prepackagedDataStatus, setPrepackagedDataStatus] = useState<
Pick<
ReturnPrePackagedRulesAndTimelines,
| 'createPrePackagedRules'
| 'refetchPrePackagedRulesStatus'
| 'rulesCustomInstalled'
| 'rulesInstalled'
| 'rulesNotInstalled'
| 'rulesNotUpdated'
| 'timelinesInstalled'
| 'timelinesNotInstalled'
| 'timelinesNotUpdated'
>
>({
createPrePackagedRules: null,
refetchPrePackagedRulesStatus: null,
rulesCustomInstalled: null,
rulesInstalled: null,
rulesNotInstalled: null,
rulesNotUpdated: null,
timelinesInstalled: null,
timelinesNotInstalled: null,
timelinesNotUpdated: null,
});
const { data: prePackagedRulesStatus, isFetching } = usePrePackagedRulesStatus();
const { mutate: installPrePackagedRules, isLoading: loadingCreatePrePackagedRules } =
useInstallPrePackagedRules();
const [loadingCreatePrePackagedRules, setLoadingCreatePrePackagedRules] = useState(false);
const [loading, setLoading] = useState(true);
const { addError, addSuccess } = useAppToasts();
const getSuccessToastMessage = (result: {
rules_installed: number;
rules_updated: number;
timelines_installed: number;
timelines_updated: number;
}) => {
const {
rules_installed: rulesInstalled,
rules_updated: rulesUpdated,
timelines_installed: timelinesInstalled,
timelines_updated: timelinesUpdated,
} = result;
if (rulesInstalled === 0 && (timelinesInstalled > 0 || timelinesUpdated > 0)) {
return i18n.TIMELINE_PREPACKAGED_SUCCESS;
} else if ((rulesInstalled > 0 || rulesUpdated > 0) && timelinesInstalled === 0) {
return i18n.RULE_PREPACKAGED_SUCCESS;
} else {
return i18n.RULE_AND_TIMELINE_PREPACKAGED_SUCCESS;
const createPrePackagedRules = useCallback(() => {
if (
canUserCRUD &&
hasIndexWrite &&
isAuthenticated &&
hasEncryptionKey &&
isSignalIndexExists
) {
installPrePackagedRules();
}
};
useEffect(() => {
let isSubscribed = true;
const abortCtrl = new AbortController();
const fetchPrePackagedRules = async () => {
try {
setLoading(true);
const prePackagedRuleStatusResponse = await getPrePackagedRulesStatus({
signal: abortCtrl.signal,
});
if (isSubscribed) {
setPrepackagedDataStatus({
createPrePackagedRules: createElasticRules,
refetchPrePackagedRulesStatus: fetchPrePackagedRules,
rulesCustomInstalled: prePackagedRuleStatusResponse.rules_custom_installed,
rulesInstalled: prePackagedRuleStatusResponse.rules_installed,
rulesNotInstalled: prePackagedRuleStatusResponse.rules_not_installed,
rulesNotUpdated: prePackagedRuleStatusResponse.rules_not_updated,
timelinesInstalled: prePackagedRuleStatusResponse.timelines_installed,
timelinesNotInstalled: prePackagedRuleStatusResponse.timelines_not_installed,
timelinesNotUpdated: prePackagedRuleStatusResponse.timelines_not_updated,
});
}
} catch (error) {
if (isSubscribed) {
setPrepackagedDataStatus({
createPrePackagedRules: null,
refetchPrePackagedRulesStatus: null,
rulesCustomInstalled: null,
rulesInstalled: null,
rulesNotInstalled: null,
rulesNotUpdated: null,
timelinesInstalled: null,
timelinesNotInstalled: null,
timelinesNotUpdated: null,
});
addError(error, { title: i18n.RULE_AND_TIMELINE_FETCH_FAILURE });
}
}
if (isSubscribed) {
setLoading(false);
}
};
const createElasticRules = async (): Promise<boolean> => {
return new Promise(async (resolve) => {
try {
if (
canUserCRUD &&
hasIndexWrite &&
isAuthenticated &&
hasEncryptionKey &&
isSignalIndexExists
) {
setLoadingCreatePrePackagedRules(true);
const result = await createPrepackagedRules({
signal: abortCtrl.signal,
});
if (isSubscribed) {
let iterationTryOfFetchingPrePackagedCount = 0;
let timeoutId = -1;
const stopTimeOut = () => {
if (timeoutId !== -1) {
window.clearTimeout(timeoutId);
}
};
const reFetch = () =>
window.setTimeout(async () => {
iterationTryOfFetchingPrePackagedCount =
iterationTryOfFetchingPrePackagedCount + 1;
const prePackagedRuleStatusResponse = await getPrePackagedRulesStatus({
signal: abortCtrl.signal,
});
if (
isSubscribed &&
((prePackagedRuleStatusResponse.rules_not_installed === 0 &&
prePackagedRuleStatusResponse.rules_not_updated === 0 &&
prePackagedRuleStatusResponse.timelines_not_installed === 0 &&
prePackagedRuleStatusResponse.timelines_not_updated === 0) ||
iterationTryOfFetchingPrePackagedCount > 100)
) {
setLoadingCreatePrePackagedRules(false);
setPrepackagedDataStatus({
createPrePackagedRules: createElasticRules,
refetchPrePackagedRulesStatus: fetchPrePackagedRules,
rulesCustomInstalled: prePackagedRuleStatusResponse.rules_custom_installed,
rulesInstalled: prePackagedRuleStatusResponse.rules_installed,
rulesNotInstalled: prePackagedRuleStatusResponse.rules_not_installed,
rulesNotUpdated: prePackagedRuleStatusResponse.rules_not_updated,
timelinesInstalled: prePackagedRuleStatusResponse.timelines_installed,
timelinesNotInstalled: prePackagedRuleStatusResponse.timelines_not_installed,
timelinesNotUpdated: prePackagedRuleStatusResponse.timelines_not_updated,
});
addSuccess(getSuccessToastMessage(result));
stopTimeOut();
resolve(true);
} else {
timeoutId = reFetch();
}
}, 300);
timeoutId = reFetch();
}
} else {
resolve(false);
}
} catch (error) {
if (isSubscribed) {
setLoadingCreatePrePackagedRules(false);
addError(error, {
title: i18n.RULE_AND_TIMELINE_PREPACKAGED_FAILURE,
});
resolve(false);
}
}
});
};
fetchPrePackagedRules();
return () => {
isSubscribed = false;
abortCtrl.abort();
};
}, [
canUserCRUD,
hasIndexWrite,
isAuthenticated,
hasEncryptionKey,
hasIndexWrite,
installPrePackagedRules,
isAuthenticated,
isSignalIndexExists,
addError,
addSuccess,
]);
const prePackagedRuleStatus = useMemo(
const prePackagedAssetsStatus = useMemo(
() =>
getPrePackagedRuleStatus(
prepackagedDataStatus.rulesInstalled,
prepackagedDataStatus.rulesNotInstalled,
prepackagedDataStatus.rulesNotUpdated
prePackagedRulesStatus?.rulesInstalled,
prePackagedRulesStatus?.rulesNotInstalled,
prePackagedRulesStatus?.rulesNotUpdated
),
[
prepackagedDataStatus.rulesInstalled,
prepackagedDataStatus.rulesNotInstalled,
prepackagedDataStatus.rulesNotUpdated,
prePackagedRulesStatus?.rulesInstalled,
prePackagedRulesStatus?.rulesNotInstalled,
prePackagedRulesStatus?.rulesNotUpdated,
]
);
const prePackagedTimelineStatus = useMemo(
() =>
getPrePackagedTimelineStatus(
prepackagedDataStatus.timelinesInstalled,
prepackagedDataStatus.timelinesNotInstalled,
prepackagedDataStatus.timelinesNotUpdated
prePackagedRulesStatus?.timelinesInstalled,
prePackagedRulesStatus?.timelinesNotInstalled,
prePackagedRulesStatus?.timelinesNotUpdated
),
[
prepackagedDataStatus.timelinesInstalled,
prepackagedDataStatus.timelinesNotInstalled,
prepackagedDataStatus.timelinesNotUpdated,
prePackagedRulesStatus?.timelinesInstalled,
prePackagedRulesStatus?.timelinesNotInstalled,
prePackagedRulesStatus?.timelinesNotUpdated,
]
);
const getLoadPrebuiltRulesAndTemplatesButton = useCallback(
({ isDisabled, onClick, fill, 'data-test-subj': dataTestSubj = 'loadPrebuiltRulesBtn' }) => {
return (prePackagedRuleStatus === 'ruleNotInstalled' ||
return (prePackagedAssetsStatus === 'ruleNotInstalled' ||
prePackagedTimelineStatus === 'timelinesNotInstalled') &&
prePackagedRuleStatus !== 'someRuleUninstall' ? (
prePackagedAssetsStatus !== 'someRuleUninstall' ? (
<EuiButton
fill={fill}
iconType="indexOpen"
@ -308,21 +131,21 @@ export const usePrePackagedRules = ({
onClick={onClick}
data-test-subj={dataTestSubj}
>
{prePackagedRuleStatus === 'ruleNotInstalled' &&
{prePackagedAssetsStatus === 'ruleNotInstalled' &&
prePackagedTimelineStatus === 'timelinesNotInstalled' &&
i18n.LOAD_PREPACKAGED_RULES_AND_TEMPLATES}
{prePackagedRuleStatus === 'ruleNotInstalled' &&
{prePackagedAssetsStatus === 'ruleNotInstalled' &&
prePackagedTimelineStatus !== 'timelinesNotInstalled' &&
i18n.LOAD_PREPACKAGED_RULES}
{prePackagedRuleStatus !== 'ruleNotInstalled' &&
{prePackagedAssetsStatus !== 'ruleNotInstalled' &&
prePackagedTimelineStatus === 'timelinesNotInstalled' &&
i18n.LOAD_PREPACKAGED_TIMELINE_TEMPLATES}
</EuiButton>
) : null;
},
[loadingCreatePrePackagedRules, prePackagedRuleStatus, prePackagedTimelineStatus]
[loadingCreatePrePackagedRules, prePackagedAssetsStatus, prePackagedTimelineStatus]
);
const getMissingRulesOrTimelinesButtonTitle = useCallback(
@ -339,7 +162,7 @@ export const usePrePackagedRules = ({
const getReloadPrebuiltRulesAndTemplatesButton = useCallback(
({ isDisabled, onClick, fill = false }) => {
return prePackagedRuleStatus === 'someRuleUninstall' ||
return prePackagedAssetsStatus === 'someRuleUninstall' ||
prePackagedTimelineStatus === 'someTimelineUninstall' ? (
<EuiButton
fill={fill}
@ -350,8 +173,8 @@ export const usePrePackagedRules = ({
data-test-subj="reloadPrebuiltRulesBtn"
>
{getMissingRulesOrTimelinesButtonTitle(
prepackagedDataStatus.rulesNotInstalled ?? 0,
prepackagedDataStatus.timelinesNotInstalled ?? 0
prePackagedRulesStatus?.rulesNotInstalled ?? 0,
prePackagedRulesStatus?.timelinesNotInstalled ?? 0
)}
</EuiButton>
) : null;
@ -359,18 +182,19 @@ export const usePrePackagedRules = ({
[
getMissingRulesOrTimelinesButtonTitle,
loadingCreatePrePackagedRules,
prePackagedRuleStatus,
prePackagedAssetsStatus,
prePackagedRulesStatus?.rulesNotInstalled,
prePackagedRulesStatus?.timelinesNotInstalled,
prePackagedTimelineStatus,
prepackagedDataStatus.rulesNotInstalled,
prepackagedDataStatus.timelinesNotInstalled,
]
);
return {
loading,
loading: isFetching,
loadingCreatePrePackagedRules,
...prepackagedDataStatus,
createPrePackagedRules,
getLoadPrebuiltRulesAndTemplatesButton,
getReloadPrebuiltRulesAndTemplatesButton,
...prePackagedRulesStatus,
};
};

View file

@ -0,0 +1,70 @@
/*
* 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 { useCallback } from 'react';
import { useQuery, useQueryClient } from 'react-query';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
import { getPrePackagedRulesStatus } from './api';
import * as i18n from './translations';
const ONE_MINUTE = 60000;
export interface PrePackagedRulesStatusResponse {
rulesCustomInstalled: number;
rulesInstalled: number;
rulesNotInstalled: number;
rulesNotUpdated: number;
timelinesInstalled: number;
timelinesNotInstalled: number;
timelinesNotUpdated: number;
}
export const PRE_PACKAGED_RULES_STATUS_QUERY_KEY = 'prePackagedRulesStatus';
export const usePrePackagedRulesStatus = () => {
const { addError } = useAppToasts();
return useQuery<PrePackagedRulesStatusResponse>(
[PRE_PACKAGED_RULES_STATUS_QUERY_KEY],
async ({ signal }) => {
const response = await getPrePackagedRulesStatus({ signal });
return {
rulesCustomInstalled: response.rules_custom_installed,
rulesInstalled: response.rules_installed,
rulesNotInstalled: response.rules_not_installed,
rulesNotUpdated: response.rules_not_updated,
timelinesInstalled: response.timelines_installed,
timelinesNotInstalled: response.timelines_not_installed,
timelinesNotUpdated: response.timelines_not_updated,
};
},
{
staleTime: ONE_MINUTE * 5,
onError: (err) => {
addError(err, { title: i18n.RULE_AND_TIMELINE_FETCH_FAILURE });
},
}
);
};
/**
* We should use this hook to invalidate the prepackaged rules cache. For
* example, rule mutations that affect rule set size, like creation or deletion,
* should lead to cache invalidation.
*
* @returns A rules cache invalidation callback
*/
export const useInvalidatePrePackagedRulesStatus = () => {
const queryClient = useQueryClient();
return useCallback(() => {
queryClient.invalidateQueries(PRE_PACKAGED_RULES_STATUS_QUERY_KEY, {
refetchActive: true,
refetchInactive: false,
});
}, [queryClient]);
};

View file

@ -39,6 +39,7 @@ import {
} from '../../../../../containers/detection_engine/rules/use_find_rules_query';
import { BULK_RULE_ACTIONS } from '../../../../../../common/lib/apm/user_actions';
import { useStartTransaction } from '../../../../../../common/lib/apm/use_start_transaction';
import { useInvalidatePrePackagedRulesStatus } from '../../../../../containers/detection_engine/rules/use_pre_packaged_rules_status';
interface UseBulkActionsArgs {
filterOptions: FilterOptions;
@ -62,6 +63,7 @@ export const useBulkActions = ({
const rulesTableContext = useRulesTableContext();
const invalidateRules = useInvalidateRules();
const updateRulesCache = useUpdateRulesCache();
const invalidatePrePackagedRulesStatus = useInvalidatePrePackagedRulesStatus();
const hasActionsPrivileges = useHasActionsPrivileges();
const toasts = useAppToasts();
const getIsMounted = useIsMounted();
@ -154,6 +156,10 @@ export const useBulkActions = ({
search: isAllSelected ? { query: filterQuery } : { ids: selectedRuleIds },
});
invalidateRules();
// We use prePackagedRulesStatus to display Prebuilt/Custom rules
// counters, so we need to invalidate it when the total number of rules
// changes.
invalidatePrePackagedRulesStatus();
clearRulesSelection();
};
@ -176,6 +182,10 @@ export const useBulkActions = ({
search: isAllSelected ? { query: filterQuery } : { ids: selectedRuleIds },
});
invalidateRules();
// We use prePackagedRulesStatus to display Prebuilt/Custom rules
// counters, so we need to invalidate it when the total number of rules
// changes.
invalidatePrePackagedRulesStatus();
};
const handleExportAction = async () => {
@ -433,6 +443,7 @@ export const useBulkActions = ({
toasts,
filterQuery,
invalidateRules,
invalidatePrePackagedRulesStatus,
confirmDeletion,
confirmBulkEdit,
completeBulkEditForm,

View file

@ -13,8 +13,8 @@ describe('AllRulesTable Helpers', () => {
describe('showRulesTable', () => {
test('returns false when rulesCustomInstalled and rulesInstalled are null', () => {
const result = showRulesTable({
rulesCustomInstalled: null,
rulesInstalled: null,
rulesCustomInstalled: undefined,
rulesInstalled: undefined,
});
expect(result).toBeFalsy();
});
@ -30,7 +30,7 @@ describe('AllRulesTable Helpers', () => {
test('returns false when both rulesCustomInstalled and rulesInstalled checks return false', () => {
const result = showRulesTable({
rulesCustomInstalled: 0,
rulesInstalled: null,
rulesInstalled: undefined,
});
expect(result).toBeFalsy();
});
@ -38,14 +38,14 @@ describe('AllRulesTable Helpers', () => {
test('returns true if rulesCustomInstalled is not null or 0', () => {
const result = showRulesTable({
rulesCustomInstalled: 5,
rulesInstalled: null,
rulesInstalled: undefined,
});
expect(result).toBeTruthy();
});
test('returns true if rulesInstalled is not null or 0', () => {
const result = showRulesTable({
rulesCustomInstalled: null,
rulesCustomInstalled: undefined,
rulesInstalled: 5,
});
expect(result).toBeTruthy();

View file

@ -13,8 +13,8 @@ export const showRulesTable = ({
rulesCustomInstalled,
rulesInstalled,
}: {
rulesCustomInstalled: number | null;
rulesInstalled: number | null;
rulesCustomInstalled?: number;
rulesInstalled?: number;
}) =>
(rulesCustomInstalled != null && rulesCustomInstalled > 0) ||
(rulesInstalled != null && rulesInstalled > 0);

View file

@ -7,18 +7,17 @@
import { EuiSpacer } from '@elastic/eui';
import React, { useState } from 'react';
import type { CreatePreBuiltRules } from '../../../../containers/detection_engine/rules';
import { RulesTables } from './rules_tables';
import { AllRulesTabs, RulesTableToolbar } from './rules_table_toolbar';
interface AllRulesProps {
createPrePackagedRules: CreatePreBuiltRules | null;
createPrePackagedRules: () => void;
hasPermissions: boolean;
loadingCreatePrePackagedRules: boolean;
rulesCustomInstalled: number | null;
rulesInstalled: number | null;
rulesNotInstalled: number | null;
rulesNotUpdated: number | null;
rulesCustomInstalled?: number;
rulesInstalled?: number;
rulesNotInstalled?: number;
rulesNotUpdated?: number;
}
/**

View file

@ -154,17 +154,13 @@ const RulesTableContext = createContext<RulesTableContextType | null>(null);
interface RulesTableContextProviderProps {
children: React.ReactNode;
refetchPrePackagedRulesStatus: () => Promise<void>;
}
const IN_MEMORY_STORAGE_KEY = 'detection-rules-table-in-memory';
const DEFAULT_RULES_PER_PAGE = 20;
export const RulesTableContextProvider = ({
children,
refetchPrePackagedRulesStatus,
}: RulesTableContextProviderProps) => {
export const RulesTableContextProvider = ({ children }: RulesTableContextProviderProps) => {
const [autoRefreshSettings] = useUiSetting$<{
on: boolean;
value: number;
@ -251,13 +247,6 @@ export const RulesTableContextProvider = ({
refetchInterval: isRefreshOn && !isActionInProgress && autoRefreshSettings.value,
});
useEffect(() => {
// Synchronize re-fetching of rules and pre-packaged rule statuses
if (isFetched && isRefetching) {
refetchPrePackagedRulesStatus();
}
}, [isFetched, isRefetching, refetchPrePackagedRulesStatus]);
// Paginate and sort rules
const rulesToDisplay = isInMemorySorting
? rules.sort(getRulesComparator(sortingOptions)).slice((page - 1) * perPage, page * perPage)

View file

@ -21,6 +21,7 @@ describe('getRulesTableActions', () => {
const rule = mockRule(uuid.v4());
const toasts = useAppToastsMock.create();
const invalidateRules = jest.fn();
const invalidatePrePackagedRulesStatus = jest.fn();
const setLoadingRules = jest.fn();
const startTransaction = jest.fn();
@ -35,14 +36,15 @@ describe('getRulesTableActions', () => {
Promise.resolve({ attributes: { results: { created: [ruleDuplicate] } } })
);
const duplicateRulesActionObject = getRulesTableActions(
const duplicateRulesActionObject = getRulesTableActions({
toasts,
navigateToApp,
invalidateRules,
true,
invalidatePrePackagedRulesStatus,
actionsPrivileges: true,
setLoadingRules,
startTransaction
)[1];
startTransaction,
})[1];
const duplicateRulesActionHandler = duplicateRulesActionObject.onClick;
expect(duplicateRulesActionHandler).toBeDefined();
@ -56,14 +58,15 @@ describe('getRulesTableActions', () => {
test('delete rule onClick should call refetch after the rule is deleted', async () => {
const navigateToApp = jest.fn();
const deleteRulesActionObject = getRulesTableActions(
const deleteRulesActionObject = getRulesTableActions({
toasts,
navigateToApp,
invalidateRules,
true,
invalidatePrePackagedRulesStatus,
actionsPrivileges: true,
setLoadingRules,
startTransaction
)[3];
startTransaction,
})[3];
const deleteRuleActionHandler = deleteRulesActionObject.onClick;
expect(deleteRuleActionHandler).toBeDefined();

View file

@ -27,14 +27,23 @@ type NavigateToApp = (appId: string, options?: NavigateToAppOptions | undefined)
export type TableColumn = EuiBasicTableColumn<Rule> | EuiTableActionsColumnType<Rule>;
export const getRulesTableActions = (
toasts: UseAppToasts,
navigateToApp: NavigateToApp,
invalidateRules: () => void,
actionsPrivileges: boolean,
setLoadingRules: RulesTableActions['setLoadingRules'],
startTransaction: ReturnType<typeof useStartTransaction>['startTransaction']
): Array<DefaultItemAction<Rule>> => [
export const getRulesTableActions = ({
toasts,
navigateToApp,
invalidateRules,
invalidatePrePackagedRulesStatus,
actionsPrivileges,
setLoadingRules,
startTransaction,
}: {
toasts: UseAppToasts;
navigateToApp: NavigateToApp;
invalidateRules: () => void;
invalidatePrePackagedRulesStatus: () => void;
actionsPrivileges: boolean;
setLoadingRules: RulesTableActions['setLoadingRules'];
startTransaction: ReturnType<typeof useStartTransaction>['startTransaction'];
}): Array<DefaultItemAction<Rule>> => [
{
type: 'icon',
'data-test-subj': 'editRuleAction',
@ -73,6 +82,7 @@ export const getRulesTableActions = (
search: { ids: [rule.id] },
});
invalidateRules();
invalidatePrePackagedRulesStatus();
const createdRules = result?.attributes.results.created;
if (createdRules?.length) {
goToRuleEditPage(createdRules[0].id, navigateToApp);
@ -113,6 +123,7 @@ export const getRulesTableActions = (
search: { ids: [rule.id] },
});
invalidateRules();
invalidatePrePackagedRulesStatus();
},
},
];

View file

@ -15,10 +15,7 @@ jest.mock('../rules_table/rules_table_context');
describe('RulesTableFilters', () => {
it('renders no numbers next to rule type button filter if none exist', async () => {
const wrapper = mount(
<RulesTableFilters rulesCustomInstalled={null} rulesInstalled={null} allTags={[]} />,
{ wrappingComponent: TestProviders }
);
const wrapper = mount(<RulesTableFilters allTags={[]} />, { wrappingComponent: TestProviders });
expect(wrapper.find('[data-test-subj="showElasticRulesFilterButton"]').at(0).text()).toEqual(
'Elastic rules'

View file

@ -35,8 +35,8 @@ const SearchBarWrapper = styled(EuiFlexItem)`
`;
interface RulesTableFiltersProps {
rulesCustomInstalled: number | null;
rulesInstalled: number | null;
rulesCustomInstalled?: number;
rulesInstalled?: number;
allTags: string[];
}

View file

@ -14,48 +14,43 @@ import {
EuiLoadingContent,
EuiProgress,
} from '@elastic/eui';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { partition } from 'lodash/fp';
import { AllRulesTabs } from './rules_table_toolbar';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { RULES_TABLE_PAGE_SIZE_OPTIONS } from '../../../../../../common/constants';
import { Loader } from '../../../../../common/components/loader';
import { useBoolState } from '../../../../../common/hooks/use_bool_state';
import { useValueChanged } from '../../../../../common/hooks/use_value_changed';
import { RULES_TABLE_ACTIONS } from '../../../../../common/lib/apm/user_actions';
import { useStartTransaction } from '../../../../../common/lib/apm/use_start_transaction';
import { PrePackagedRulesPrompt } from '../../../../components/rules/pre_packaged_rules/load_empty_prompt';
import type {
CreatePreBuiltRules,
Rule,
RulesSortingFields,
} from '../../../../containers/detection_engine/rules';
import { useRulesTableContext } from './rules_table/rules_table_context';
import { useAsyncConfirmation } from './rules_table/use_async_confirmation';
import type { Rule, RulesSortingFields } from '../../../../containers/detection_engine/rules';
import { useTags } from '../../../../containers/detection_engine/rules/use_tags';
import { getPrePackagedRuleStatus } from '../helpers';
import * as i18n from '../translations';
import type { EuiBasicTableOnChange } from '../types';
import { useMonitoringColumns, useRulesColumns } from './use_columns';
import { showRulesTable } from './helpers';
import { RulesTableFilters } from './rules_table_filters/rules_table_filters';
import { AllRulesUtilityBar } from './utility_bar';
import { RULES_TABLE_PAGE_SIZE_OPTIONS } from '../../../../../../common/constants';
import { useTags } from '../../../../containers/detection_engine/rules/use_tags';
import { useCustomRulesCount } from './bulk_actions/use_custom_rules_count';
import { useBulkEditFormFlyout } from './bulk_actions/use_bulk_edit_form_flyout';
import { BulkEditConfirmation } from './bulk_actions/bulk_edit_confirmation';
import { BulkEditFlyout } from './bulk_actions/bulk_edit_flyout';
import { useBulkActions } from './bulk_actions/use_bulk_actions';
import { useStartTransaction } from '../../../../../common/lib/apm/use_start_transaction';
import { RULES_TABLE_ACTIONS } from '../../../../../common/lib/apm/user_actions';
import { useBulkEditFormFlyout } from './bulk_actions/use_bulk_edit_form_flyout';
import { useCustomRulesCount } from './bulk_actions/use_custom_rules_count';
import { showRulesTable } from './helpers';
import { useRulesTableContext } from './rules_table/rules_table_context';
import { useAsyncConfirmation } from './rules_table/use_async_confirmation';
import { RulesTableFilters } from './rules_table_filters/rules_table_filters';
import { AllRulesTabs } from './rules_table_toolbar';
import { useMonitoringColumns, useRulesColumns } from './use_columns';
import { AllRulesUtilityBar } from './utility_bar';
const INITIAL_SORT_FIELD = 'enabled';
interface RulesTableProps {
createPrePackagedRules: CreatePreBuiltRules | null;
createPrePackagedRules: () => void;
hasPermissions: boolean;
loadingCreatePrePackagedRules: boolean;
rulesCustomInstalled: number | null;
rulesInstalled: number | null;
rulesNotInstalled: number | null;
rulesNotUpdated: number | null;
rulesCustomInstalled?: number;
rulesInstalled?: number;
rulesNotInstalled?: number;
rulesNotUpdated?: number;
selectedTab: AllRulesTabs;
}

View file

@ -42,6 +42,8 @@ import type {
} from '../../../../../../common/detection_engine/schemas/common';
import { useAppToasts } from '../../../../../common/hooks/use_app_toasts';
import { useStartTransaction } from '../../../../../common/lib/apm/use_start_transaction';
import { useInvalidateRules } from '../../../../containers/detection_engine/rules/use_find_rules_query';
import { useInvalidatePrePackagedRulesStatus } from '../../../../containers/detection_engine/rules/use_pre_packaged_rules_status';
export type TableColumn = EuiBasicTableColumn<Rule> | EuiTableActionsColumnType<Rule>;
@ -172,22 +174,33 @@ const useActionsColumn = (): EuiTableActionsColumnType<Rule> => {
const { navigateToApp } = useKibana().services.application;
const hasActionsPrivileges = useHasActionsPrivileges();
const toasts = useAppToasts();
const { reFetchRules, setLoadingRules } = useRulesTableContext().actions;
const { setLoadingRules } = useRulesTableContext().actions;
const { startTransaction } = useStartTransaction();
const invalidateRules = useInvalidateRules();
const invalidatePrePackagedRulesStatus = useInvalidatePrePackagedRulesStatus();
return useMemo(
() => ({
actions: getRulesTableActions(
actions: getRulesTableActions({
toasts,
navigateToApp,
reFetchRules,
hasActionsPrivileges,
invalidateRules,
invalidatePrePackagedRulesStatus,
actionsPrivileges: hasActionsPrivileges,
setLoadingRules,
startTransaction
),
startTransaction,
}),
width: '40px',
}),
[hasActionsPrivileges, navigateToApp, reFetchRules, setLoadingRules, startTransaction, toasts]
[
hasActionsPrivileges,
invalidatePrePackagedRulesStatus,
invalidateRules,
navigateToApp,
setLoadingRules,
startTransaction,
toasts,
]
);
};

View file

@ -1,49 +0,0 @@
/*
* 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 { shallow } from 'enzyme';
import '../../../../../common/mock/match_media';
import { TestProviders } from '../../../../../common/mock';
import { CreateRulePage } from '.';
import { useUserData } from '../../../../components/user_info';
import { useAppToastsMock } from '../../../../../common/hooks/use_app_toasts.mock';
import { useAppToasts } from '../../../../../common/hooks/use_app_toasts';
jest.mock('react-router-dom', () => {
const original = jest.requireActual('react-router-dom');
return {
...original,
useHistory: () => ({
useHistory: jest.fn(),
}),
};
});
jest.mock('../../../../../common/lib/kibana');
jest.mock('../../../../containers/detection_engine/lists/use_lists_config');
jest.mock('../../../../containers/detection_engine/rules/use_find_rules_query');
jest.mock('../../../../../common/components/link_to');
jest.mock('../../../../components/user_info');
jest.mock('../../../../../common/hooks/use_app_toasts');
describe('CreateRulePage', () => {
let appToastsMock: jest.Mocked<ReturnType<typeof useAppToastsMock.create>>;
beforeEach(() => {
appToastsMock = useAppToastsMock.create();
(useAppToasts as jest.Mock).mockReturnValue(appToastsMock);
});
it('renders correctly', () => {
(useUserData as jest.Mock).mockReturnValue([{}]);
const wrapper = shallow(<CreateRulePage />, { wrappingComponent: TestProviders });
expect(wrapper.find('[title="Create new rule"]')).toHaveLength(1);
});
});

View file

@ -507,9 +507,9 @@ describe('rule helpers', () => {
});
test('unknown', () => {
const rulesInstalled = null;
const rulesNotInstalled = null;
const rulesNotUpdated = null;
const rulesInstalled = undefined;
const rulesNotInstalled = undefined;
const rulesNotUpdated = undefined;
const result: string = getPrePackagedRuleStatus(
rulesInstalled,
rulesNotInstalled,
@ -574,9 +574,9 @@ describe('rule helpers', () => {
});
test('unknown', () => {
const timelinesInstalled = null;
const timelinesNotInstalled = null;
const timelinesNotUpdated = null;
const timelinesInstalled = undefined;
const timelinesNotInstalled = undefined;
const timelinesNotUpdated = undefined;
const result: string = getPrePackagedTimelineStatus(
timelinesInstalled,
timelinesNotInstalled,

View file

@ -255,9 +255,9 @@ export type PrePackagedTimelineStatus =
| 'unknown';
export const getPrePackagedRuleStatus = (
rulesInstalled: number | null,
rulesNotInstalled: number | null,
rulesNotUpdated: number | null
rulesInstalled?: number,
rulesNotInstalled?: number,
rulesNotUpdated?: number
): PrePackagedRuleStatus => {
if (
rulesNotInstalled != null &&
@ -294,9 +294,9 @@ export const getPrePackagedRuleStatus = (
return 'unknown';
};
export const getPrePackagedTimelineStatus = (
timelinesInstalled: number | null,
timelinesNotInstalled: number | null,
timelinesNotUpdated: number | null
timelinesInstalled?: number,
timelinesNotInstalled?: number,
timelinesNotUpdated?: number
): PrePackagedTimelineStatus => {
if (
timelinesNotInstalled != null &&

View file

@ -1,214 +0,0 @@
/*
* 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 type { ReactWrapper } from 'enzyme';
import { shallow, mount } from 'enzyme';
import '../../../../common/mock/match_media';
import { RulesPage } from '.';
import { useUserData } from '../../../components/user_info';
import { waitFor } from '@testing-library/react';
import { TestProviders } from '../../../../common/mock';
import { getPrePackagedRulesStatus } from '../../../containers/detection_engine/rules/api';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
import { useAppToastsMock } from '../../../../common/hooks/use_app_toasts.mock';
jest.mock('react-router-dom', () => {
const original = jest.requireActual('react-router-dom');
return {
...original,
useHistory: () => ({
useHistory: jest.fn(),
}),
};
});
jest.mock('./all/rules_table/rules_table_context');
jest.mock('../../../containers/detection_engine/lists/use_lists_config');
jest.mock('../../../containers/detection_engine/rules/use_find_rules_query');
jest.mock('../../../../common/components/link_to');
jest.mock('../../../components/user_info');
jest.mock('../../../../common/lib/kibana', () => {
const actual = jest.requireActual('../../../../common/lib/kibana');
return {
...actual,
useKibana: () => ({
services: {
...actual.useKibana().services,
application: {
navigateToApp: jest.fn(),
},
},
}),
useNavigation: () => ({
navigateTo: jest.fn(),
}),
};
});
jest.mock('../../../../common/components/toasters', () => {
const actual = jest.requireActual('../../../../common/components/toasters');
return {
...actual,
errorToToaster: jest.fn(),
useStateToaster: jest.fn().mockReturnValue([jest.fn(), jest.fn()]),
displaySuccessToast: jest.fn(),
};
});
jest.mock('../../../containers/detection_engine/rules/api', () => ({
getPrePackagedRulesStatus: jest.fn().mockResolvedValue({
rules_not_installed: 0,
rules_installed: 0,
rules_not_updated: 0,
timelines_not_installed: 0,
timelines_installed: 0,
timelines_not_updated: 0,
}),
createPrepackagedRules: jest.fn(),
}));
jest.mock('../../../components/value_lists_management_flyout', () => {
return {
ValueListsFlyout: jest.fn().mockReturnValue(<div />),
};
});
jest.mock('./all', () => {
return {
AllRules: jest.fn().mockReturnValue(<div />),
};
});
jest.mock('../../../../common/utils/route/spy_routes', () => {
return {
SpyRoute: jest.fn().mockReturnValue(<div />),
};
});
jest.mock('../../../components/rules/pre_packaged_rules/update_callout', () => {
return {
UpdatePrePackagedRulesCallOut: jest.fn().mockReturnValue(<div />),
};
});
jest.mock('../../../../common/hooks/use_app_toasts');
describe('RulesPage', () => {
let appToastsMock: jest.Mocked<ReturnType<typeof useAppToastsMock.create>>;
beforeAll(() => {
(useUserData as jest.Mock).mockReturnValue([{}]);
appToastsMock = useAppToastsMock.create();
(useAppToasts as jest.Mock).mockReturnValue(appToastsMock);
});
it('renders AllRules', () => {
const wrapper = shallow(<RulesPage />);
expect(wrapper.find('[data-test-subj="all-rules"]').exists()).toEqual(true);
});
it('renders correct button with correct text - Load Elastic prebuilt rules and timeline templates', async () => {
(getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({
rules_not_installed: 3,
rules_installed: 0,
rules_not_updated: 0,
timelines_not_installed: 3,
timelines_installed: 0,
timelines_not_updated: 0,
});
const wrapper: ReactWrapper = mount(
<TestProviders>
<RulesPage />
</TestProviders>
);
await waitFor(() => {
wrapper.update();
expect(wrapper.find('[data-test-subj="loadPrebuiltRulesBtn"]').exists()).toEqual(true);
expect(wrapper.find('[data-test-subj="loadPrebuiltRulesBtn"]').last().text()).toEqual(
'Load Elastic prebuilt rules and timeline templates'
);
});
});
it('renders correct button with correct text - Load Elastic prebuilt rules', async () => {
(getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({
rules_not_installed: 3,
rules_installed: 0,
rules_not_updated: 0,
timelines_not_installed: 0,
timelines_installed: 0,
timelines_not_updated: 0,
});
const wrapper: ReactWrapper = mount(
<TestProviders>
<RulesPage />
</TestProviders>
);
await waitFor(() => {
wrapper.update();
expect(wrapper.find('[data-test-subj="loadPrebuiltRulesBtn"]').exists()).toEqual(true);
expect(wrapper.find('[data-test-subj="loadPrebuiltRulesBtn"]').last().text()).toEqual(
'Load Elastic prebuilt rules'
);
});
});
it('renders correct button with correct text - Load Elastic prebuilt timeline templates', async () => {
(getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({
rules_not_installed: 0,
rules_installed: 0,
rules_not_updated: 0,
timelines_not_installed: 3,
timelines_installed: 0,
timelines_not_updated: 0,
});
const wrapper: ReactWrapper = mount(
<TestProviders>
<RulesPage />
</TestProviders>
);
await waitFor(() => {
wrapper.update();
expect(wrapper.find('[data-test-subj="loadPrebuiltRulesBtn"]').exists()).toEqual(true);
expect(wrapper.find('[data-test-subj="loadPrebuiltRulesBtn"]').last().text()).toEqual(
'Load Elastic prebuilt timeline templates'
);
});
});
it('renders a callout - Update Elastic prebuilt rules', async () => {
(getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({
rules_not_installed: 2,
rules_installed: 1,
rules_not_updated: 1,
timelines_not_installed: 0,
timelines_installed: 0,
timelines_not_updated: 0,
});
const wrapper: ReactWrapper = mount(
<TestProviders>
<RulesPage />
</TestProviders>
);
await waitFor(() => {
wrapper.update();
expect(wrapper.find('[data-test-subj="update-callout-button"]').exists()).toEqual(true);
});
});
});

View file

@ -73,7 +73,6 @@ const RulesPageComponent: React.FC = () => {
const {
createPrePackagedRules,
loadingCreatePrePackagedRules,
refetchPrePackagedRulesStatus,
rulesCustomInstalled,
rulesInstalled,
rulesNotInstalled,
@ -106,9 +105,8 @@ const RulesPageComponent: React.FC = () => {
if (createPrePackagedRules != null) {
startTransaction({ name: RULES_TABLE_ACTIONS.LOAD_PREBUILT });
await createPrePackagedRules();
invalidateRules();
}
}, [createPrePackagedRules, invalidateRules, startTransaction]);
}, [createPrePackagedRules, startTransaction]);
// Wrapper to add confirmation modal for users who may be running older ML Jobs that would
// be overridden by updating their rules. For details, see: https://github.com/elastic/kibana/issues/128121
@ -125,14 +123,6 @@ const RulesPageComponent: React.FC = () => {
}
}, [handleCreatePrePackagedRules, legacyJobsInstalled.length]);
const handleRefetchPrePackagedRulesStatus = useCallback(() => {
if (refetchPrePackagedRulesStatus != null) {
return refetchPrePackagedRulesStatus();
} else {
return Promise.resolve();
}
}, [refetchPrePackagedRulesStatus]);
const loadPrebuiltRulesAndTemplatesButton = useMemo(
() =>
getLoadPrebuiltRulesAndTemplatesButton({
@ -208,9 +198,7 @@ const RulesPageComponent: React.FC = () => {
showCheckBox
/>
<RulesTableContextProvider
refetchPrePackagedRulesStatus={handleRefetchPrePackagedRulesStatus}
>
<RulesTableContextProvider>
<SecuritySolutionPageWrapper>
<HeaderPage title={i18n.PAGE_TITLE}>
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false} wrap={true}>

View file

@ -31,6 +31,7 @@ import { ruleAssetSavedObjectsClientFactory } from '../../rules/rule_asset/rule_
import { buildSiemResponse } from '../utils';
import { installPrepackagedTimelines } from '../../../timeline/routes/prepackaged_timelines/install_prepackaged_timelines';
import { rulesToMap } from '../../rules/utils';
export const addPrepackedRulesRoute = (router: SecuritySolutionPluginRouter) => {
router.put(
@ -106,18 +107,16 @@ export const createPrepackagedRules = async (
await exceptionsListClient.createEndpointList();
}
const latestPrepackagedRules = await getLatestPrepackagedRules(
const latestPrepackagedRulesMap = await getLatestPrepackagedRules(
ruleAssetsClient,
prebuiltRulesFromFileSystem,
prebuiltRulesFromSavedObjects
);
const prepackagedRules = await getExistingPrepackagedRules({
rulesClient,
});
const rulesToInstall = getRulesToInstall(latestPrepackagedRules, prepackagedRules);
const rulesToUpdate = getRulesToUpdate(latestPrepackagedRules, prepackagedRules);
const installedPrePackagedRules = rulesToMap(await getExistingPrepackagedRules({ rulesClient }));
const rulesToInstall = getRulesToInstall(latestPrepackagedRulesMap, installedPrePackagedRules);
const rulesToUpdate = getRulesToUpdate(latestPrepackagedRulesMap, installedPrePackagedRules);
await Promise.all(installPrepackagedRules(rulesClient, rulesToInstall));
await installPrepackagedRules(rulesClient, rulesToInstall);
const timeline = await installPrepackagedTimelines(
maxTimelineImportExportSize,
frameworkRequest,

View file

@ -26,6 +26,7 @@ import {
checkTimelinesStatus,
checkTimelineStatusRt,
} from '../../../timeline/utils/check_timelines_status';
import { rulesToMap } from '../../rules/utils';
export const getPrepackagedRulesStatusRoute = (
router: SecuritySolutionPluginRouter,
@ -63,27 +64,27 @@ export const getPrepackagedRulesStatusRoute = (
fields: undefined,
});
const frameworkRequest = await buildFrameworkRequest(context, security, request);
const prepackagedRules = await getExistingPrepackagedRules({
rulesClient,
});
const installedPrePackagedRules = rulesToMap(
await getExistingPrepackagedRules({ rulesClient })
);
const rulesToInstall = getRulesToInstall(latestPrepackagedRules, prepackagedRules);
const rulesToUpdate = getRulesToUpdate(latestPrepackagedRules, prepackagedRules);
const rulesToInstall = getRulesToInstall(latestPrepackagedRules, installedPrePackagedRules);
const rulesToUpdate = getRulesToUpdate(latestPrepackagedRules, installedPrePackagedRules);
const prepackagedTimelineStatus = await checkTimelinesStatus(frameworkRequest);
const [validatedprepackagedTimelineStatus] = validate(
const [validatedPrepackagedTimelineStatus] = validate(
prepackagedTimelineStatus,
checkTimelineStatusRt
);
const prepackagedRulesStatus: PrePackagedRulesAndTimelinesStatusSchema = {
rules_custom_installed: customRules.total,
rules_installed: prepackagedRules.length,
rules_installed: installedPrePackagedRules.size,
rules_not_installed: rulesToInstall.length,
rules_not_updated: rulesToUpdate.length,
timelines_installed: validatedprepackagedTimelineStatus?.prepackagedTimelines.length ?? 0,
timelines_installed: validatedPrepackagedTimelineStatus?.prepackagedTimelines.length ?? 0,
timelines_not_installed:
validatedprepackagedTimelineStatus?.timelinesToInstall.length ?? 0,
timelines_not_updated: validatedprepackagedTimelineStatus?.timelinesToUpdate.length ?? 0,
validatedPrepackagedTimelineStatus?.timelinesToInstall.length ?? 0,
timelines_not_updated: validatedPrepackagedTimelineStatus?.timelinesToUpdate.length ?? 0,
};
const [validated, errors] = validate(
prepackagedRulesStatus,

View file

@ -6,9 +6,9 @@
*/
import type { RulesClient } from '@kbn/alerting-plugin/server';
import type { RuleAlertType } from './types';
import { isAlertTypes } from './types';
import { withSecuritySpan } from '../../../utils/with_security_span';
import { findRules } from './find_rules';
import type { RuleAlertType } from './types';
export const FILTER_NON_PREPACKED_RULES = 'alert.attributes.params.immutable: false';
export const FILTER_PREPACKED_RULES = 'alert.attributes.params.immutable: true';
@ -28,16 +28,18 @@ export const getRulesCount = async ({
rulesClient: RulesClient;
filter: string;
}): Promise<number> => {
const firstRule = await findRules({
rulesClient,
filter,
perPage: 1,
page: 1,
sortField: 'createdAt',
sortOrder: 'desc',
fields: undefined,
return withSecuritySpan('getRulesCount', async () => {
const { total } = await findRules({
rulesClient,
filter,
perPage: 0,
page: 1,
sortField: 'createdAt',
sortOrder: 'desc',
fields: undefined,
});
return total;
});
return firstRule.total;
};
export const getRules = async ({
@ -46,26 +48,21 @@ export const getRules = async ({
}: {
rulesClient: RulesClient;
filter: string;
}) => {
const count = await getRulesCount({ rulesClient, filter });
const rules = await findRules({
rulesClient,
filter,
perPage: count,
page: 1,
sortField: 'createdAt',
sortOrder: 'desc',
fields: undefined,
});
}): Promise<RuleAlertType[]> =>
withSecuritySpan('getRules', async () => {
const count = await getRulesCount({ rulesClient, filter });
const rules = await findRules({
rulesClient,
filter,
perPage: count,
page: 1,
sortField: 'createdAt',
sortOrder: 'desc',
fields: undefined,
});
if (isAlertTypes(rules.data)) {
return rules.data;
} else {
// If this was ever true, you have a really messed up system.
// This is keep typescript happy since we have an unknown with data
return [];
}
};
});
export const getNonPackagedRules = async ({
rulesClient,

View file

@ -5,20 +5,20 @@
* 2.0.
*/
import type * as t from 'io-ts';
import type { SavedObjectAttributes } from '@kbn/core/types';
import { BadRequestError } from '@kbn/securitysolution-es-utils';
import { exactCheck, formatErrors } from '@kbn/securitysolution-io-ts-utils';
import { fold } from 'fp-ts/lib/Either';
import { pipe } from 'fp-ts/lib/pipeable';
import { exactCheck, formatErrors } from '@kbn/securitysolution-io-ts-utils';
import { BadRequestError } from '@kbn/securitysolution-es-utils';
import type { SavedObjectAttributes } from '@kbn/core/types';
import type * as t from 'io-ts';
import type { AddPrepackagedRulesSchema } from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema';
import { addPrepackagedRulesSchema } from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema';
import type { ConfigType } from '../../../config';
import { withSecuritySpan } from '../../../utils/with_security_span';
// TODO: convert rules files to TS and add explicit type definitions
import { rawRules } from './prepackaged_rules';
import type { RuleAssetSavedObjectsClient } from './rule_asset/rule_asset_saved_objects_client';
import type { IRuleAssetSOAttributes } from './types';
import type { ConfigType } from '../../../config';
/**
* Validate the rules from the file system and throw any errors indicating to the developer
@ -104,22 +104,24 @@ export const getLatestPrepackagedRules = async (
client: RuleAssetSavedObjectsClient,
prebuiltRulesFromFileSystem: ConfigType['prebuiltRulesFromFileSystem'],
prebuiltRulesFromSavedObjects: ConfigType['prebuiltRulesFromSavedObjects']
): Promise<AddPrepackagedRulesSchema[]> => {
// build a map of the most recent version of each rule
const prepackaged = prebuiltRulesFromFileSystem ? getPrepackagedRules() : [];
const ruleMap = new Map(prepackaged.map((r) => [r.rule_id, r]));
): Promise<Map<string, AddPrepackagedRulesSchema>> =>
withSecuritySpan('getLatestPrepackagedRules', async () => {
// build a map of the most recent version of each rule
const prepackaged = prebuiltRulesFromFileSystem ? getPrepackagedRules() : [];
const ruleMap = new Map(prepackaged.map((r) => [r.rule_id, r]));
// check the rules installed via fleet and create/update if the version is newer
if (prebuiltRulesFromSavedObjects) {
const fleetRules = await getFleetInstalledRules(client);
const fleetUpdates = fleetRules.filter((r) => {
const rule = ruleMap.get(r.rule_id);
return rule == null || rule.version < r.version;
});
// check the rules installed via fleet and create/update if the version is newer
if (prebuiltRulesFromSavedObjects) {
const fleetRules = await getFleetInstalledRules(client);
fleetRules.forEach((fleetRule) => {
const fsRule = ruleMap.get(fleetRule.rule_id);
// add the new or updated rules to the map
fleetUpdates.forEach((r) => ruleMap.set(r.rule_id, r));
}
if (fsRule == null || fsRule.version < fleetRule.version) {
// add the new or updated rules to the map
ruleMap.set(fleetRule.rule_id, fleetRule);
}
});
}
return Array.from(ruleMap.values());
};
return ruleMap;
});

View file

@ -9,10 +9,11 @@ import { getRulesToInstall } from './get_rules_to_install';
import { getRuleMock } from '../routes/__mocks__/request_responses';
import { getAddPrepackagedRulesSchemaMock } from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema.mock';
import { getQueryRuleParams } from '../schemas/rule_schemas.mock';
import { prepackagedRulesToMap, rulesToMap } from './utils';
describe('get_rules_to_install', () => {
test('should return empty array if both rule sets are empty', () => {
const update = getRulesToInstall([], []);
const update = getRulesToInstall(prepackagedRulesToMap([]), rulesToMap([]));
expect(update).toEqual([]);
});
@ -22,7 +23,10 @@ describe('get_rules_to_install', () => {
const installedRule = getRuleMock(getQueryRuleParams());
installedRule.params.ruleId = 'rule-1';
const update = getRulesToInstall([ruleFromFileSystem], [installedRule]);
const update = getRulesToInstall(
prepackagedRulesToMap([ruleFromFileSystem]),
rulesToMap([installedRule])
);
expect(update).toEqual([]);
});
@ -32,7 +36,10 @@ describe('get_rules_to_install', () => {
const installedRule = getRuleMock(getQueryRuleParams());
installedRule.params.ruleId = 'rule-2';
const update = getRulesToInstall([ruleFromFileSystem], [installedRule]);
const update = getRulesToInstall(
prepackagedRulesToMap([ruleFromFileSystem]),
rulesToMap([installedRule])
);
expect(update).toEqual([ruleFromFileSystem]);
});
@ -45,7 +52,10 @@ describe('get_rules_to_install', () => {
const installedRule = getRuleMock(getQueryRuleParams());
installedRule.params.ruleId = 'rule-3';
const update = getRulesToInstall([ruleFromFileSystem1, ruleFromFileSystem2], [installedRule]);
const update = getRulesToInstall(
prepackagedRulesToMap([ruleFromFileSystem1, ruleFromFileSystem2]),
rulesToMap([installedRule])
);
expect(update).toEqual([ruleFromFileSystem1, ruleFromFileSystem2]);
});
@ -62,8 +72,8 @@ describe('get_rules_to_install', () => {
const installedRule = getRuleMock(getQueryRuleParams());
installedRule.params.ruleId = 'rule-3';
const update = getRulesToInstall(
[ruleFromFileSystem1, ruleFromFileSystem2, ruleFromFileSystem3],
[installedRule]
prepackagedRulesToMap([ruleFromFileSystem1, ruleFromFileSystem2, ruleFromFileSystem3]),
rulesToMap([installedRule])
);
expect(update).toEqual([ruleFromFileSystem1, ruleFromFileSystem2]);
});

View file

@ -9,10 +9,10 @@ import type { AddPrepackagedRulesSchema } from '../../../../common/detection_eng
import type { RuleAlertType } from './types';
export const getRulesToInstall = (
rulesFromFileSystem: AddPrepackagedRulesSchema[],
installedRules: RuleAlertType[]
latestPrePackagedRules: Map<string, AddPrepackagedRulesSchema>,
installedRules: Map<string, RuleAlertType>
) => {
return rulesFromFileSystem.filter(
(rule) => !installedRules.some((installedRule) => installedRule.params.ruleId === rule.rule_id)
return Array.from(latestPrePackagedRules.values()).filter(
(rule) => !installedRules.has(rule.rule_id)
);
};

View file

@ -9,10 +9,11 @@ import { filterInstalledRules, getRulesToUpdate, mergeExceptionLists } from './g
import { getRuleMock } from '../routes/__mocks__/request_responses';
import { getAddPrepackagedRulesSchemaMock } from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema.mock';
import { getQueryRuleParams } from '../schemas/rule_schemas.mock';
import { prepackagedRulesToMap, rulesToMap } from './utils';
describe('get_rules_to_update', () => {
test('should return empty array if both rule sets are empty', () => {
const update = getRulesToUpdate([], []);
const update = getRulesToUpdate(prepackagedRulesToMap([]), rulesToMap([]));
expect(update).toEqual([]);
});
@ -24,7 +25,10 @@ describe('get_rules_to_update', () => {
const installedRule = getRuleMock(getQueryRuleParams());
installedRule.params.ruleId = 'rule-2';
installedRule.params.version = 1;
const update = getRulesToUpdate([ruleFromFileSystem], [installedRule]);
const update = getRulesToUpdate(
prepackagedRulesToMap([ruleFromFileSystem]),
rulesToMap([installedRule])
);
expect(update).toEqual([]);
});
@ -36,7 +40,10 @@ describe('get_rules_to_update', () => {
const installedRule = getRuleMock(getQueryRuleParams());
installedRule.params.ruleId = 'rule-1';
installedRule.params.version = 2;
const update = getRulesToUpdate([ruleFromFileSystem], [installedRule]);
const update = getRulesToUpdate(
prepackagedRulesToMap([ruleFromFileSystem]),
rulesToMap([installedRule])
);
expect(update).toEqual([]);
});
@ -48,7 +55,10 @@ describe('get_rules_to_update', () => {
const installedRule = getRuleMock(getQueryRuleParams());
installedRule.params.ruleId = 'rule-1';
installedRule.params.version = 1;
const update = getRulesToUpdate([ruleFromFileSystem], [installedRule]);
const update = getRulesToUpdate(
prepackagedRulesToMap([ruleFromFileSystem]),
rulesToMap([installedRule])
);
expect(update).toEqual([]);
});
@ -62,7 +72,10 @@ describe('get_rules_to_update', () => {
installedRule.params.version = 1;
installedRule.params.exceptionsList = [];
const update = getRulesToUpdate([ruleFromFileSystem], [installedRule]);
const update = getRulesToUpdate(
prepackagedRulesToMap([ruleFromFileSystem]),
rulesToMap([installedRule])
);
expect(update).toEqual([ruleFromFileSystem]);
});
@ -81,7 +94,10 @@ describe('get_rules_to_update', () => {
installedRule2.params.version = 1;
installedRule2.params.exceptionsList = [];
const update = getRulesToUpdate([ruleFromFileSystem], [installedRule1, installedRule2]);
const update = getRulesToUpdate(
prepackagedRulesToMap([ruleFromFileSystem]),
rulesToMap([installedRule1, installedRule2])
);
expect(update).toEqual([ruleFromFileSystem]);
});
@ -105,8 +121,8 @@ describe('get_rules_to_update', () => {
installedRule2.params.exceptionsList = [];
const update = getRulesToUpdate(
[ruleFromFileSystem1, ruleFromFileSystem2],
[installedRule1, installedRule2]
prepackagedRulesToMap([ruleFromFileSystem1, ruleFromFileSystem2]),
rulesToMap([installedRule1, installedRule2])
);
expect(update).toEqual([ruleFromFileSystem1, ruleFromFileSystem2]);
});
@ -129,7 +145,10 @@ describe('get_rules_to_update', () => {
installedRule1.params.version = 1;
installedRule1.params.exceptionsList = [];
const [update] = getRulesToUpdate([ruleFromFileSystem1], [installedRule1]);
const [update] = getRulesToUpdate(
prepackagedRulesToMap([ruleFromFileSystem1]),
rulesToMap([installedRule1])
);
expect(update.exceptions_list).toEqual(ruleFromFileSystem1.exceptions_list);
});
@ -158,7 +177,10 @@ describe('get_rules_to_update', () => {
},
];
const [update] = getRulesToUpdate([ruleFromFileSystem1], [installedRule1]);
const [update] = getRulesToUpdate(
prepackagedRulesToMap([ruleFromFileSystem1]),
rulesToMap([installedRule1])
);
expect(update.exceptions_list).toEqual([
...ruleFromFileSystem1.exceptions_list,
...installedRule1.params.exceptionsList,
@ -190,7 +212,10 @@ describe('get_rules_to_update', () => {
},
];
const [update] = getRulesToUpdate([ruleFromFileSystem1], [installedRule1]);
const [update] = getRulesToUpdate(
prepackagedRulesToMap([ruleFromFileSystem1]),
rulesToMap([installedRule1])
);
expect(update.exceptions_list).toEqual(ruleFromFileSystem1.exceptions_list);
});
@ -212,7 +237,10 @@ describe('get_rules_to_update', () => {
},
];
const [update] = getRulesToUpdate([ruleFromFileSystem1], [installedRule1]);
const [update] = getRulesToUpdate(
prepackagedRulesToMap([ruleFromFileSystem1]),
rulesToMap([installedRule1])
);
expect(update.exceptions_list).toEqual(installedRule1.params.exceptionsList);
});
@ -251,8 +279,8 @@ describe('get_rules_to_update', () => {
];
const [update1, update2] = getRulesToUpdate(
[ruleFromFileSystem1, ruleFromFileSystem2],
[installedRule1, installedRule2]
prepackagedRulesToMap([ruleFromFileSystem1, ruleFromFileSystem2]),
rulesToMap([installedRule1, installedRule2])
);
expect(update1.exceptions_list).toEqual(installedRule1.params.exceptionsList);
expect(update2.exceptions_list).toEqual(installedRule2.params.exceptionsList);
@ -302,8 +330,8 @@ describe('get_rules_to_update', () => {
];
const [update1, update2] = getRulesToUpdate(
[ruleFromFileSystem1, ruleFromFileSystem2],
[installedRule1, installedRule2]
prepackagedRulesToMap([ruleFromFileSystem1, ruleFromFileSystem2]),
rulesToMap([installedRule1, installedRule2])
);
expect(update1.exceptions_list).toEqual(installedRule1.params.exceptionsList);
expect(update2.exceptions_list).toEqual([
@ -322,7 +350,7 @@ describe('filterInstalledRules', () => {
const installedRule = getRuleMock(getQueryRuleParams());
installedRule.params.ruleId = 'rule-2';
installedRule.params.version = 1;
const shouldUpdate = filterInstalledRules(ruleFromFileSystem, [installedRule]);
const shouldUpdate = filterInstalledRules(ruleFromFileSystem, rulesToMap([installedRule]));
expect(shouldUpdate).toEqual(false);
});
@ -334,7 +362,7 @@ describe('filterInstalledRules', () => {
const installedRule = getRuleMock(getQueryRuleParams());
installedRule.params.ruleId = 'rule-1';
installedRule.params.version = 2;
const shouldUpdate = filterInstalledRules(ruleFromFileSystem, [installedRule]);
const shouldUpdate = filterInstalledRules(ruleFromFileSystem, rulesToMap([installedRule]));
expect(shouldUpdate).toEqual(false);
});
@ -346,7 +374,7 @@ describe('filterInstalledRules', () => {
const installedRule = getRuleMock(getQueryRuleParams());
installedRule.params.ruleId = 'rule-1';
installedRule.params.version = 1;
const shouldUpdate = filterInstalledRules(ruleFromFileSystem, [installedRule]);
const shouldUpdate = filterInstalledRules(ruleFromFileSystem, rulesToMap([installedRule]));
expect(shouldUpdate).toEqual(false);
});
@ -360,7 +388,7 @@ describe('filterInstalledRules', () => {
installedRule.params.version = 1;
installedRule.params.exceptionsList = [];
const shouldUpdate = filterInstalledRules(ruleFromFileSystem, [installedRule]);
const shouldUpdate = filterInstalledRules(ruleFromFileSystem, rulesToMap([installedRule]));
expect(shouldUpdate).toEqual(true);
});
});
@ -384,7 +412,7 @@ describe('mergeExceptionLists', () => {
installedRule1.params.version = 1;
installedRule1.params.exceptionsList = [];
const update = mergeExceptionLists(ruleFromFileSystem1, [installedRule1]);
const update = mergeExceptionLists(ruleFromFileSystem1, rulesToMap([installedRule1]));
expect(update.exceptions_list).toEqual(ruleFromFileSystem1.exceptions_list);
});
@ -413,7 +441,7 @@ describe('mergeExceptionLists', () => {
},
];
const update = mergeExceptionLists(ruleFromFileSystem1, [installedRule1]);
const update = mergeExceptionLists(ruleFromFileSystem1, rulesToMap([installedRule1]));
expect(update.exceptions_list).toEqual([
...ruleFromFileSystem1.exceptions_list,
...installedRule1.params.exceptionsList,
@ -445,7 +473,7 @@ describe('mergeExceptionLists', () => {
},
];
const update = mergeExceptionLists(ruleFromFileSystem1, [installedRule1]);
const update = mergeExceptionLists(ruleFromFileSystem1, rulesToMap([installedRule1]));
expect(update.exceptions_list).toEqual(ruleFromFileSystem1.exceptions_list);
});
@ -467,7 +495,7 @@ describe('mergeExceptionLists', () => {
},
];
const update = mergeExceptionLists(ruleFromFileSystem1, [installedRule1]);
const update = mergeExceptionLists(ruleFromFileSystem1, rulesToMap([installedRule1]));
expect(update.exceptions_list).toEqual(installedRule1.params.exceptionsList);
});
});

View file

@ -12,63 +12,60 @@ import type { RuleAlertType } from './types';
* Returns the rules to update by doing a compare to the rules from the file system against
* the installed rules already. This also merges exception list items between the two since
* exception list items can exist on both rules to update and already installed rules.
* @param rulesFromFileSystem The rules on the file system to check against installed
* @param latestPrePackagedRules The latest rules to check against installed
* @param installedRules The installed rules
*/
export const getRulesToUpdate = (
rulesFromFileSystem: AddPrepackagedRulesSchema[],
installedRules: RuleAlertType[]
latestPrePackagedRules: Map<string, AddPrepackagedRulesSchema>,
installedRules: Map<string, RuleAlertType>
) => {
return rulesFromFileSystem
.filter((ruleFromFileSystem) => filterInstalledRules(ruleFromFileSystem, installedRules))
.map((ruleFromFileSystem) => mergeExceptionLists(ruleFromFileSystem, installedRules));
return Array.from(latestPrePackagedRules.values())
.filter((latestRule) => filterInstalledRules(latestRule, installedRules))
.map((latestRule) => mergeExceptionLists(latestRule, installedRules));
};
/**
* Filters rules from the file system that do not match the installed rules so you only
* get back rules that are going to be updated
* @param ruleFromFileSystem The rules from the file system to check if any are updates
* Filters latest prepackaged rules that do not match the installed rules so you
* only get back rules that are going to be updated
* @param latestPrePackagedRule The latest prepackaged rule version
* @param installedRules The installed rules to compare against for updates
*/
export const filterInstalledRules = (
ruleFromFileSystem: AddPrepackagedRulesSchema,
installedRules: RuleAlertType[]
latestPrePackagedRule: AddPrepackagedRulesSchema,
installedRules: Map<string, RuleAlertType>
): boolean => {
return installedRules.some((installedRule) => {
return (
ruleFromFileSystem.rule_id === installedRule.params.ruleId &&
ruleFromFileSystem.version > installedRule.params.version
);
});
const installedRule = installedRules.get(latestPrePackagedRule.rule_id);
return !!installedRule && installedRule.params.version < latestPrePackagedRule.version;
};
/**
* Given a rule from the file system and the set of installed rules this will merge the exception lists
* from the installed rules onto the rules from the file system.
* @param ruleFromFileSystem The rules from the file system that might have exceptions_lists
* @param latestPrePackagedRule The latest prepackaged rule version that might have exceptions_lists
* @param installedRules The installed rules which might have user driven exceptions_lists
*/
export const mergeExceptionLists = (
ruleFromFileSystem: AddPrepackagedRulesSchema,
installedRules: RuleAlertType[]
latestPrePackagedRule: AddPrepackagedRulesSchema,
installedRules: Map<string, RuleAlertType>
): AddPrepackagedRulesSchema => {
if (ruleFromFileSystem.exceptions_list != null) {
const installedRule = installedRules.find(
(ruleToFind) => ruleToFind.params.ruleId === ruleFromFileSystem.rule_id
);
if (latestPrePackagedRule.exceptions_list != null) {
const installedRule = installedRules.get(latestPrePackagedRule.rule_id);
if (installedRule != null && installedRule.params.exceptionsList != null) {
const installedExceptionList = installedRule.params.exceptionsList;
const fileSystemExceptions = ruleFromFileSystem.exceptions_list.filter((potentialDuplicate) =>
installedExceptionList.every((item) => item.list_id !== potentialDuplicate.list_id)
const fileSystemExceptions = latestPrePackagedRule.exceptions_list.filter(
(potentialDuplicate) =>
installedExceptionList.every((item) => item.list_id !== potentialDuplicate.list_id)
);
return {
...ruleFromFileSystem,
...latestPrePackagedRule,
exceptions_list: [...fileSystemExceptions, ...installedRule.params.exceptionsList],
};
} else {
return ruleFromFileSystem;
return latestPrePackagedRule;
}
} else {
return ruleFromFileSystem;
return latestPrePackagedRule;
}
};

View file

@ -5,23 +5,32 @@
* 2.0.
*/
import type { SanitizedRule, RuleTypeParams } from '@kbn/alerting-plugin/common';
import type { RulesClient } from '@kbn/alerting-plugin/server';
import { MAX_RULES_TO_UPDATE_IN_PARALLEL } from '../../../../common/constants';
import type { AddPrepackagedRulesSchema } from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema';
import { initPromisePool } from '../../../utils/promise_pool';
import { withSecuritySpan } from '../../../utils/with_security_span';
import { createRules } from './create_rules';
export const installPrepackagedRules = (
rulesClient: RulesClient,
rules: AddPrepackagedRulesSchema[]
): Array<Promise<SanitizedRule<RuleTypeParams>>> =>
rules.reduce<Array<Promise<SanitizedRule<RuleTypeParams>>>>((acc, rule) => {
return [
...acc,
createRules({
rulesClient,
params: rule,
immutable: true,
defaultEnabled: false,
}),
];
}, []);
) =>
withSecuritySpan('installPrepackagedRules', async () => {
const result = await initPromisePool({
concurrency: MAX_RULES_TO_UPDATE_IN_PARALLEL,
items: rules,
executor: async (rule) => {
return createRules({
rulesClient,
params: rule,
immutable: true,
defaultEnabled: false,
});
},
});
if (result.errors.length > 0) {
throw new AggregateError(result.errors, 'Error installing prepackaged rules');
}
});

View file

@ -6,6 +6,7 @@
*/
import type { ResolvedSanitizedRule, SanitizedRule } from '@kbn/alerting-plugin/common';
import { withSecuritySpan } from '../../../utils/with_security_span';
import type { RuleParams } from '../schemas/rule_schemas';
import { findRules } from './find_rules';
import type { ReadRuleOptions } from './types';
@ -26,43 +27,45 @@ export const readRules = async ({
}: ReadRuleOptions): Promise<
SanitizedRule<RuleParams> | ResolvedSanitizedRule<RuleParams> | null
> => {
if (id != null) {
try {
const rule = await rulesClient.resolve({ id });
if (isAlertType(rule)) {
if (rule?.outcome === 'exactMatch') {
const { outcome, ...restOfRule } = rule;
return restOfRule;
return withSecuritySpan('readRules', async () => {
if (id != null) {
try {
const rule = await rulesClient.resolve({ id });
if (isAlertType(rule)) {
if (rule?.outcome === 'exactMatch') {
const { outcome, ...restOfRule } = rule;
return restOfRule;
}
return rule;
} else {
return null;
}
} catch (err) {
if (err?.output?.statusCode === 404) {
return null;
} else {
// throw non-404 as they would be 500 or other internal errors
throw err;
}
return rule;
} else {
return null;
}
} catch (err) {
if (err?.output?.statusCode === 404) {
} else if (ruleId != null) {
const ruleFromFind = await findRules({
rulesClient,
filter: `alert.attributes.params.ruleId: "${ruleId}"`,
page: 1,
fields: undefined,
perPage: undefined,
sortField: undefined,
sortOrder: undefined,
});
if (ruleFromFind.data.length === 0 || !isAlertType(ruleFromFind.data[0])) {
return null;
} else {
// throw non-404 as they would be 500 or other internal errors
throw err;
return ruleFromFind.data[0];
}
}
} else if (ruleId != null) {
const ruleFromFind = await findRules({
rulesClient,
filter: `alert.attributes.params.ruleId: "${ruleId}"`,
page: 1,
fields: undefined,
perPage: undefined,
sortField: undefined,
sortOrder: undefined,
});
if (ruleFromFind.data.length === 0 || !isAlertType(ruleFromFind.data[0])) {
return null;
} else {
return ruleFromFind.data[0];
// should never get here, and yet here we are.
return null;
}
} else {
// should never get here, and yet here we are.
return null;
}
});
};

View file

@ -5,26 +5,30 @@
* 2.0.
*/
import { isEmpty } from 'lodash/fp';
import type { SavedObjectReference } from '@kbn/core/server';
import type { RuleAction, RuleNotifyWhenType, SanitizedRule } from '@kbn/alerting-plugin/common';
import type { RulesClient } from '@kbn/alerting-plugin/server';
import type { RuleParams } from '../schemas/rule_schemas';
import type { SavedObjectReference } from '@kbn/core/server';
import { isEmpty } from 'lodash/fp';
import {
NOTIFICATION_THROTTLE_NO_ACTIONS,
NOTIFICATION_THROTTLE_RULE,
} from '../../../../common/constants';
import type {
AddPrepackagedRulesSchema,
FullResponseSchema,
} from '../../../../common/detection_engine/schemas/request';
import { transformAlertToRuleAction } from '../../../../common/detection_engine/transform_actions';
import { withSecuritySpan } from '../../../utils/with_security_span';
// eslint-disable-next-line no-restricted-imports
import { legacyRuleActionsSavedObjectType } from '../rule_actions/legacy_saved_object_mappings';
// eslint-disable-next-line no-restricted-imports
import type {
LegacyIRuleActionsAttributes,
LegacyRuleActions,
LegacyRuleAlertSavedObjectAction,
} from '../rule_actions/legacy_types';
import type { FullResponseSchema } from '../../../../common/detection_engine/schemas/request';
import { transformAlertToRuleAction } from '../../../../common/detection_engine/transform_actions';
// eslint-disable-next-line no-restricted-imports
import { legacyRuleActionsSavedObjectType } from '../rule_actions/legacy_saved_object_mappings';
import type { LegacyMigrateParams } from './types';
import type { RuleParams } from '../schemas/rule_schemas';
import type { LegacyMigrateParams, RuleAlertType } from './types';
/**
* Given a throttle from a "security_solution" rule this will transform it into an "alerting" notifyWhen
@ -203,88 +207,106 @@ export const legacyMigrate = async ({
rulesClient,
savedObjectsClient,
rule,
}: LegacyMigrateParams): Promise<SanitizedRule<RuleParams> | null | undefined> => {
if (rule == null || rule.id == null) {
return rule;
}
/**
* On update / patch I'm going to take the actions as they are, better off taking rules client.find (siem.notification) result
* and putting that into the actions array of the rule, then set the rules onThrottle property, notifyWhen and throttle from null -> actual value (1hr etc..)
* Then use the rules client to delete the siem.notification
* Then with the legacy Rule Actions saved object type, just delete it.
*/
// find it using the references array, not params.ruleAlertId
const [siemNotification, legacyRuleActionsSO] = await Promise.all([
rulesClient.find({
options: {
filter: 'alert.attributes.alertTypeId:(siem.notifications)',
}: LegacyMigrateParams): Promise<SanitizedRule<RuleParams> | null | undefined> =>
withSecuritySpan('legacyMigrate', async () => {
if (rule == null || rule.id == null) {
return rule;
}
/**
* On update / patch I'm going to take the actions as they are, better off taking rules client.find (siem.notification) result
* and putting that into the actions array of the rule, then set the rules onThrottle property, notifyWhen and throttle from null -> actual value (1hr etc..)
* Then use the rules client to delete the siem.notification
* Then with the legacy Rule Actions saved object type, just delete it.
*/
// find it using the references array, not params.ruleAlertId
const [siemNotification, legacyRuleActionsSO] = await Promise.all([
rulesClient.find({
options: {
filter: 'alert.attributes.alertTypeId:(siem.notifications)',
hasReference: {
type: 'alert',
id: rule.id,
},
},
}),
savedObjectsClient.find<LegacyIRuleActionsAttributes>({
type: legacyRuleActionsSavedObjectType,
hasReference: {
type: 'alert',
id: rule.id,
},
},
}),
savedObjectsClient.find<LegacyIRuleActionsAttributes>({
type: legacyRuleActionsSavedObjectType,
hasReference: {
type: 'alert',
id: rule.id,
},
}),
]);
}),
]);
const siemNotificationsExist = siemNotification != null && siemNotification.data.length > 0;
const legacyRuleNotificationSOsExist =
legacyRuleActionsSO != null && legacyRuleActionsSO.saved_objects.length > 0;
const siemNotificationsExist = siemNotification != null && siemNotification.data.length > 0;
const legacyRuleNotificationSOsExist =
legacyRuleActionsSO != null && legacyRuleActionsSO.saved_objects.length > 0;
// Assumption: if no legacy sidecar SO or notification rule types exist
// that reference the rule in question, assume rule actions are not legacy
if (!siemNotificationsExist && !legacyRuleNotificationSOsExist) {
return rule;
}
// If the legacy notification rule type ("siem.notification") exist,
// migration and cleanup are needed
if (siemNotificationsExist) {
await rulesClient.delete({ id: siemNotification.data[0].id });
}
// If legacy notification sidecar ("siem-detection-engine-rule-actions")
// exist, migration and cleanup are needed
if (legacyRuleNotificationSOsExist) {
// Delete the legacy sidecar SO
await savedObjectsClient.delete(
legacyRuleActionsSavedObjectType,
legacyRuleActionsSO.saved_objects[0].id
);
// If "siem-detection-engine-rule-actions" notes that `ruleThrottle` is
// "no_actions" or "rule", rule has no actions or rule is set to run
// action on every rule run. In these cases, sidecar deletion is the only
// cleanup needed and updates to the "throttle" and "notifyWhen". "siem.notification" are
// not created for these action types
if (
legacyRuleActionsSO.saved_objects[0].attributes.ruleThrottle === 'no_actions' ||
legacyRuleActionsSO.saved_objects[0].attributes.ruleThrottle === 'rule'
) {
// Assumption: if no legacy sidecar SO or notification rule types exist
// that reference the rule in question, assume rule actions are not legacy
if (!siemNotificationsExist && !legacyRuleNotificationSOsExist) {
return rule;
}
// If the legacy notification rule type ("siem.notification") exist,
// migration and cleanup are needed
if (siemNotificationsExist) {
await rulesClient.delete({ id: siemNotification.data[0].id });
}
// If legacy notification sidecar ("siem-detection-engine-rule-actions")
// exist, migration and cleanup are needed
if (legacyRuleNotificationSOsExist) {
// Delete the legacy sidecar SO
await savedObjectsClient.delete(
legacyRuleActionsSavedObjectType,
legacyRuleActionsSO.saved_objects[0].id
);
// Use "legacyRuleActionsSO" instead of "siemNotification" as "siemNotification" is not created
// until a rule is run and added to task manager. That means that if by chance a user has a rule
// with actions which they have yet to enable, the actions would be lost. Instead,
// "legacyRuleActionsSO" is created on rule creation (pre 7.15) and we can rely on it to be there
const migratedRule = getUpdatedActionsParams({
rule,
ruleThrottle: legacyRuleActionsSO.saved_objects[0].attributes.ruleThrottle,
actions: legacyRuleActionsSO.saved_objects[0].attributes.actions,
references: legacyRuleActionsSO.saved_objects[0].references,
});
// If "siem-detection-engine-rule-actions" notes that `ruleThrottle` is
// "no_actions" or "rule", rule has no actions or rule is set to run
// action on every rule run. In these cases, sidecar deletion is the only
// cleanup needed and updates to the "throttle" and "notifyWhen". "siem.notification" are
// not created for these action types
if (
legacyRuleActionsSO.saved_objects[0].attributes.ruleThrottle === 'no_actions' ||
legacyRuleActionsSO.saved_objects[0].attributes.ruleThrottle === 'rule'
) {
return rule;
}
await rulesClient.update({
id: rule.id,
data: migratedRule,
});
// Use "legacyRuleActionsSO" instead of "siemNotification" as "siemNotification" is not created
// until a rule is run and added to task manager. That means that if by chance a user has a rule
// with actions which they have yet to enable, the actions would be lost. Instead,
// "legacyRuleActionsSO" is created on rule creation (pre 7.15) and we can rely on it to be there
const migratedRule = getUpdatedActionsParams({
rule,
ruleThrottle: legacyRuleActionsSO.saved_objects[0].attributes.ruleThrottle,
actions: legacyRuleActionsSO.saved_objects[0].attributes.actions,
references: legacyRuleActionsSO.saved_objects[0].references,
});
return { id: rule.id, ...migratedRule };
}
};
await rulesClient.update({
id: rule.id,
data: migratedRule,
});
return { id: rule.id, ...migratedRule };
}
});
/**
* Converts an array of prepackaged rules to a Map with rule IDs as keys
*
* @param rules Array of prepackaged rules
* @returns Map
*/
export const prepackagedRulesToMap = (rules: AddPrepackagedRulesSchema[]) =>
new Map(rules.map((rule) => [rule.rule_id, rule]));
/**
* Converts an array of rules to a Map with rule IDs as keys
*
* @param rules Array of rules
* @returns Map
*/
export const rulesToMap = (rules: RuleAlertType[]) =>
new Map(rules.map((rule) => [rule.params.ruleId, rule]));