From 4f1f2a84fdf3421d6b56aac82274dee2e881d376 Mon Sep 17 00:00:00 2001 From: Dmitrii Shevchenko Date: Mon, 13 Feb 2023 16:00:20 +0100 Subject: [PATCH] [Security Solution] Invalidate prebuilt rules status after package upgrade or installation (#150292) **Resolves: https://github.com/elastic/kibana/issues/150306** ## Summary Fixes the Load Prebuilt rules button not visible when users visit the rules management page for the first time (no prebuilt detection rules package installed). ## Steps to test 1. Ensure that the detection engine package is not installed: 2. Navigate to the rules management page. ### Previously The "Load Elastic Prebuilt Rules" button is not visible, and users cannot install prebuilt rules. ### With the fix Users now see loading animation, indicating that the package installation happens in the background. Once the package installation finishes, users see the Load Prebuilt rules button appear. https://user-images.githubusercontent.com/1938181/217585144-879fe288-0ede-4e01-b585-6aced1d89379.mov --- .../public/app/home/index.tsx | 2 +- .../hooks/use_upgrade_security_packages.ts | 113 ------------------ .../rule_management/api/api.ts | 61 ++++++++++ ...se_bulk_install_fleet_packages_mutation.ts | 44 +++++++ .../use_install_fleet_package_mutation.ts | 41 +++++++ .../logic/use_install_pre_packaged_rules.ts | 11 +- .../use_upgrade_secuirty_packages.test.tsx | 109 +++++++---------- .../logic/use_upgrade_security_packages.ts | 95 +++++++++++++++ .../components/rules_table/rules_tables.tsx | 4 +- .../load_prepackaged_rules.tsx | 10 +- 10 files changed, 304 insertions(+), 186 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/common/hooks/use_upgrade_security_packages.ts create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_bulk_install_fleet_packages_mutation.ts create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_install_fleet_package_mutation.ts rename x-pack/plugins/security_solution/public/{common/hooks => detection_engine/rule_management/logic}/use_upgrade_secuirty_packages.test.tsx (57%) create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_upgrade_security_packages.ts diff --git a/x-pack/plugins/security_solution/public/app/home/index.tsx b/x-pack/plugins/security_solution/public/app/home/index.tsx index 5aecd3cdc62ba..d74eb3eed5af7 100644 --- a/x-pack/plugins/security_solution/public/app/home/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/index.tsx @@ -18,7 +18,6 @@ import { getScopeFromPath, useSourcererDataView, } from '../../common/containers/sourcerer'; -import { useUpgradeSecurityPackages } from '../../common/hooks/use_upgrade_security_packages'; import { GlobalHeader } from './global_header'; import { ConsoleManager } from '../../management/components/console/components/console_manager'; @@ -26,6 +25,7 @@ import { TourContextProvider } from '../../common/components/guided_onboarding_t import { useUrlState } from '../../common/hooks/use_url_state'; import { useUpdateBrowserTitle } from '../../common/hooks/use_update_browser_title'; +import { useUpgradeSecurityPackages } from '../../detection_engine/rule_management/logic/use_upgrade_security_packages'; interface HomePageProps { children: React.ReactNode; diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_upgrade_security_packages.ts b/x-pack/plugins/security_solution/public/common/hooks/use_upgrade_security_packages.ts deleted file mode 100644 index 3ffb3ca149b20..0000000000000 --- a/x-pack/plugins/security_solution/public/common/hooks/use_upgrade_security_packages.ts +++ /dev/null @@ -1,113 +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 { useEffect } from 'react'; -import type { HttpFetchOptions, HttpStart } from '@kbn/core/public'; -import type { BulkInstallPackagesResponse } from '@kbn/fleet-plugin/common'; -import { epmRouteService } from '@kbn/fleet-plugin/common'; -import type { InstallPackageResponse } from '@kbn/fleet-plugin/common/types'; -import { KibanaServices, useKibana } from '../lib/kibana'; -import { useUserPrivileges } from '../components/user_privileges'; -import { PREBUILT_RULES_PACKAGE_NAME } from '../../../common/detection_engine/constants'; - -/** - * Requests that the endpoint and security_detection_engine package be upgraded to the latest version - * - * @param http an http client for sending the request - * @param options an object containing options for the request - * @param prebuiltRulesPackageVersion specific version of the prebuilt rules package to install - */ -const sendUpgradeSecurityPackages = async ( - http: HttpStart, - options: HttpFetchOptions = {}, - prebuiltRulesPackageVersion?: string -): Promise => { - const packages = ['endpoint', PREBUILT_RULES_PACKAGE_NAME]; - const requests: Array> = []; - - // If `prebuiltRulesPackageVersion` is provided, try to install that version - // Must be done as two separate requests as bulk API doesn't support versions - if (prebuiltRulesPackageVersion != null) { - packages.splice(packages.indexOf(PREBUILT_RULES_PACKAGE_NAME), 1); - requests.push( - http.post( - epmRouteService.getInstallPath(PREBUILT_RULES_PACKAGE_NAME, prebuiltRulesPackageVersion), - { - ...options, - body: JSON.stringify({ - force: true, - }), - } - ) - ); - } - - // Note: if `prerelease:true` option is provided, endpoint package will also be installed as prerelease - requests.push( - http.post(epmRouteService.getBulkInstallPath(), { - ...options, - body: JSON.stringify({ - packages, - }), - }) - ); - - await Promise.allSettled(requests); -}; - -export const useUpgradeSecurityPackages = () => { - const context = useKibana(); - const canAccessFleet = useUserPrivileges().endpointPrivileges.canAccessFleet; - - useEffect(() => { - const abortController = new AbortController(); - - // cancel any ongoing requests - const abortRequests = () => { - abortController.abort(); - }; - - if (canAccessFleet) { - const signal = abortController.signal; - - (async () => { - try { - // Make sure fleet is initialized first - await context.services.fleet?.isInitialized(); - - // Always install the latest package if in dev env or snapshot build - const isPrerelease = - KibanaServices.getKibanaVersion().includes('-SNAPSHOT') || - KibanaServices.getKibanaBranch() === 'main'; - - // ignore the response for now since we aren't notifying the user - // Note: response would be Promise.allSettled, so must iterate all responses for errors and throw manually - await sendUpgradeSecurityPackages( - context.services.http, - { - query: { - prerelease: isPrerelease, - }, - signal, - }, - KibanaServices.getPrebuiltRulesPackageVersion() - ); - } catch (error) { - // Ignore Errors, since this should not hinder the user's ability to use the UI - - // log to console, except if the error occurred due to aborting a request - if (!abortController.signal.aborted) { - // eslint-disable-next-line no-console - console.error(error); - } - } - })(); - - return abortRequests; - } - }, [canAccessFleet, context.services.fleet, context.services.http]); -}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts index 87334a121c993..386dbf3c7b525 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts @@ -10,6 +10,9 @@ import type { ExceptionListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; +import type { BulkInstallPackagesResponse } from '@kbn/fleet-plugin/common'; +import { epmRouteService } from '@kbn/fleet-plugin/common'; +import type { InstallPackageResponse } from '@kbn/fleet-plugin/common/types'; import type { RuleManagementFiltersResponse } from '../../../../common/detection_engine/rule_management/api/rules/filters/response_schema'; import { RULE_MANAGEMENT_FILTERS_URL } from '../../../../common/detection_engine/rule_management/api/urls'; import type { BulkActionsDryRunErrCode } from '../../../../common/constants'; @@ -481,3 +484,61 @@ export const addRuleExceptions = async ({ signal, } ); + +export interface InstallFleetPackageProps { + packageName: string; + packageVersion: string; + prerelease?: boolean; + force?: boolean; +} + +/** + * Install a Fleet package from the registry + * + * @param packageName Name of the package to install + * @param packageVersion Version of the package to install + * @param prerelease Whether to install a prerelease version of the package + * @param force Whether to force install the package. If false, the package will only be installed if it is not already installed + * + * @returns The response from the Fleet API + */ +export const installFleetPackage = ({ + packageName, + packageVersion, + prerelease = false, + force = true, +}: InstallFleetPackageProps): Promise => { + return KibanaServices.get().http.post( + epmRouteService.getInstallPath(packageName, packageVersion), + { + query: { prerelease }, + body: JSON.stringify({ force }), + } + ); +}; + +export interface BulkInstallFleetPackagesProps { + packages: string[]; + prerelease?: boolean; +} + +/** + * Install multiple Fleet packages from the registry + * + * @param packages Array of package names to install + * @param prerelease Whether to install prerelease versions of the packages + * + * @returns The response from the Fleet API + */ +export const bulkInstallFleetPackages = ({ + packages, + prerelease = false, +}: BulkInstallFleetPackagesProps): Promise => { + return KibanaServices.get().http.post( + epmRouteService.getBulkInstallPath(), + { + query: { prerelease }, + body: JSON.stringify({ packages }), + } + ); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_bulk_install_fleet_packages_mutation.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_bulk_install_fleet_packages_mutation.ts new file mode 100644 index 0000000000000..adbcec981ca3c --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_bulk_install_fleet_packages_mutation.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { EPM_API_ROUTES } from '@kbn/fleet-plugin/common'; +import type { BulkInstallPackagesResponse } from '@kbn/fleet-plugin/common/types'; +import type { UseMutationOptions } from '@tanstack/react-query'; +import { useMutation } from '@tanstack/react-query'; +import { PREBUILT_RULES_PACKAGE_NAME } from '../../../../../common/detection_engine/constants'; +import type { BulkInstallFleetPackagesProps } from '../api'; +import { bulkInstallFleetPackages } from '../api'; +import { useInvalidateFetchPrebuiltRulesStatusQuery } from './use_fetch_prebuilt_rules_status_query'; + +export const BULK_INSTALL_FLEET_PACKAGES_MUTATION_KEY = [ + 'POST', + EPM_API_ROUTES.BULK_INSTALL_PATTERN, +]; + +export const useBulkInstallFleetPackagesMutation = ( + options?: UseMutationOptions +) => { + const invalidatePrePackagedRulesStatus = useInvalidateFetchPrebuiltRulesStatusQuery(); + + return useMutation((props: BulkInstallFleetPackagesProps) => bulkInstallFleetPackages(props), { + ...options, + mutationKey: BULK_INSTALL_FLEET_PACKAGES_MUTATION_KEY, + onSettled: (...args) => { + const response = args[0]; + const rulesPackage = response?.items.find( + (item) => item.name === PREBUILT_RULES_PACKAGE_NAME + ); + if (rulesPackage && 'result' in rulesPackage && rulesPackage.result.status === 'installed') { + // The rules package was installed/updated, so invalidate the pre-packaged rules status query + invalidatePrePackagedRulesStatus(); + } + + if (options?.onSettled) { + options.onSettled(...args); + } + }, + }); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_install_fleet_package_mutation.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_install_fleet_package_mutation.ts new file mode 100644 index 0000000000000..0e6927e1745dd --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_install_fleet_package_mutation.ts @@ -0,0 +1,41 @@ +/* + * 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 { EPM_API_ROUTES } from '@kbn/fleet-plugin/common'; +import type { InstallPackageResponse } from '@kbn/fleet-plugin/common/types'; +import type { UseMutationOptions } from '@tanstack/react-query'; +import { useMutation } from '@tanstack/react-query'; +import { PREBUILT_RULES_PACKAGE_NAME } from '../../../../../common/detection_engine/constants'; +import type { InstallFleetPackageProps } from '../api'; +import { installFleetPackage } from '../api'; +import { useInvalidateFetchPrebuiltRulesStatusQuery } from './use_fetch_prebuilt_rules_status_query'; + +export const INSTALL_FLEET_PACKAGE_MUTATION_KEY = [ + 'POST', + EPM_API_ROUTES.INSTALL_FROM_REGISTRY_PATTERN, +]; + +export const useInstallFleetPackageMutation = ( + options?: UseMutationOptions +) => { + const invalidatePrePackagedRulesStatus = useInvalidateFetchPrebuiltRulesStatusQuery(); + + return useMutation((props: InstallFleetPackageProps) => installFleetPackage(props), { + ...options, + mutationKey: INSTALL_FLEET_PACKAGE_MUTATION_KEY, + onSettled: (...args) => { + const { packageName } = args[2]; + if (packageName === PREBUILT_RULES_PACKAGE_NAME) { + // Invalidate the pre-packaged rules status query as there might be new rules to install + invalidatePrePackagedRulesStatus(); + } + + if (options?.onSettled) { + options.onSettled(...args); + } + }, + }); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_install_pre_packaged_rules.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_install_pre_packaged_rules.ts index 21ea298986598..b7fa307c0fedc 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_install_pre_packaged_rules.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_install_pre_packaged_rules.ts @@ -4,8 +4,12 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { useIsMutating } from '@tanstack/react-query'; import { useAppToasts } from '../../../common/hooks/use_app_toasts'; -import { useCreatePrebuiltRulesMutation } from '../api/hooks/use_create_prebuilt_rules_mutation'; +import { + CREATE_PREBUILT_RULES_MUTATION_KEY, + useCreatePrebuiltRulesMutation, +} from '../api/hooks/use_create_prebuilt_rules_mutation'; import * as i18n from './translations'; export const useInstallPrePackagedRules = () => { @@ -21,6 +25,11 @@ export const useInstallPrePackagedRules = () => { }); }; +export const useIsInstallingPrePackagedRules = () => { + const mutationsCount = useIsMutating(CREATE_PREBUILT_RULES_MUTATION_KEY); + return mutationsCount > 0; +}; + const getSuccessToastMessage = (result: { rules_installed: number; rules_updated: number; diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_upgrade_secuirty_packages.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_upgrade_secuirty_packages.test.tsx similarity index 57% rename from x-pack/plugins/security_solution/public/common/hooks/use_upgrade_secuirty_packages.test.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_upgrade_secuirty_packages.test.tsx index 0352dd03bbcff..dd01465b8875a 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_upgrade_secuirty_packages.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_upgrade_secuirty_packages.test.tsx @@ -5,14 +5,13 @@ * 2.0. */ -import React, { memo } from 'react'; -import { KibanaServices, useKibana } from '../lib/kibana'; -import type { RenderHookResult } from '@testing-library/react-hooks'; -import { renderHook as _renderHook } from '@testing-library/react-hooks'; -import { useUpgradeSecurityPackages } from './use_upgrade_security_packages'; import { epmRouteService } from '@kbn/fleet-plugin/common'; +import { renderHook } from '@testing-library/react-hooks'; +import { useKibana, KibanaServices } from '../../../common/lib/kibana'; +import { TestProviders } from '../../../common/mock'; +import { useUpgradeSecurityPackages } from './use_upgrade_security_packages'; -jest.mock('../components/user_privileges', () => { +jest.mock('../../../common/components/user_privileges', () => { return { useUserPrivileges: jest.fn().mockReturnValue({ endpointPrivileges: { @@ -21,48 +20,30 @@ jest.mock('../components/user_privileges', () => { }), }; }); -jest.mock('../lib/kibana'); +jest.mock('../../../common/lib/kibana'); -describe('When using the `useUpgradeSecurityPackages()` hook', () => { - const mockGetPrebuiltRulesPackageVersion = - KibanaServices.getPrebuiltRulesPackageVersion as jest.Mock; - const mockGetKibanaVersion = KibanaServices.getKibanaVersion as jest.Mock; - const mockGetKibanaBranch = KibanaServices.getKibanaBranch as jest.Mock; - let renderResult: RenderHookResult; - let renderHook: () => RenderHookResult; - let kibana: ReturnType; - - // eslint-disable-next-line react/display-name - const Wrapper = memo(({ children }) => { - kibana = useKibana(); - return <>{children}; - }); +const mockGetPrebuiltRulesPackageVersion = + KibanaServices.getPrebuiltRulesPackageVersion as jest.Mock; +const mockGetKibanaVersion = KibanaServices.getKibanaVersion as jest.Mock; +const mockGetKibanaBranch = KibanaServices.getKibanaBranch as jest.Mock; +const useKibanaMock = useKibana as jest.MockedFunction; +describe('When using the `useUpgradeSecurityPackages()` hook', () => { beforeEach(() => { - renderHook = () => { - renderResult = _renderHook(() => useUpgradeSecurityPackages(), { wrapper: Wrapper }); - return renderResult; - }; - }); - - afterEach(() => { jest.clearAllMocks(); - if (renderResult) { - renderResult.unmount(); - } }); it('should call fleet setup first via `isInitialized()` and then send upgrade request', async () => { - renderHook(); + const { waitFor } = renderHook(() => useUpgradeSecurityPackages(), { + wrapper: TestProviders, + }); - expect(kibana.services.fleet?.isInitialized).toHaveBeenCalled(); - expect(kibana.services.http.post).not.toHaveBeenCalled(); + expect(useKibanaMock().services.fleet?.isInitialized).toHaveBeenCalled(); + expect(useKibanaMock().services.http.post).not.toHaveBeenCalled(); - await renderResult.waitFor( - () => (kibana.services.http.post as jest.Mock).mock.calls.length > 0 - ); + await waitFor(() => (useKibanaMock().services.http.post as jest.Mock).mock.calls.length > 0); - expect(kibana.services.http.post).toHaveBeenCalledWith( + expect(useKibanaMock().services.http.post).toHaveBeenCalledWith( `${epmRouteService.getBulkInstallPath()}`, expect.objectContaining({ body: '{"packages":["endpoint","security_detection_engine"]}', @@ -74,13 +55,13 @@ describe('When using the `useUpgradeSecurityPackages()` hook', () => { mockGetKibanaVersion.mockReturnValue('8.0.0'); mockGetKibanaBranch.mockReturnValue('release'); - renderHook(); + const { waitFor } = renderHook(() => useUpgradeSecurityPackages(), { + wrapper: TestProviders, + }); - await renderResult.waitFor( - () => (kibana.services.http.post as jest.Mock).mock.calls.length > 0 - ); + await waitFor(() => (useKibanaMock().services.http.post as jest.Mock).mock.calls.length > 0); - expect(kibana.services.http.post).toHaveBeenCalledWith( + expect(useKibanaMock().services.http.post).toHaveBeenCalledWith( `${epmRouteService.getBulkInstallPath()}`, expect.objectContaining({ body: '{"packages":["endpoint","security_detection_engine"]}', @@ -93,13 +74,13 @@ describe('When using the `useUpgradeSecurityPackages()` hook', () => { mockGetKibanaVersion.mockReturnValue('8.0.0-SNAPSHOT'); mockGetKibanaBranch.mockReturnValue('main'); - renderHook(); + const { waitFor } = renderHook(() => useUpgradeSecurityPackages(), { + wrapper: TestProviders, + }); - await renderResult.waitFor( - () => (kibana.services.http.post as jest.Mock).mock.calls.length > 0 - ); + await waitFor(() => (useKibanaMock().services.http.post as jest.Mock).mock.calls.length > 0); - expect(kibana.services.http.post).toHaveBeenCalledWith( + expect(useKibanaMock().services.http.post).toHaveBeenCalledWith( `${epmRouteService.getBulkInstallPath()}`, expect.objectContaining({ body: '{"packages":["endpoint","security_detection_engine"]}', @@ -112,13 +93,13 @@ describe('When using the `useUpgradeSecurityPackages()` hook', () => { mockGetKibanaVersion.mockReturnValue('8.0.0-SNAPSHOT'); mockGetKibanaBranch.mockReturnValue('release'); - renderHook(); + const { waitFor } = renderHook(() => useUpgradeSecurityPackages(), { + wrapper: TestProviders, + }); - await renderResult.waitFor( - () => (kibana.services.http.post as jest.Mock).mock.calls.length > 0 - ); + await waitFor(() => (useKibanaMock().services.http.post as jest.Mock).mock.calls.length > 0); - expect(kibana.services.http.post).toHaveBeenCalledWith( + expect(useKibanaMock().services.http.post).toHaveBeenCalledWith( `${epmRouteService.getBulkInstallPath()}`, expect.objectContaining({ body: '{"packages":["endpoint","security_detection_engine"]}', @@ -131,13 +112,13 @@ describe('When using the `useUpgradeSecurityPackages()` hook', () => { mockGetKibanaVersion.mockReturnValue('8.0.0'); mockGetKibanaBranch.mockReturnValue('main'); - renderHook(); + const { waitFor } = renderHook(() => useUpgradeSecurityPackages(), { + wrapper: TestProviders, + }); - await renderResult.waitFor( - () => (kibana.services.http.post as jest.Mock).mock.calls.length > 0 - ); + await waitFor(() => (useKibanaMock().services.http.post as jest.Mock).mock.calls.length > 0); - expect(kibana.services.http.post).toHaveBeenCalledWith( + expect(useKibanaMock().services.http.post).toHaveBeenCalledWith( `${epmRouteService.getBulkInstallPath()}`, expect.objectContaining({ body: '{"packages":["endpoint","security_detection_engine"]}', @@ -149,18 +130,18 @@ describe('When using the `useUpgradeSecurityPackages()` hook', () => { it('should send separate upgrade requests if prebuiltRulesPackageVersion is provided', async () => { mockGetPrebuiltRulesPackageVersion.mockReturnValue('8.2.1'); - renderHook(); + const { waitFor } = renderHook(() => useUpgradeSecurityPackages(), { + wrapper: TestProviders, + }); - await renderResult.waitFor( - () => (kibana.services.http.post as jest.Mock).mock.calls.length > 0 - ); + await waitFor(() => (useKibanaMock().services.http.post as jest.Mock).mock.calls.length > 0); - expect(kibana.services.http.post).toHaveBeenNthCalledWith( + expect(useKibanaMock().services.http.post).toHaveBeenNthCalledWith( 1, `${epmRouteService.getInstallPath('security_detection_engine', '8.2.1')}`, expect.objectContaining({ query: { prerelease: true } }) ); - expect(kibana.services.http.post).toHaveBeenNthCalledWith( + expect(useKibanaMock().services.http.post).toHaveBeenNthCalledWith( 2, `${epmRouteService.getBulkInstallPath()}`, expect.objectContaining({ diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_upgrade_security_packages.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_upgrade_security_packages.ts new file mode 100644 index 0000000000000..296d041002f21 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_upgrade_security_packages.ts @@ -0,0 +1,95 @@ +/* + * 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 { useIsMutating } from '@tanstack/react-query'; +import { useEffect } from 'react'; +import { PREBUILT_RULES_PACKAGE_NAME } from '../../../../common/detection_engine/constants'; +import { useUserPrivileges } from '../../../common/components/user_privileges'; +import { KibanaServices, useKibana } from '../../../common/lib/kibana'; +import type { BulkInstallFleetPackagesProps, InstallFleetPackageProps } from '../api/api'; +import { + BULK_INSTALL_FLEET_PACKAGES_MUTATION_KEY, + useBulkInstallFleetPackagesMutation, +} from '../api/hooks/use_bulk_install_fleet_packages_mutation'; +import { + INSTALL_FLEET_PACKAGE_MUTATION_KEY, + useInstallFleetPackageMutation, +} from '../api/hooks/use_install_fleet_package_mutation'; + +/** + * Install or upgrade the security packages (endpoint and prebuilt rules) + */ +export const useUpgradeSecurityPackages = () => { + const context = useKibana(); + const canAccessFleet = useUserPrivileges().endpointPrivileges.canAccessFleet; + const { mutate: bulkInstallFleetPackages } = useBulkInstallFleetPackagesMutation(); + const { mutate: installFleetPackage } = useInstallFleetPackageMutation(); + + useEffect(() => { + if (!canAccessFleet) { + return; + } + + (async () => { + // Make sure fleet is initialized first + await context.services.fleet?.isInitialized(); + + // Always install the latest package if in dev env or snapshot build + const prerelease = + KibanaServices.getKibanaVersion().includes('-SNAPSHOT') || + KibanaServices.getKibanaBranch() === 'main'; + + const prebuiltRulesPackageVersion = KibanaServices.getPrebuiltRulesPackageVersion(); + // ignore the response for now since we aren't notifying the user + const packages = ['endpoint', PREBUILT_RULES_PACKAGE_NAME]; + + // If `prebuiltRulesPackageVersion` is provided, try to install that version + // Must be done as two separate requests as bulk API doesn't support versions + if (prebuiltRulesPackageVersion != null) { + installFleetPackage({ + packageName: PREBUILT_RULES_PACKAGE_NAME, + packageVersion: prebuiltRulesPackageVersion, + prerelease, + force: true, + }); + packages.splice(packages.indexOf(PREBUILT_RULES_PACKAGE_NAME), 1); + } + + // Note: if `prerelease:true` option is provided, endpoint package will also be installed as prerelease + bulkInstallFleetPackages({ + packages, + prerelease, + }); + })(); + }, [bulkInstallFleetPackages, canAccessFleet, context.services.fleet, installFleetPackage]); +}; + +/** + * @returns true if the security packages are being installed or upgraded + */ +export const useIsUpgradingSecurityPackages = () => { + const isInstallingPackages = useIsMutating({ + predicate: ({ options }) => { + const { mutationKey, variables } = options; + + // The mutation is bulk Fleet packages installation. Check if the packages include the prebuilt rules package + if (mutationKey === BULK_INSTALL_FLEET_PACKAGES_MUTATION_KEY) { + return (variables as BulkInstallFleetPackagesProps).packages.includes( + PREBUILT_RULES_PACKAGE_NAME + ); + } + + // The mutation is single Fleet package installation. Check if the package is the prebuilt rules package + if (mutationKey === INSTALL_FLEET_PACKAGE_MUTATION_KEY) { + return (variables as InstallFleetPackageProps).packageName === PREBUILT_RULES_PACKAGE_NAME; + } + return false; + }, + }); + + return isInstallingPackages > 0; +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_tables.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_tables.tsx index bbe5659e7481b..31ae3af97f0ae 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_tables.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_tables.tsx @@ -40,6 +40,7 @@ import { useStartMlJobs } from '../../../rule_management/logic/use_start_ml_jobs import { RULES_TABLE_PAGE_SIZE_OPTIONS } from './constants'; import { useRuleManagementFilters } from '../../../rule_management/logic/use_rule_management_filters'; import type { FindRulesSortField } from '../../../../../common/detection_engine/rule_management'; +import { useIsUpgradingSecurityPackages } from '../../../rule_management/logic/use_upgrade_security_packages'; const INITIAL_SORT_FIELD = 'enabled'; @@ -63,6 +64,7 @@ const NO_ITEMS_MESSAGE = ( export const RulesTables = React.memo(({ selectedTab }) => { const [{ canUserCRUD }] = useUserData(); const hasPermissions = hasUserCRUDPermission(canUserCRUD); + const isUpgradingSecurityPackages = useIsUpgradingSecurityPackages(); const tableRef = useRef(null); const rulesTableContext = useRulesTableContext(); @@ -227,7 +229,7 @@ export const RulesTables = React.memo(({ selectedTab }) => { } : { 'data-test-subj': 'monitoring-table', columns: monitoringColumns }; - const shouldShowLinearProgress = isFetched && isRefetching; + const shouldShowLinearProgress = (isFetched && isRefetching) || isUpgradingSecurityPackages; const shouldShowLoadingOverlay = (!isFetched && isRefetching) || isPreflightInProgress; return ( diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_prepackaged_rules.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_prepackaged_rules.tsx index e5d05d7e7fbb0..9ec37ecfcb7c0 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_prepackaged_rules.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_prepackaged_rules.tsx @@ -11,6 +11,7 @@ import { useBoolState } from '../../../../common/hooks/use_bool_state'; import { RULES_TABLE_ACTIONS } from '../../../../common/lib/apm/user_actions'; import { useStartTransaction } from '../../../../common/lib/apm/use_start_transaction'; import { useCreatePrePackagedRules } from '../../../../detection_engine/rule_management/logic/use_create_pre_packaged_rules'; +import { useIsInstallingPrePackagedRules } from '../../../../detection_engine/rule_management/logic/use_install_pre_packaged_rules'; import { usePrePackagedRulesStatus } from '../../../../detection_engine/rule_management/logic/use_pre_packaged_rules_status'; import { affectedJobIds } from '../../callouts/ml_job_compatibility_callout/affected_job_ids'; import { MlJobUpgradeModal } from '../../modals/ml_job_upgrade_modal'; @@ -27,11 +28,8 @@ interface LoadPrePackagedRulesProps { export const LoadPrePackagedRules = ({ children }: LoadPrePackagedRulesProps) => { const { isFetching: isFetchingPrepackagedStatus } = usePrePackagedRulesStatus(); - const { - createPrePackagedRules, - canCreatePrePackagedRules, - isLoading: loadingCreatePrePackagedRules, - } = useCreatePrePackagedRules(); + const isInstallingPrebuiltRules = useIsInstallingPrePackagedRules(); + const { createPrePackagedRules, canCreatePrePackagedRules } = useCreatePrePackagedRules(); const { startTransaction } = useStartTransaction(); const handleCreatePrePackagedRules = useCallback(async () => { @@ -63,7 +61,7 @@ export const LoadPrePackagedRules = ({ children }: LoadPrePackagedRulesProps) => return ( <> {children({ - isLoading: loadingCreatePrePackagedRules, + isLoading: isInstallingPrebuiltRules, isDisabled, onClick: handleInstallPrePackagedRules, })}