diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table.test.tsx
new file mode 100644
index 0000000000000..4be2547598b5d
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table.test.tsx
@@ -0,0 +1,306 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import { AddPrebuiltRulesTable } from './add_prebuilt_rules_table';
+import { AddPrebuiltRulesHeaderButtons } from './add_prebuilt_rules_header_buttons';
+import { AddPrebuiltRulesTableContextProvider } from './add_prebuilt_rules_table_context';
+
+import { useUserData } from '../../../../../detections/components/user_info';
+import { usePrebuiltRulesInstallReview } from '../../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_install_review';
+import { useFetchPrebuiltRulesStatusQuery } from '../../../../rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_status_query';
+import { useIsUpgradingSecurityPackages } from '../../../../rule_management/logic/use_upgrade_security_packages';
+
+// Mock components not needed in this test suite
+jest.mock('../../../../rule_management/components/rule_details/rule_details_flyout', () => ({
+ RuleDetailsFlyout: jest.fn().mockReturnValue(<>>),
+}));
+jest.mock('../rules_changelog_link', () => ({
+ RulesChangelogLink: jest.fn().mockReturnValue(<>>),
+}));
+jest.mock('./add_prebuilt_rules_table_filters', () => ({
+ AddPrebuiltRulesTableFilters: jest.fn().mockReturnValue(<>>),
+}));
+
+jest.mock('../../../../rule_management/logic/prebuilt_rules/use_perform_rule_install', () => ({
+ usePerformInstallAllRules: () => ({
+ performInstallAll: jest.fn(),
+ isLoading: false,
+ }),
+ usePerformInstallSpecificRules: () => ({
+ performInstallSpecific: jest.fn(),
+ isLoading: false,
+ }),
+}));
+
+jest.mock('../../../../../common/lib/kibana', () => ({
+ useUiSetting$: jest.fn().mockReturnValue([false]),
+ useKibana: jest.fn().mockReturnValue({
+ services: {
+ docLinks: { links: { siem: { ruleChangeLog: '' } } },
+ },
+ }),
+}));
+
+jest.mock('../../../../../common/components/links', () => ({
+ useGetSecuritySolutionLinkProps: () =>
+ jest.fn().mockReturnValue({
+ onClick: jest.fn(),
+ }),
+}));
+
+jest.mock(
+ '../../../../rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_status_query',
+ () => ({
+ useFetchPrebuiltRulesStatusQuery: jest.fn().mockReturnValue({
+ data: {
+ prebuiltRulesStatus: {
+ num_prebuilt_rules_total_in_package: 1,
+ },
+ },
+ }),
+ })
+);
+
+jest.mock('../../../../rule_management/logic/use_upgrade_security_packages', () => ({
+ useIsUpgradingSecurityPackages: jest.fn().mockImplementation(() => false),
+}));
+
+jest.mock(
+ '../../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_install_review',
+ () => ({
+ usePrebuiltRulesInstallReview: jest.fn().mockReturnValue({
+ data: {
+ rules: [
+ {
+ id: 'rule-1',
+ name: 'rule-1',
+ tags: [],
+ risk_score: 1,
+ severity: 'low',
+ },
+ ],
+ stats: {
+ num_rules_to_install: 1,
+ tags: [],
+ },
+ },
+ isLoading: false,
+ isFetched: true,
+ }),
+ })
+);
+
+jest.mock('../../../../../detections/components/user_info', () => ({
+ useUserData: jest.fn(),
+}));
+
+describe('AddPrebuiltRulesTable', () => {
+ it('disables `Install all` button if user has no write permissions', async () => {
+ (useUserData as jest.Mock).mockReturnValue([
+ {
+ loading: false,
+ canUserCRUD: false,
+ },
+ ]);
+
+ render(
+
+
+
+
+ );
+
+ const installAllButton = screen.getByTestId('installAllRulesButton');
+
+ expect(installAllButton).toHaveTextContent('Install all');
+ expect(installAllButton).toBeDisabled();
+ });
+
+ it('disables `Install all` button if prebuilt package is being installed', async () => {
+ (useUserData as jest.Mock).mockReturnValue([
+ {
+ loading: false,
+ canUserCRUD: true,
+ },
+ ]);
+
+ (useIsUpgradingSecurityPackages as jest.Mock).mockReturnValueOnce(true);
+
+ render(
+
+
+
+
+ );
+
+ const installAllButton = screen.getByTestId('installAllRulesButton');
+
+ expect(installAllButton).toHaveTextContent('Install all');
+ expect(installAllButton).toBeDisabled();
+ });
+
+ it('enables Install all` button when user has permissions', async () => {
+ (useUserData as jest.Mock).mockReturnValue([
+ {
+ loading: false,
+ canUserCRUD: true,
+ },
+ ]);
+
+ render(
+
+
+
+
+ );
+
+ const installAllButton = screen.getByTestId('installAllRulesButton');
+
+ expect(installAllButton).toHaveTextContent('Install all');
+ expect(installAllButton).toBeEnabled();
+ });
+
+ it.each([
+ ['Security:Read', true],
+ ['Security:Write', false],
+ ])(
+ `renders "No rules available for install" when there are no rules to install and user has %s`,
+ async (_permissions, canUserCRUD) => {
+ (useUserData as jest.Mock).mockReturnValue([
+ {
+ loading: false,
+ canUserCRUD,
+ },
+ ]);
+
+ (usePrebuiltRulesInstallReview as jest.Mock).mockReturnValueOnce({
+ data: {
+ rules: [],
+ stats: {
+ num_rules_to_install: 0,
+ tags: [],
+ },
+ },
+ isLoading: false,
+ isFetched: true,
+ });
+ (useFetchPrebuiltRulesStatusQuery as jest.Mock).mockReturnValueOnce({
+ data: {
+ prebuiltRulesStatus: {
+ num_prebuilt_rules_total_in_package: 0,
+ },
+ },
+ });
+
+ const { findByText } = render(
+
+
+
+ );
+
+ expect(await findByText('All Elastic rules have been installed')).toBeInTheDocument();
+ }
+ );
+
+ it('does not render `Install rule` on rule rows for users with no write permissions', async () => {
+ (useUserData as jest.Mock).mockReturnValue([
+ {
+ loading: false,
+ canUserCRUD: false,
+ },
+ ]);
+
+ const id = 'rule-1';
+ (usePrebuiltRulesInstallReview as jest.Mock).mockReturnValueOnce({
+ data: {
+ rules: [
+ {
+ id,
+ rule_id: id,
+ name: 'rule-1',
+ tags: [],
+ risk_score: 1,
+ severity: 'low',
+ },
+ ],
+ stats: {
+ num_rules_to_install: 1,
+ tags: [],
+ },
+ },
+ isLoading: false,
+ isFetched: true,
+ });
+ (useFetchPrebuiltRulesStatusQuery as jest.Mock).mockReturnValueOnce({
+ data: {
+ prebuiltRulesStatus: {
+ num_prebuilt_rules_total_in_package: 1,
+ },
+ },
+ });
+
+ render(
+
+
+
+ );
+
+ const installRuleButton = screen.queryByTestId(`installSinglePrebuiltRuleButton-${id}`);
+
+ expect(installRuleButton).not.toBeInTheDocument();
+ });
+
+ it('renders `Install rule` on rule rows for users with write permissions', async () => {
+ (useUserData as jest.Mock).mockReturnValue([
+ {
+ loading: false,
+ canUserCRUD: true,
+ },
+ ]);
+
+ const id = 'rule-1';
+ (usePrebuiltRulesInstallReview as jest.Mock).mockReturnValueOnce({
+ data: {
+ rules: [
+ {
+ id,
+ rule_id: id,
+ name: 'rule-1',
+ tags: [],
+ risk_score: 1,
+ severity: 'low',
+ },
+ ],
+ stats: {
+ num_rules_to_install: 1,
+ tags: [],
+ },
+ },
+ isLoading: false,
+ isFetched: true,
+ });
+ (useFetchPrebuiltRulesStatusQuery as jest.Mock).mockReturnValueOnce({
+ data: {
+ prebuiltRulesStatus: {
+ num_prebuilt_rules_total_in_package: 1,
+ },
+ },
+ });
+
+ render(
+
+
+
+ );
+
+ const installRuleButton = screen.queryByTestId(`installSinglePrebuiltRuleButton-${id}`);
+
+ expect(installRuleButton).toBeInTheDocument();
+ expect(installRuleButton).toBeEnabled();
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx
index f13c8130bf740..ac89ff017c78a 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx
@@ -24,6 +24,7 @@ import { useRuleDetailsFlyout } from '../../../../rule_management/components/rul
import type { RuleResponse } from '../../../../../../common/api/detection_engine/model/rule_schema';
import { RuleDetailsFlyout } from '../../../../rule_management/components/rule_details/rule_details_flyout';
import * as i18n from './translations';
+import { isUpgradeReviewRequestEnabled } from './add_prebuilt_rules_utils';
export interface AddPrebuiltRulesTableState {
/**
@@ -125,11 +126,11 @@ export const AddPrebuiltRulesTableContextProvider = ({
refetchInterval: 60000, // Refetch available rules for installation every minute
keepPreviousData: true, // Use this option so that the state doesn't jump between "success" and "loading" on page change
// Fetch rules to install only after background installation of security_detection_rules package is complete
- enabled: Boolean(
- !isUpgradingSecurityPackages &&
- prebuiltRulesStatus &&
- prebuiltRulesStatus.num_prebuilt_rules_total_in_package > 0
- ),
+ enabled: isUpgradeReviewRequestEnabled({
+ canUserCRUD,
+ isUpgradingSecurityPackages,
+ prebuiltRulesStatus,
+ }),
});
const { mutateAsync: installAllRulesRequest } = usePerformInstallAllRules();
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_utils.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_utils.ts
new file mode 100644
index 0000000000000..fa032f1b32f6d
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_utils.ts
@@ -0,0 +1,34 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type { PrebuiltRulesStatusStats } from '../../../../../../common/api/detection_engine';
+
+interface UpgradeReviewEnabledProps {
+ canUserCRUD: boolean | null;
+ isUpgradingSecurityPackages: boolean;
+ prebuiltRulesStatus?: PrebuiltRulesStatusStats;
+}
+
+export const isUpgradeReviewRequestEnabled = ({
+ canUserCRUD,
+ isUpgradingSecurityPackages,
+ prebuiltRulesStatus,
+}: UpgradeReviewEnabledProps) => {
+ // Wait until security package is updated
+ if (isUpgradingSecurityPackages) {
+ return false;
+ }
+
+ // If user is read-only, allow request to proceed even though the Prebuilt
+ // Rules might not be installed. For these users, the Fleet endpoint quickly
+ // fails with 403 so isUpgradingSecurityPackages is false
+ if (canUserCRUD === false) {
+ return true;
+ }
+
+ return prebuiltRulesStatus && prebuiltRulesStatus.num_prebuilt_rules_total_in_package > 0;
+};