mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Fix performance issues affecting rules management (#135311)
This commit is contained in:
parent
d8553f5647
commit
c7a5b1336d
40 changed files with 736 additions and 971 deletions
|
@ -1,2 +1,3 @@
|
|||
videos
|
||||
screenshots
|
||||
downloads
|
|
@ -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":[]}
|
|
@ -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');
|
||||
};
|
||||
|
|
|
@ -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`,
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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];
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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]);
|
||||
};
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -35,8 +35,8 @@ const SearchBarWrapper = styled(EuiFlexItem)`
|
|||
`;
|
||||
|
||||
interface RulesTableFiltersProps {
|
||||
rulesCustomInstalled: number | null;
|
||||
rulesInstalled: number | null;
|
||||
rulesCustomInstalled?: number;
|
||||
rulesInstalled?: number;
|
||||
allTags: string[];
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
|
|
|
@ -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 &&
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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}>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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]);
|
||||
});
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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]));
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue