diff --git a/x-pack/solutions/security/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/prebuilt_rule_deprecation.md b/x-pack/solutions/security/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/prebuilt_rule_deprecation.md
index effad99d116df..00f87ece00706 100644
--- a/x-pack/solutions/security/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/prebuilt_rule_deprecation.md
+++ b/x-pack/solutions/security/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/prebuilt_rule_deprecation.md
@@ -45,7 +45,7 @@ https://marketplace.visualstudio.com/items?itemName=yzhang.markdown-all-in-one
- [Deprecation review: with ids filter](#deprecation-review-with-ids-filter)
- [**Scenario: Review filters by installed rule SO ids**](#scenario-review-filters-by-installed-rule-so-ids)
- [**Scenario: Review returns empty when filtered rule is not deprecated**](#scenario-review-returns-empty-when-filtered-rule-is-not-deprecated)
- - [**Scenario: Review returns empty when filtered id does not exist**](#scenario-review-returns-empty-when-filtered-id-does-not-exist)
+ - [**Scenario: Review returns 400 when filtered id does not exist**](#scenario-review-returns-400-when-filtered-id-does-not-exist)
- [Deprecation review: edge cases](#deprecation-review-edge-cases)
- [**Scenario: Review respects MAX\_DEPRECATED\_RULES\_TO\_RETURN limit**](#scenario-review-respects-max_deprecated_rules_to_return-limit)
- [**Scenario: Review handles package with no deprecated rules**](#scenario-review-handles-package-with-no-deprecated-rules)
@@ -239,14 +239,14 @@ When the user requests the deprecation review filtered to rule D
Then the response contains an empty rules array
```
-#### **Scenario: Review returns empty when filtered id does not exist**
+#### **Scenario: Review returns 400 when filtered id does not exist**
**Automation**: API integration tests.
```Gherkin
Given a non-existent rule SO id
When the user requests the deprecation review filtered to the non-existent id
-Then the response contains an empty rules array
+Then the endpoint returns a 400 error with message "No rules found for bulk get"
```
### Deprecation review: edge cases
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_deprecation/deprecated_rules_callout.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_deprecation/deprecated_rules_callout.test.tsx
new file mode 100644
index 0000000000000..cb58bbb77e7c5
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_deprecation/deprecated_rules_callout.test.tsx
@@ -0,0 +1,53 @@
+/*
+ * 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 { DeprecatedRulesCallout } from './deprecated_rules_callout';
+
+describe('DeprecatedRulesCallout', () => {
+ const defaultProps = {
+ title: 'Test Callout Title',
+ description: 'Test callout description',
+ buttons: [
+ ,
+ ],
+ };
+
+ it('renders the title', () => {
+ render();
+ expect(screen.getByText('Test Callout Title')).toBeInTheDocument();
+ });
+
+ it('renders the description', () => {
+ render();
+ expect(screen.getByTestId('deprecated-rule-callout-description')).toHaveTextContent(
+ 'Test callout description'
+ );
+ });
+
+ it('renders the provided buttons', () => {
+ render();
+ expect(screen.getByText('Do Action')).toBeInTheDocument();
+ });
+
+ it('displays deprecation reason when provided', () => {
+ render();
+
+ const reasonEl = screen.getByTestId('deprecated-rule-reason');
+ expect(reasonEl).toBeInTheDocument();
+ expect(reasonEl).toHaveTextContent('Replaced by rule XYZ');
+ });
+
+ it('does not display deprecation reason section when reason is absent', () => {
+ render();
+
+ expect(screen.queryByTestId('deprecated-rule-reason')).not.toBeInTheDocument();
+ });
+});
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_deprecation/deprecated_rules_modal.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_deprecation/deprecated_rules_modal.test.tsx
new file mode 100644
index 0000000000000..1beb227142edd
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_deprecation/deprecated_rules_modal.test.tsx
@@ -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 React from 'react';
+import { act, render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { useUserPrivileges } from '../../../../common/components/user_privileges';
+import { useExecuteBulkAction } from '../../logic/bulk_actions/use_execute_bulk_action';
+import { DeprecatedRulesModal } from './deprecated_rules_modal';
+import type { DeprecatedRuleForReview } from '../../../../../common/api/detection_engine/prebuilt_rules';
+
+jest.mock('../../../../common/components/user_privileges');
+jest.mock('../../logic/bulk_actions/use_execute_bulk_action');
+
+const mockUseUserPrivileges = useUserPrivileges as jest.Mock;
+const mockUseExecuteBulkAction = useExecuteBulkAction as jest.Mock;
+
+// Simplified RuleLink component for testing.
+jest.mock('../../../rule_management_ui/components/rules_table/use_columns', () => ({
+ RuleLink: ({ name, id }: { name: string; id: string }) => (
+
+ {name}
+
+ ),
+}));
+
+const mockExecuteBulkAction = jest.fn();
+const mockOnClose = jest.fn();
+
+const MOCK_RULES: DeprecatedRuleForReview[] = [
+ { id: 'rule-so-id-1', rule_id: 'rule-rule-id-1', name: 'Deprecated Rule A' },
+ { id: 'rule-so-id-2', rule_id: 'rule-rule-id-2', name: 'Deprecated Rule B' },
+];
+
+describe('DeprecatedRulesModal', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+
+ mockUseUserPrivileges.mockReturnValue({
+ rulesPrivileges: {
+ rules: { edit: true },
+ exceptions: { edit: true },
+ },
+ });
+
+ mockUseExecuteBulkAction.mockReturnValue({ executeBulkAction: mockExecuteBulkAction });
+ });
+
+ describe('Delete all button is disabled for read-only users', () => {
+ it('disables the delete-all button when user cannot edit rules', () => {
+ mockUseUserPrivileges.mockReturnValue({
+ rulesPrivileges: {
+ rules: { edit: false },
+ exceptions: { edit: false },
+ },
+ });
+
+ render();
+
+ expect(screen.getByTestId('deprecated-rules-modal-delete-all')).toBeDisabled();
+ });
+
+ it('enables the delete-all button when user can edit rules', () => {
+ render();
+
+ expect(screen.getByTestId('deprecated-rules-modal-delete-all')).not.toBeDisabled();
+ });
+ });
+
+ describe('Cancel bulk delete confirmation modal', () => {
+ it('does not delete rules when the confirmation is cancelled', async () => {
+ render();
+
+ // Open the confirm modal
+ await act(async () => {
+ await userEvent.click(screen.getByTestId('deprecated-rules-modal-delete-all'));
+ });
+
+ expect(screen.getByTestId('deprecated-rules-delete-confirm-modal')).toBeInTheDocument();
+
+ // Click the cancel button inside the confirmation modal
+ await act(async () => {
+ await userEvent.click(screen.getByTestId('confirmModalCancelButton'));
+ });
+
+ // The confirm modal should be dismissed and no deletion should have been triggered
+ expect(screen.queryByTestId('deprecated-rules-delete-confirm-modal')).not.toBeInTheDocument();
+ expect(mockExecuteBulkAction).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_deprecation/use_deprecated_rule_details_callout.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_deprecation/use_deprecated_rule_details_callout.test.tsx
new file mode 100644
index 0000000000000..ef6b6253ddd23
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_deprecation/use_deprecated_rule_details_callout.test.tsx
@@ -0,0 +1,221 @@
+/*
+ * 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 { act, render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import type { RuleResponse } from '../../../../../common/api/detection_engine';
+import { BulkActionTypeEnum } from '../../../../../common/api/detection_engine/rule_management';
+import { DuplicateOptions } from '../../../../../common/detection_engine/rule_management/constants';
+import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
+import { useUserPrivileges } from '../../../../common/components/user_privileges';
+import { useExecuteBulkAction } from '../../logic/bulk_actions/use_execute_bulk_action';
+import { usePrebuiltRulesDeprecationReview } from '../../logic/prebuilt_rules/use_prebuilt_rules_deprecation_review';
+import { useKibana } from '../../../../common/lib/kibana';
+import { savedRuleMock } from '../../logic/mock';
+import { useDeprecatedRuleDetailsCallout } from './use_deprecated_rule_details_callout';
+import { createDefaultExternalRuleSource } from '../../../../../server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/create_default_external_rule_source';
+
+jest.mock('../../../../common/hooks/use_experimental_features');
+jest.mock('../../../../common/components/user_privileges');
+jest.mock('../../logic/bulk_actions/use_execute_bulk_action');
+jest.mock('../../logic/prebuilt_rules/use_prebuilt_rules_deprecation_review');
+jest.mock('../../../../common/lib/kibana');
+
+const mockUseIsExperimentalFeatureEnabled = useIsExperimentalFeatureEnabled as jest.Mock;
+const mockUseUserPrivileges = useUserPrivileges as jest.Mock;
+const mockUseExecuteBulkAction = useExecuteBulkAction as jest.Mock;
+const mockUsePrebuiltRulesDeprecationReview = usePrebuiltRulesDeprecationReview as jest.Mock;
+const mockUseKibana = useKibana as jest.Mock;
+
+const RULE_ID = savedRuleMock.id;
+const RULE_RULE_ID = savedRuleMock.rule_id;
+
+// A prebuilt rule (external rule_source) for tests
+const mockPrebuiltRule: RuleResponse = {
+ ...savedRuleMock,
+ rule_source: createDefaultExternalRuleSource(),
+};
+
+const mockExecuteBulkAction = jest.fn();
+const mockNavigateToApp = jest.fn();
+const mockConfirmDeletion = jest.fn();
+const mockShowBulkDuplicateExceptionsConfirmation = jest.fn();
+
+/**
+ * Renders a wrapper component that calls the hook and renders the returned callout.
+ */
+function TestComponent({
+ rule = mockPrebuiltRule,
+ confirmDeletion = mockConfirmDeletion,
+ showBulkDuplicateExceptionsConfirmation = mockShowBulkDuplicateExceptionsConfirmation,
+}: Partial<{
+ rule: RuleResponse | null;
+ confirmDeletion: () => Promise;
+ showBulkDuplicateExceptionsConfirmation: () => Promise;
+}>) {
+ const callout = useDeprecatedRuleDetailsCallout({
+ rule,
+ confirmDeletion,
+ showBulkDuplicateExceptionsConfirmation,
+ });
+
+ return <>{callout}>;
+}
+
+describe('useDeprecatedRuleDetailsCallout', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+
+ mockUseIsExperimentalFeatureEnabled.mockReturnValue(true);
+
+ mockUseUserPrivileges.mockReturnValue({
+ rulesPrivileges: {
+ rules: { edit: true },
+ exceptions: { edit: true },
+ },
+ });
+
+ mockUseExecuteBulkAction.mockReturnValue({ executeBulkAction: mockExecuteBulkAction });
+
+ // We utilize the enabled flag in the react query call, this mimics that behavior
+ mockUsePrebuiltRulesDeprecationReview.mockImplementation((_request, options) => {
+ if (options?.enabled === false) {
+ return { data: undefined, isLoading: false };
+ }
+ return {
+ data: {
+ rules: [{ id: RULE_ID, rule_id: RULE_RULE_ID, name: savedRuleMock.name }],
+ },
+ isLoading: false,
+ };
+ });
+
+ mockUseKibana.mockReturnValue({
+ services: {
+ application: { navigateToApp: mockNavigateToApp },
+ },
+ });
+ });
+
+ describe('Original rule is not deleted if duplication fails', () => {
+ it('does not call delete when duplicate bulk action returns no created rules', async () => {
+ mockExecuteBulkAction.mockResolvedValue({
+ attributes: { results: { created: [] } },
+ });
+ mockShowBulkDuplicateExceptionsConfirmation.mockResolvedValue(
+ DuplicateOptions.withoutExceptions
+ );
+
+ render();
+
+ const duplicateButton = screen.getByTestId('deprecated-rule-duplicate-and-delete-button');
+ await act(async () => {
+ await userEvent.click(duplicateButton);
+ });
+
+ expect(mockExecuteBulkAction).toHaveBeenCalledTimes(1);
+ expect(mockExecuteBulkAction).toHaveBeenCalledWith(
+ expect.objectContaining({ type: BulkActionTypeEnum.duplicate })
+ );
+ expect(mockExecuteBulkAction).not.toHaveBeenCalledWith(
+ expect.objectContaining({ type: BulkActionTypeEnum.delete })
+ );
+ });
+
+ it('does not call delete when duplicate bulk action returns undefined', async () => {
+ mockExecuteBulkAction.mockResolvedValue(undefined);
+ mockShowBulkDuplicateExceptionsConfirmation.mockResolvedValue(
+ DuplicateOptions.withoutExceptions
+ );
+
+ render();
+
+ const duplicateButton = screen.getByTestId('deprecated-rule-duplicate-and-delete-button');
+ await act(async () => {
+ await userEvent.click(duplicateButton);
+ });
+
+ expect(mockExecuteBulkAction).toHaveBeenCalledTimes(1);
+ expect(mockExecuteBulkAction).not.toHaveBeenCalledWith(
+ expect.objectContaining({ type: BulkActionTypeEnum.delete })
+ );
+ });
+ });
+
+ describe('Action buttons are disabled for read-only users', () => {
+ beforeEach(() => {
+ mockUseUserPrivileges.mockReturnValue({
+ rulesPrivileges: {
+ rules: { edit: false },
+ exceptions: { edit: false },
+ },
+ });
+ });
+
+ it('disables both delete and duplicate-and-delete buttons when user cannot edit rules', () => {
+ render();
+
+ expect(screen.getByTestId('deprecated-rule-delete-button')).toBeDisabled();
+ expect(screen.getByTestId('deprecated-rule-duplicate-and-delete-button')).toBeDisabled();
+ });
+ });
+
+ describe('Cancel duplicate-and-delete flow', () => {
+ it('does not duplicate or delete when exceptions confirmation is cancelled', async () => {
+ mockShowBulkDuplicateExceptionsConfirmation.mockResolvedValue(null);
+
+ render();
+
+ const duplicateButton = screen.getByTestId('deprecated-rule-duplicate-and-delete-button');
+ await act(async () => {
+ await userEvent.click(duplicateButton);
+ });
+
+ expect(mockExecuteBulkAction).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('Cancel delete flow', () => {
+ it('does not delete when deletion confirmation returns false', async () => {
+ mockConfirmDeletion.mockResolvedValue(false);
+
+ render();
+
+ const deleteButton = screen.getByTestId('deprecated-rule-delete-button');
+ await act(async () => {
+ await userEvent.click(deleteButton);
+ });
+
+ expect(mockExecuteBulkAction).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('Returns null when callout should not show', () => {
+ it('returns null when rule is null', () => {
+ const { container } = render();
+
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it('returns null when rule is a custom rule (not external)', () => {
+ const customRule: RuleResponse = { ...savedRuleMock };
+
+ const { container } = render();
+
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it('returns null while deprecation data is loading', () => {
+ mockUsePrebuiltRulesDeprecationReview.mockReturnValue({ data: undefined, isLoading: true });
+
+ const { container } = render();
+
+ expect(container).toBeEmptyDOMElement();
+ });
+ });
+});
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_deprecation/use_deprecated_rules_table_callout.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_deprecation/use_deprecated_rules_table_callout.tsx
index f7274e4e4a8fc..8326216f085d7 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_deprecation/use_deprecated_rules_table_callout.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_deprecation/use_deprecated_rules_table_callout.tsx
@@ -19,7 +19,7 @@ import { DeprecatedRulesCallout } from './deprecated_rules_callout';
import { DeprecatedRulesModal } from './deprecated_rules_modal';
import * as i18n from './translations';
-const DISMISSAL_STORAGE_KEY = 'securitySolution.deprecatedRulesCallout.dismissedAt';
+export const DISMISSAL_STORAGE_KEY = 'securitySolution.deprecatedRulesCallout.dismissedAt';
export const useDeprecatedRulesTableCallout = () => {
const isFeatureEnabled = useIsExperimentalFeatureEnabled('prebuiltRulesDeprecationUIEnabled');
diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client/methods/fetch_deprecated_rules.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client/methods/fetch_deprecated_rules.test.ts
new file mode 100644
index 0000000000000..56f1a3faa88cc
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client/methods/fetch_deprecated_rules.test.ts
@@ -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 { savedObjectsClientMock } from '@kbn/core-saved-objects-api-server-mocks';
+import { MAX_DEPRECATED_RULES_TO_RETURN } from '../../../../../../../../common/api/detection_engine/prebuilt_rules';
+import { getDeprecatedPrebuiltRuleMock } from '../../../../model/rule_assets/deprecated_prebuilt_rule_asset.mock';
+import { PREBUILT_RULE_ASSETS_SO_TYPE } from '../../prebuilt_rule_assets_type';
+import { fetchDeprecatedRules } from './fetch_deprecated_rules';
+
+describe('fetchDeprecatedRules', () => {
+ it('passes MAX_DEPRECATED_RULES_TO_RETURN as perPage so the limit is enforced at the query level', async () => {
+ const soClient = savedObjectsClientMock.create();
+ soClient.find.mockResolvedValue({ saved_objects: [], total: 0, per_page: 0, page: 1 });
+
+ await fetchDeprecatedRules(soClient);
+
+ expect(soClient.find).toHaveBeenCalledWith(
+ expect.objectContaining({ perPage: MAX_DEPRECATED_RULES_TO_RETURN })
+ );
+ });
+
+ it('returns validated deprecated rule assets from the saved objects result', async () => {
+ const soClient = savedObjectsClientMock.create();
+ const mockAsset = getDeprecatedPrebuiltRuleMock({ rule_id: 'rule-1', version: 2 });
+
+ soClient.find.mockResolvedValue({
+ saved_objects: [
+ {
+ id: 'so-id-1',
+ type: PREBUILT_RULE_ASSETS_SO_TYPE,
+ references: [],
+ attributes: mockAsset,
+ score: 0,
+ },
+ ],
+ total: 1,
+ per_page: MAX_DEPRECATED_RULES_TO_RETURN,
+ page: 1,
+ });
+
+ const result = await fetchDeprecatedRules(soClient);
+
+ expect(result).toHaveLength(1);
+ expect(result[0]).toMatchObject({ rule_id: 'rule-1', version: 2, deprecated: true });
+ });
+
+ it('scopes the query to the provided rule_ids when given', async () => {
+ const soClient = savedObjectsClientMock.create();
+ soClient.find.mockResolvedValue({ saved_objects: [], total: 0, per_page: 0, page: 1 });
+
+ await fetchDeprecatedRules(soClient, ['rule-a', 'rule-b']);
+
+ const callArgs = soClient.find.mock.calls[0][0];
+ expect(callArgs.filter).toContain('rule-a');
+ expect(callArgs.filter).toContain('rule-b');
+ });
+
+ it('returns empty array immediately when given an empty rule_ids list', async () => {
+ const soClient = savedObjectsClientMock.create();
+
+ const result = await fetchDeprecatedRules(soClient, []);
+
+ expect(result).toEqual([]);
+ expect(soClient.find).not.toHaveBeenCalled();
+ });
+});
diff --git a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/deprecation/deprecation_review.ts b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/deprecation/deprecation_review.ts
new file mode 100644
index 0000000000000..3d9927ae723de
--- /dev/null
+++ b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/deprecation/deprecation_review.ts
@@ -0,0 +1,245 @@
+/*
+ * 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 expect from 'expect';
+import { deleteAllRules } from '@kbn/detections-response-ftr-services';
+import { REVIEW_RULE_DEPRECATION_URL } from '@kbn/security-solution-plugin/common/api/detection_engine/prebuilt_rules';
+import type { FtrProviderContext } from '../../../../../../ftr_provider_context';
+import {
+ createPrebuiltRuleAssetSavedObjects,
+ createRuleAssetSavedObject,
+ createDeprecatedPrebuiltRuleAssetSavedObjects,
+ deleteAllPrebuiltRuleAssets,
+ installPrebuiltRules,
+ reviewRuleDeprecation,
+} from '../../../../utils';
+
+export default ({ getService }: FtrProviderContext): void => {
+ const supertest = getService('supertest');
+ const supertestWithoutAuth = getService('supertestWithoutAuth');
+ const es = getService('es');
+ const log = getService('log');
+ const security = getService('security');
+
+ describe('@ess @serverless @skipInServerlessMKI Review rule deprecation endpoint', () => {
+ beforeEach(async () => {
+ await deleteAllPrebuiltRuleAssets(es, log);
+ await deleteAllRules(supertest, log);
+ });
+
+ describe('No ids filter (returns all installed deprecated rules)', () => {
+ it('returns empty rules array when no deprecated rule assets exist', async () => {
+ const response = await reviewRuleDeprecation(es, supertest);
+ expect(response.rules).toEqual([]);
+ });
+
+ it('returns empty when deprecated assets exist but no matching rules are installed', async () => {
+ await createDeprecatedPrebuiltRuleAssetSavedObjects(es, [
+ { rule_id: 'deprecated-rule-1', version: 2 },
+ ]);
+
+ const response = await reviewRuleDeprecation(es, supertest);
+ expect(response.rules).toEqual([]);
+ });
+
+ it('returns only installed rules that have deprecated assets (intersection)', async () => {
+ // Install rule-a and rule-b
+ await createPrebuiltRuleAssetSavedObjects(es, [
+ createRuleAssetSavedObject({ rule_id: 'rule-a', version: 1, name: 'Rule A' }),
+ createRuleAssetSavedObject({ rule_id: 'rule-b', version: 1, name: 'Rule B' }),
+ ]);
+ await installPrebuiltRules(es, supertest);
+
+ // Deprecate rule-a (installed) and rule-c (not installed)
+ await createDeprecatedPrebuiltRuleAssetSavedObjects(es, [
+ { rule_id: 'rule-a', version: 2 },
+ { rule_id: 'rule-c', version: 1 },
+ ]);
+
+ const response = await reviewRuleDeprecation(es, supertest);
+
+ expect(response.rules).toHaveLength(1);
+ expect(response.rules[0]).toMatchObject({ rule_id: 'rule-a' });
+ });
+
+ it('includes deprecated_reason when present on the deprecated asset', async () => {
+ await createPrebuiltRuleAssetSavedObjects(es, [
+ createRuleAssetSavedObject({ rule_id: 'rule-1', version: 1 }),
+ ]);
+ await installPrebuiltRules(es, supertest);
+
+ await createDeprecatedPrebuiltRuleAssetSavedObjects(es, [
+ { rule_id: 'rule-1', version: 2, deprecated_reason: 'Replaced by rule-2' },
+ ]);
+
+ const response = await reviewRuleDeprecation(es, supertest);
+
+ expect(response.rules).toHaveLength(1);
+ expect(response.rules[0].deprecated_reason).toBe('Replaced by rule-2');
+ });
+
+ it('does not include deprecated_reason when absent from the deprecated asset', async () => {
+ await createPrebuiltRuleAssetSavedObjects(es, [
+ createRuleAssetSavedObject({ rule_id: 'rule-1', version: 1 }),
+ ]);
+ await installPrebuiltRules(es, supertest);
+
+ await createDeprecatedPrebuiltRuleAssetSavedObjects(es, [
+ { rule_id: 'rule-1', version: 2 },
+ ]);
+
+ const response = await reviewRuleDeprecation(es, supertest);
+
+ expect(response.rules).toHaveLength(1);
+ expect(response.rules[0]).not.toHaveProperty('deprecated_reason');
+ });
+
+ it('returns the installed rule name, not the deprecated asset name', async () => {
+ const customizedName = 'My Customized Rule Name';
+
+ await createPrebuiltRuleAssetSavedObjects(es, [
+ createRuleAssetSavedObject({ rule_id: 'rule-1', version: 1, name: 'Original Name' }),
+ ]);
+ await installPrebuiltRules(es, supertest);
+
+ // Rename the installed rule to simulate a user customization
+ await supertest
+ .patch('/api/detection_engine/rules')
+ .set('kbn-xsrf', 'true')
+ .send({ rule_id: 'rule-1', name: customizedName })
+ .expect(200);
+
+ // Add a deprecated asset with a different name
+ await createDeprecatedPrebuiltRuleAssetSavedObjects(es, [
+ { rule_id: 'rule-1', version: 2, name: 'Package Stub Name' },
+ ]);
+
+ const response = await reviewRuleDeprecation(es, supertest);
+
+ expect(response.rules).toHaveLength(1);
+ expect(response.rules[0].name).toBe(customizedName);
+ });
+
+ it('returns the installed SO id, not the deprecated asset id', async () => {
+ await createPrebuiltRuleAssetSavedObjects(es, [
+ createRuleAssetSavedObject({ rule_id: 'rule-1', version: 1 }),
+ ]);
+ const installResult = await installPrebuiltRules(es, supertest);
+ const installedRuleId = installResult.results.created[0].id;
+
+ await createDeprecatedPrebuiltRuleAssetSavedObjects(es, [
+ { rule_id: 'rule-1', version: 2 },
+ ]);
+
+ const response = await reviewRuleDeprecation(es, supertest);
+
+ expect(response.rules).toHaveLength(1);
+ expect(response.rules[0].id).toBe(installedRuleId);
+ });
+
+ it('returns empty when package has no deprecated rule assets', async () => {
+ await createPrebuiltRuleAssetSavedObjects(es, [
+ createRuleAssetSavedObject({ rule_id: 'rule-1', version: 1 }),
+ ]);
+ await installPrebuiltRules(es, supertest);
+
+ const response = await reviewRuleDeprecation(es, supertest);
+ expect(response.rules).toEqual([]);
+ });
+ });
+
+ describe('With ids filter', () => {
+ it('returns only the rule matching the provided SO id', async () => {
+ await createPrebuiltRuleAssetSavedObjects(es, [
+ createRuleAssetSavedObject({ rule_id: 'rule-a', version: 1 }),
+ createRuleAssetSavedObject({ rule_id: 'rule-b', version: 1 }),
+ ]);
+ const installResult = await installPrebuiltRules(es, supertest);
+ const ruleAId = installResult.results.created.find((r) => r.rule_id === 'rule-a')?.id;
+
+ // Deprecate both rules
+ await createDeprecatedPrebuiltRuleAssetSavedObjects(es, [
+ { rule_id: 'rule-a', version: 2 },
+ { rule_id: 'rule-b', version: 2 },
+ ]);
+
+ // Filter to only rule-a
+ const response = await reviewRuleDeprecation(es, supertest, { ids: [ruleAId!] });
+
+ expect(response.rules).toHaveLength(1);
+ expect(response.rules[0].id).toBe(ruleAId);
+ expect(response.rules[0].rule_id).toBe('rule-a');
+ });
+
+ it('returns empty when the filtered rule is not deprecated', async () => {
+ await createPrebuiltRuleAssetSavedObjects(es, [
+ createRuleAssetSavedObject({ rule_id: 'rule-1', version: 1 }),
+ ]);
+ const installResult = await installPrebuiltRules(es, supertest);
+ const installedRuleId = installResult.results.created[0].id;
+
+ // No deprecated asset for rule-1
+ const response = await reviewRuleDeprecation(es, supertest, {
+ ids: [installedRuleId],
+ });
+
+ expect(response.rules).toEqual([]);
+ });
+
+ it('returns 400 when the filtered id does not exist', async () => {
+ const response = await reviewRuleDeprecation(
+ es,
+ supertest,
+ { ids: ['non-existent-so-id'] },
+ 400
+ );
+
+ expect(response).toMatchObject({
+ message: 'No rules found for bulk get',
+ status_code: 400,
+ });
+ });
+ });
+
+ describe('Authorization', () => {
+ const roleName = 'no_kibana_privileges_deprecation_test';
+ let noPrivilegesUser: { username: string; password: string };
+
+ before(async () => {
+ noPrivilegesUser = { username: 'test_no_privileges_user', password: 'changeme' };
+
+ // Create a role with no Kibana privileges so the user cannot access any Security Solution route
+ await security.role.create(roleName, {
+ elasticsearch: { cluster: [], indices: [], run_as: [] },
+ kibana: [],
+ });
+
+ await security.user.create(noPrivilegesUser.username, {
+ password: noPrivilegesUser.password,
+ roles: [roleName],
+ full_name: 'No Privileges User',
+ });
+ });
+
+ after(async () => {
+ await security.user.delete(noPrivilegesUser.username);
+ await security.role.delete(roleName);
+ });
+
+ it('returns 403 Forbidden when user lacks rules read privileges', async () => {
+ await supertestWithoutAuth
+ .post(REVIEW_RULE_DEPRECATION_URL)
+ .set('kbn-xsrf', 'true')
+ .set('elastic-api-version', '1')
+ .set('x-elastic-internal-origin', 'foo')
+ .auth(noPrivilegesUser.username, noPrivilegesUser.password)
+ .send(null)
+ .expect(403);
+ });
+ });
+ });
+};
diff --git a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/deprecation/index.ts b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/deprecation/index.ts
new file mode 100644
index 0000000000000..22a8bb3af908c
--- /dev/null
+++ b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/deprecation/index.ts
@@ -0,0 +1,12 @@
+/*
+ * 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 { FtrProviderContext } from '../../../../../../ftr_provider_context';
+
+export default ({ loadTestFile }: FtrProviderContext): void => {
+ loadTestFile(require.resolve('./deprecation_review'));
+};
diff --git a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/index.ts b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/index.ts
index 5bfa96fcc6f2c..802650399e66e 100644
--- a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/index.ts
+++ b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/index.ts
@@ -16,5 +16,6 @@ export default ({ loadTestFile }: FtrProviderContext): void => {
loadTestFile(require.resolve('./non_customizable_fields'));
loadTestFile(require.resolve('./revert_prebuilt_rules'));
loadTestFile(require.resolve('./status'));
+ loadTestFile(require.resolve('./deprecation'));
});
};
diff --git a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/install_prebuilt_rules/install_mocked_prebuilt_rule_assets.ts b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/install_prebuilt_rules/install_mocked_prebuilt_rule_assets.ts
index b13ded7859f44..0c0b78bcc83d1 100644
--- a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/install_prebuilt_rules/install_mocked_prebuilt_rule_assets.ts
+++ b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/install_prebuilt_rules/install_mocked_prebuilt_rule_assets.ts
@@ -13,12 +13,14 @@ import {
deleteAllPrebuiltRuleAssets,
createRuleAssetSavedObject,
createPrebuiltRuleAssetSavedObjects,
+ createDeprecatedPrebuiltRuleAssetSavedObjects,
installPrebuiltRulesAndTimelines,
getPrebuiltRulesAndTimelinesStatus,
createHistoricalPrebuiltRuleAssetSavedObjects,
getPrebuiltRulesStatus,
installPrebuiltRules,
getInstalledRules,
+ reviewPrebuiltRulesToInstall,
} from '../../../../utils';
export default ({ getService }: FtrProviderContext): void => {
@@ -294,6 +296,49 @@ export default ({ getService }: FtrProviderContext): void => {
]);
});
+ describe('Deprecated rule exclusion', () => {
+ it('does not install deprecated rule assets when installing all rules', async () => {
+ await createPrebuiltRuleAssetSavedObjects(es, [
+ createRuleAssetSavedObject({ rule_id: 'active-rule-1', version: 1 }),
+ createRuleAssetSavedObject({ rule_id: 'active-rule-2', version: 1 }),
+ ]);
+ await createDeprecatedPrebuiltRuleAssetSavedObjects(es, [
+ { rule_id: 'deprecated-rule-1', version: 1 },
+ ]);
+
+ const body = await installPrebuiltRules(es, supertest);
+
+ const installedRuleIds = body.results.created.map((r) => r.rule_id);
+ expect(installedRuleIds).toContain('active-rule-1');
+ expect(installedRuleIds).toContain('active-rule-2');
+ expect(installedRuleIds).not.toContain('deprecated-rule-1');
+ expect(body.summary.succeeded).toBe(2);
+ });
+
+ it('installs zero rules when only deprecated rule assets are present', async () => {
+ await createDeprecatedPrebuiltRuleAssetSavedObjects(es, [
+ { rule_id: 'deprecated-rule-1', version: 1 },
+ ]);
+
+ const body = await installPrebuiltRules(es, supertest);
+
+ expect(body.summary.succeeded).toBe(0);
+ expect(body.results.created).toHaveLength(0);
+ });
+
+ it('does not include deprecated rule assets in the install review after the bootstrap endpoint is called', async () => {
+ await createDeprecatedPrebuiltRuleAssetSavedObjects(es, [
+ { rule_id: 'deprecated-rule-1', version: 1 },
+ ]);
+
+ await detectionsApi.bootstrapPrebuiltRules().expect(200);
+
+ const response = await reviewPrebuiltRulesToInstall(supertest);
+ const ruleIds = response.rules.map((r: { rule_id: string }) => r.rule_id);
+ expect(ruleIds).not.toContain('deprecated-rule-1');
+ });
+ });
+
describe('legacy (PUT /api/detection_engine/rules/prepackaged)', () => {
it('should install prebuilt rules', async () => {
await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects());
diff --git a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/install_prebuilt_rules/review_installation.ts b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/install_prebuilt_rules/review_installation.ts
index 9fba0ce53a291..4be0820d85238 100644
--- a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/install_prebuilt_rules/review_installation.ts
+++ b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/install_prebuilt_rules/review_installation.ts
@@ -14,6 +14,7 @@ import type { FtrProviderContext } from '../../../../../../ftr_provider_context'
import {
createPrebuiltRuleAssetSavedObjects,
createRuleAssetSavedObject,
+ createDeprecatedPrebuiltRuleAssetSavedObjects,
deleteAllPrebuiltRuleAssets,
installPrebuiltRules,
reviewPrebuiltRulesToInstall,
@@ -991,5 +992,37 @@ export default ({ getService }: FtrProviderContext): void => {
expect(response.body.rules.length).toBe(1);
});
});
+
+ describe('Deprecated rule exclusion', () => {
+ it('does not include deprecated rule assets in the install review', async () => {
+ await createPrebuiltRuleAssetSavedObjects(es, [
+ createRuleAssetSavedObject({ rule_id: 'active-rule-1', version: 1, name: 'Active 1' }),
+ createRuleAssetSavedObject({ rule_id: 'active-rule-2', version: 1, name: 'Active 2' }),
+ ]);
+ await createDeprecatedPrebuiltRuleAssetSavedObjects(es, [
+ { rule_id: 'deprecated-rule-1', version: 1, name: 'Deprecated 1' },
+ ]);
+
+ const response = await reviewPrebuiltRulesToInstall(supertest);
+
+ const ruleIds = response.rules.map((r: { rule_id: string }) => r.rule_id);
+ expect(ruleIds).toContain('active-rule-1');
+ expect(ruleIds).toContain('active-rule-2');
+ expect(ruleIds).not.toContain('deprecated-rule-1');
+ expect(response.stats.num_rules_to_install).toBe(2);
+ });
+
+ it('returns empty install review when only deprecated rule assets are present', async () => {
+ await createDeprecatedPrebuiltRuleAssetSavedObjects(es, [
+ { rule_id: 'deprecated-rule-1', version: 1 },
+ { rule_id: 'deprecated-rule-2', version: 1 },
+ ]);
+
+ const response = await reviewPrebuiltRulesToInstall(supertest);
+
+ expect(response.rules).toHaveLength(0);
+ expect(response.stats.num_rules_to_install).toBe(0);
+ });
+ });
});
};
diff --git a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/status/get_prebuilt_rules_status.ts b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/status/get_prebuilt_rules_status.ts
index e6a50bbe52271..a2a5b47b1b6d7 100644
--- a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/status/get_prebuilt_rules_status.ts
+++ b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/status/get_prebuilt_rules_status.ts
@@ -14,6 +14,7 @@ import {
getSimpleRule,
createRuleAssetSavedObject,
createPrebuiltRuleAssetSavedObjects,
+ createDeprecatedPrebuiltRuleAssetSavedObjects,
installPrebuiltRules,
performUpgradePrebuiltRules,
createHistoricalPrebuiltRuleAssetSavedObjects,
@@ -286,6 +287,96 @@ export default ({ getService }: FtrProviderContext): void => {
});
});
});
+
+ describe('num_prebuilt_rules_deprecated', () => {
+ it('returns zero when no deprecated rule assets exist', async () => {
+ const { stats } = await getPrebuiltRulesStatus(es, supertest);
+ expect(stats.num_prebuilt_rules_deprecated).toBe(0);
+ });
+
+ it('returns zero when deprecated assets exist but no matching rules are installed', async () => {
+ await createDeprecatedPrebuiltRuleAssetSavedObjects(es, [
+ { rule_id: 'deprecated-rule-1', version: 2 },
+ { rule_id: 'deprecated-rule-2', version: 1 },
+ ]);
+
+ const { stats } = await getPrebuiltRulesStatus(es, supertest);
+ expect(stats.num_prebuilt_rules_deprecated).toBe(0);
+ });
+
+ it('returns correct count of installed deprecated rules', async () => {
+ await createPrebuiltRuleAssetSavedObjects(es, [
+ createRuleAssetSavedObject({ rule_id: 'rule-1', version: 1 }),
+ createRuleAssetSavedObject({ rule_id: 'rule-2', version: 1 }),
+ createRuleAssetSavedObject({ rule_id: 'rule-3', version: 1 }),
+ ]);
+ await installPrebuiltRules(es, supertest);
+
+ await createDeprecatedPrebuiltRuleAssetSavedObjects(es, [
+ { rule_id: 'rule-1', version: 2 },
+ { rule_id: 'rule-2', version: 2 },
+ ]);
+
+ const { stats } = await getPrebuiltRulesStatus(es, supertest);
+ expect(stats.num_prebuilt_rules_deprecated).toBe(2);
+ });
+
+ it('does not count deprecated assets for rules that are not installed', async () => {
+ await createPrebuiltRuleAssetSavedObjects(es, [
+ createRuleAssetSavedObject({ rule_id: 'rule-1', version: 1 }),
+ ]);
+ await installPrebuiltRules(es, supertest);
+
+ await createDeprecatedPrebuiltRuleAssetSavedObjects(es, [
+ { rule_id: 'rule-1', version: 2 },
+ { rule_id: 'rule-2', version: 1 },
+ ]);
+
+ const { stats } = await getPrebuiltRulesStatus(es, supertest);
+ expect(stats.num_prebuilt_rules_deprecated).toBe(1);
+ });
+
+ it('deprecated count decreases when a deprecated rule is deleted', async () => {
+ await createPrebuiltRuleAssetSavedObjects(es, [
+ createRuleAssetSavedObject({ rule_id: 'rule-1', version: 1 }),
+ createRuleAssetSavedObject({ rule_id: 'rule-2', version: 1 }),
+ ]);
+ await installPrebuiltRules(es, supertest);
+
+ await createDeprecatedPrebuiltRuleAssetSavedObjects(es, [
+ { rule_id: 'rule-1', version: 2 },
+ { rule_id: 'rule-2', version: 2 },
+ ]);
+
+ const { stats: statsBefore } = await getPrebuiltRulesStatus(es, supertest);
+ expect(statsBefore.num_prebuilt_rules_deprecated).toBe(2);
+
+ await deleteRule(supertest, 'rule-1');
+
+ const { stats: statsAfter } = await getPrebuiltRulesStatus(es, supertest);
+ expect(statsAfter.num_prebuilt_rules_deprecated).toBe(1);
+ });
+
+ it('deprecated rules do not appear in install or upgrade counts', async () => {
+ await createPrebuiltRuleAssetSavedObjects(es, [
+ createRuleAssetSavedObject({ rule_id: 'rule-1', version: 1 }),
+ ]);
+ await installPrebuiltRules(es, supertest);
+
+ await deleteAllPrebuiltRuleAssets(es, log);
+ await createPrebuiltRuleAssetSavedObjects(es, [
+ createRuleAssetSavedObject({ rule_id: 'rule-2', version: 1 }),
+ ]);
+ await createDeprecatedPrebuiltRuleAssetSavedObjects(es, [
+ { rule_id: 'rule-1', version: 2 },
+ ]);
+
+ const { stats } = await getPrebuiltRulesStatus(es, supertest);
+ expect(stats.num_prebuilt_rules_deprecated).toBe(1);
+ expect(stats.num_prebuilt_rules_to_install).toBe(1);
+ expect(stats.num_prebuilt_rules_to_upgrade).toBe(0);
+ });
+ });
});
describe('get_prebuilt_rules_status - legacy', () => {
diff --git a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/upgrade_prebuilt_rules/review_prebuilt_rules_upgrade.ts b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/upgrade_prebuilt_rules/review_prebuilt_rules_upgrade.ts
index 6e987a37bf0c3..710b50401902d 100644
--- a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/upgrade_prebuilt_rules/review_prebuilt_rules_upgrade.ts
+++ b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/upgrade_prebuilt_rules/review_prebuilt_rules_upgrade.ts
@@ -9,8 +9,13 @@ import expect from 'expect';
import { deleteAllRules } from '@kbn/detections-response-ftr-services';
import type { FtrProviderContext } from '../../../../../../ftr_provider_context';
import {
+ createPrebuiltRuleAssetSavedObjects,
+ createRuleAssetSavedObject,
+ createDeprecatedPrebuiltRuleAssetSavedObjects,
deleteAllPrebuiltRuleAssets,
fetchFirstPrebuiltRuleUpgradeReviewDiff,
+ installPrebuiltRules,
+ reviewPrebuiltRulesToUpgrade,
} from '../../../../utils';
import { setUpRuleUpgrade } from '../../../../utils/rules/prebuilt_rules/set_up_rule_upgrade';
@@ -289,5 +294,31 @@ export default ({ getService }: FtrProviderContext): void => {
}
);
}
+
+ describe('Deprecated rule exclusion', () => {
+ it('does not include deprecated rule assets in the upgrade review', async () => {
+ // Install rule-a and rule-b at version 1
+ await createPrebuiltRuleAssetSavedObjects(es, [
+ createRuleAssetSavedObject({ rule_id: 'rule-a', version: 1 }),
+ createRuleAssetSavedObject({ rule_id: 'rule-b', version: 1 }),
+ ]);
+ await installPrebuiltRules(es, supertest);
+
+ // Replace assets: active upgrade for rule-a, deprecated asset for rule-b
+ await deleteAllPrebuiltRuleAssets(es, log);
+ await createPrebuiltRuleAssetSavedObjects(es, [
+ createRuleAssetSavedObject({ rule_id: 'rule-a', version: 2 }),
+ ]);
+ await createDeprecatedPrebuiltRuleAssetSavedObjects(es, [
+ { rule_id: 'rule-b', version: 2 },
+ ]);
+
+ const response = await reviewPrebuiltRulesToUpgrade(supertest);
+
+ const ruleIds = response.rules.map((r: { rule_id: string }) => r.rule_id);
+ expect(ruleIds).toContain('rule-a');
+ expect(ruleIds).not.toContain('rule-b');
+ });
+ });
});
};
diff --git a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/create_deprecated_prebuilt_rule_saved_objects.ts b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/create_deprecated_prebuilt_rule_saved_objects.ts
new file mode 100644
index 0000000000000..94250cd557469
--- /dev/null
+++ b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/create_deprecated_prebuilt_rule_saved_objects.ts
@@ -0,0 +1,81 @@
+/*
+ * 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 { Client } from '@elastic/elasticsearch';
+import { SECURITY_SOLUTION_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server';
+
+interface DeprecatedRuleAssetParams {
+ rule_id: string;
+ version: number;
+ name?: string;
+ deprecated_reason?: string;
+}
+
+const ruleAssetSavedObjectESFields = {
+ type: 'security-rule',
+ references: [],
+ coreMigrationVersion: '8.6.0',
+ updated_at: '2022-11-01T12:56:39.717Z',
+ created_at: '2022-11-01T12:56:39.717Z',
+};
+
+/**
+ * Creates a minimal deprecated rule asset saved object.
+ * Deprecated rule assets have only the fields required by model version 3:
+ * rule_id, version, name, deprecated, and optionally deprecated_reason.
+ */
+export const createDeprecatedRuleAssetSavedObject = (params: DeprecatedRuleAssetParams) => ({
+ 'security-rule': {
+ rule_id: params.rule_id,
+ version: params.version,
+ name: params.name ?? `Deprecated Rule ${params.rule_id}`,
+ deprecated: true,
+ ...(params.deprecated_reason !== undefined && {
+ deprecated_reason: params.deprecated_reason,
+ }),
+ },
+ ...ruleAssetSavedObjectESFields,
+});
+
+/**
+ * Bulk-creates deprecated rule asset saved objects in Elasticsearch.
+ * These represent rules that have been deprecated in the detection rules package.
+ *
+ * @param es Elasticsearch client
+ * @param rules Array of deprecated rule parameters
+ */
+export const createDeprecatedPrebuiltRuleAssetSavedObjects = async (
+ es: Client,
+ rules: DeprecatedRuleAssetParams[]
+): Promise => {
+ if (rules.length === 0) {
+ return;
+ }
+
+ const soObjects = rules.map(createDeprecatedRuleAssetSavedObject);
+
+ const response = await es.bulk({
+ refresh: true,
+ operations: soObjects.flatMap((doc) => [
+ {
+ index: {
+ _index: SECURITY_SOLUTION_SAVED_OBJECT_INDEX,
+ _id: `security-rule:${doc['security-rule'].rule_id}_${doc['security-rule'].version}`,
+ },
+ },
+ doc,
+ ]),
+ });
+
+ if (response.errors) {
+ throw new Error(
+ `Unable to bulk create deprecated rule assets. Response items: ${JSON.stringify(
+ response.items
+ )}`
+ );
+ }
+};
diff --git a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/index.ts b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/index.ts
index 53bdc25753706..fb6cca8a225bb 100644
--- a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/index.ts
+++ b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/index.ts
@@ -5,6 +5,7 @@
* 2.0.
*/
export * from './create_prebuilt_rule_saved_objects';
+export * from './create_deprecated_prebuilt_rule_saved_objects';
export * from './customize_rule';
export * from './delete_all_prebuilt_rule_assets';
export * from './delete_all_timelines';
@@ -21,3 +22,4 @@ export * from './install_prebuilt_rules';
export * from './review_install_prebuilt_rules';
export * from './review_upgrade_prebuilt_rules';
export * from './perform_upgrade_prebuilt_rules';
+export * from './review_rule_deprecation';
diff --git a/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/review_rule_deprecation.ts b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/review_rule_deprecation.ts
new file mode 100644
index 0000000000000..9dcca7bc18bc3
--- /dev/null
+++ b/x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/review_rule_deprecation.ts
@@ -0,0 +1,42 @@
+/*
+ * 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 SuperTest from 'supertest';
+import type { Client } from '@elastic/elasticsearch';
+import {
+ REVIEW_RULE_DEPRECATION_URL,
+ type ReviewRuleDeprecationRequestBody,
+ type ReviewRuleDeprecationResponseBody,
+} from '@kbn/security-solution-plugin/common/api/detection_engine/prebuilt_rules';
+import { refreshSavedObjectIndices } from '../../refresh_index';
+
+/**
+ * Calls the POST /internal/prebuilt_rules/deprecation/_review endpoint.
+ *
+ * @param es Elasticsearch client (used to refresh indices before the call)
+ * @param supertest SuperTest instance
+ * @param body Optional request body (null or object with optional ids array)
+ * @param expectedStatusCode Expected HTTP status code (default 200)
+ */
+export const reviewRuleDeprecation = async (
+ es: Client,
+ supertest: SuperTest.Agent,
+ body?: ReviewRuleDeprecationRequestBody,
+ expectedStatusCode: number = 200
+): Promise => {
+ await refreshSavedObjectIndices(es);
+
+ const response = await supertest
+ .post(REVIEW_RULE_DEPRECATION_URL)
+ .set('kbn-xsrf', 'true')
+ .set('elastic-api-version', '1')
+ .set('x-elastic-internal-origin', 'foo')
+ .send(body ?? undefined)
+ .expect(expectedStatusCode);
+
+ return response.body;
+};
diff --git a/x-pack/solutions/security/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/deprecation/deprecated_rule_details_callout.cy.ts b/x-pack/solutions/security/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/deprecation/deprecated_rule_details_callout.cy.ts
new file mode 100644
index 0000000000000..b2c00554ed8be
--- /dev/null
+++ b/x-pack/solutions/security/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/deprecation/deprecated_rule_details_callout.cy.ts
@@ -0,0 +1,212 @@
+/*
+ * 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 { getCustomQueryRuleParams } from '../../../../../objects/rule';
+import {
+ createDeprecatedRuleAssetSavedObject,
+ createRuleAssetSavedObject,
+} from '../../../../../helpers/rules';
+import {
+ createAndInstallMockedPrebuiltRules,
+ createDeprecatedRuleAssets,
+ installMockPrebuiltRulesPackage,
+} from '../../../../../tasks/api_calls/prebuilt_rules';
+import { createRule } from '../../../../../tasks/api_calls/rules';
+import {
+ deleteAlertsAndRules,
+ deletePrebuiltRulesAssets,
+} from '../../../../../tasks/api_calls/common';
+import { login } from '../../../../../tasks/login';
+import { visitRuleDetailsPage } from '../../../../../tasks/rule_details';
+import {
+ CONFIRM_DELETE_RULE_BTN,
+ DUPLICATE_WITHOUT_EXCEPTIONS_OPTION,
+ CONFIRM_DUPLICATE_RULE,
+} from '../../../../../screens/alerts_detection_rules';
+import { assertSuccessToast } from '../../../../../screens/common/toast';
+import { RULES_MANAGEMENT_URL } from '../../../../../urls/rules_management';
+import {
+ DEPRECATED_RULE_DETAILS_CALLOUT,
+ DEPRECATED_RULE_DELETE_BUTTON,
+ DEPRECATED_RULE_DUPLICATE_AND_DELETE_BUTTON,
+ DEPRECATED_RULE_REASON,
+} from '../../../../../screens/deprecated_rules';
+
+const ACTIVE_RULE_ASSET = createRuleAssetSavedObject({
+ name: 'My prebuilt rule',
+ rule_id: 'my-prebuilt-rule',
+ version: 1,
+});
+
+const DEPRECATED_ASSET = createDeprecatedRuleAssetSavedObject({
+ rule_id: 'my-prebuilt-rule',
+ version: 2,
+ name: 'My prebuilt rule',
+});
+
+const DEPRECATED_ASSET_WITH_REASON = createDeprecatedRuleAssetSavedObject({
+ rule_id: 'my-prebuilt-rule-with-reason',
+ version: 2,
+ name: 'My prebuilt rule with reason',
+ deprecated_reason: 'Replaced by new rule XYZ',
+});
+
+const ACTIVE_RULE_ASSET_WITH_REASON = createRuleAssetSavedObject({
+ name: 'My prebuilt rule with reason',
+ rule_id: 'my-prebuilt-rule-with-reason',
+ version: 1,
+});
+
+const NON_DEPRECATED_RULE_ASSET = createRuleAssetSavedObject({
+ name: 'Non-deprecated prebuilt rule',
+ rule_id: 'non-deprecated-rule',
+ version: 1,
+});
+
+describe(
+ 'Deprecated rules - Rule Details page callout',
+ {
+ tags: ['@ess', '@skipInServerlessMKI'],
+ env: {
+ ftrConfig: {
+ kbnServerArgs: [
+ `--xpack.securitySolution.enableExperimental=${JSON.stringify([
+ 'prebuiltRulesDeprecationUIEnabled',
+ ])}`,
+ ],
+ },
+ },
+ },
+ () => {
+ before(() => {
+ installMockPrebuiltRulesPackage();
+ });
+
+ beforeEach(() => {
+ login();
+ deleteAlertsAndRules();
+ deletePrebuiltRulesAssets();
+ });
+
+ describe('Callout visibility', () => {
+ describe('deprecated prebuilt rule', () => {
+ let ruleId: string;
+
+ beforeEach(() => {
+ createAndInstallMockedPrebuiltRules([ACTIVE_RULE_ASSET]).then(({ body }) => {
+ ruleId = body.results.created[0].id;
+ createDeprecatedRuleAssets({ rules: [DEPRECATED_ASSET] });
+ });
+ });
+
+ it('shows the deprecation callout with action buttons', () => {
+ visitRuleDetailsPage(ruleId);
+ cy.get(DEPRECATED_RULE_DETAILS_CALLOUT).should('be.visible');
+ cy.get(DEPRECATED_RULE_DELETE_BUTTON).should('be.visible');
+ cy.get(DEPRECATED_RULE_DUPLICATE_AND_DELETE_BUTTON).should('be.visible');
+ });
+ });
+
+ describe('deprecated prebuilt rule with a deprecation reason', () => {
+ let ruleId: string;
+
+ beforeEach(() => {
+ createAndInstallMockedPrebuiltRules([ACTIVE_RULE_ASSET_WITH_REASON]).then(({ body }) => {
+ ruleId = body.results.created[0].id;
+ createDeprecatedRuleAssets({ rules: [DEPRECATED_ASSET_WITH_REASON] });
+ });
+ });
+
+ it('displays the deprecation reason', () => {
+ visitRuleDetailsPage(ruleId);
+ cy.get(DEPRECATED_RULE_REASON).should('be.visible');
+ cy.get(DEPRECATED_RULE_REASON).should('contain.text', 'Replaced by new rule XYZ');
+ });
+ });
+
+ describe('non-deprecated prebuilt rule', () => {
+ let ruleId: string;
+
+ beforeEach(() => {
+ createAndInstallMockedPrebuiltRules([NON_DEPRECATED_RULE_ASSET]).then(({ body }) => {
+ ruleId = body.results.created[0].id;
+ });
+ });
+
+ it('does not show the callout', () => {
+ visitRuleDetailsPage(ruleId);
+ cy.get(DEPRECATED_RULE_DETAILS_CALLOUT).should('not.exist');
+ });
+ });
+
+ describe('custom rule', () => {
+ let ruleId: string;
+
+ beforeEach(() => {
+ createRule(
+ getCustomQueryRuleParams({ rule_id: 'custom-rule', name: 'My custom rule' })
+ ).then(({ body }) => {
+ ruleId = body.id;
+ });
+ });
+
+ it('does not show the callout', () => {
+ visitRuleDetailsPage(ruleId);
+ cy.get(DEPRECATED_RULE_DETAILS_CALLOUT).should('not.exist');
+ });
+ });
+ });
+
+ describe('Delete deprecated rule', () => {
+ let ruleId: string;
+
+ beforeEach(() => {
+ createAndInstallMockedPrebuiltRules([ACTIVE_RULE_ASSET]).then(({ body }) => {
+ ruleId = body.results.created[0].id;
+ createDeprecatedRuleAssets({ rules: [DEPRECATED_ASSET] });
+ });
+ });
+
+ it('deletes a deprecated rule from its details page and navigates back to the rules list', () => {
+ visitRuleDetailsPage(ruleId);
+ cy.get(DEPRECATED_RULE_DELETE_BUTTON).click();
+ cy.get(CONFIRM_DELETE_RULE_BTN).click();
+
+ assertSuccessToast('Rules deleted', 'Successfully deleted 1 rule');
+ cy.url().should('include', RULES_MANAGEMENT_URL);
+ });
+ });
+
+ describe('Duplicate and delete deprecated rule', () => {
+ let ruleId: string;
+
+ beforeEach(() => {
+ createAndInstallMockedPrebuiltRules([ACTIVE_RULE_ASSET]).then(({ body }) => {
+ ruleId = body.results.created[0].id;
+ createDeprecatedRuleAssets({ rules: [DEPRECATED_ASSET] });
+ });
+ });
+
+ it('duplicates a deprecated rule as a custom rule and deletes the original', () => {
+ visitRuleDetailsPage(ruleId);
+ cy.get(DEPRECATED_RULE_DETAILS_CALLOUT).should('be.visible');
+
+ cy.location('pathname').then((originalPath) => {
+ cy.get(DEPRECATED_RULE_DUPLICATE_AND_DELETE_BUTTON).click();
+
+ cy.get(DUPLICATE_WITHOUT_EXCEPTIONS_OPTION).click();
+ cy.get(CONFIRM_DUPLICATE_RULE).click();
+
+ assertSuccessToast('Rules duplicated', 'Successfully duplicated 1 rule');
+
+ cy.location('pathname').should('not.eq', originalPath);
+ cy.location('pathname').should('match', /\/rules\/id\//);
+ });
+ });
+ });
+ }
+);
diff --git a/x-pack/solutions/security/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/deprecation/deprecated_rules_management_callout.cy.ts b/x-pack/solutions/security/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/deprecation/deprecated_rules_management_callout.cy.ts
new file mode 100644
index 0000000000000..1e44b7bdbb2ef
--- /dev/null
+++ b/x-pack/solutions/security/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/deprecation/deprecated_rules_management_callout.cy.ts
@@ -0,0 +1,174 @@
+/*
+ * 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 {
+ createDeprecatedRuleAssetSavedObject,
+ createRuleAssetSavedObject,
+} from '../../../../../helpers/rules';
+import {
+ createAndInstallMockedPrebuiltRules,
+ createDeprecatedRuleAssets,
+ installMockPrebuiltRulesPackage,
+} from '../../../../../tasks/api_calls/prebuilt_rules';
+import {
+ deleteAlertsAndRules,
+ deletePrebuiltRulesAssets,
+} from '../../../../../tasks/api_calls/common';
+import { login } from '../../../../../tasks/login';
+import { visitRulesManagementTable } from '../../../../../tasks/rules_management';
+import {
+ DEPRECATED_RULES_TABLE_CALLOUT,
+ DEPRECATED_RULES_TABLE_VIEW_BUTTON,
+ DEPRECATED_RULES_TABLE_DELETE_BUTTON,
+ DEPRECATED_RULES_TABLE_CALLOUT_DISMISS_BUTTON,
+ DEPRECATED_RULES_MODAL,
+ DEPRECATED_RULES_MODAL_CLOSE,
+ DEPRECATED_RULES_MODAL_DELETE_ALL,
+ DEPRECATED_RULES_DELETE_CONFIRM_MODAL,
+ DEPRECATED_RULES_DELETE_CONFIRM_MODAL_CONFIRM_BUTTON,
+} from '../../../../../screens/deprecated_rules';
+
+const DISMISSAL_STORAGE_KEY = 'securitySolution.deprecatedRulesCallout.dismissedAt';
+
+const ACTIVE_RULE = createRuleAssetSavedObject({
+ name: 'My prebuilt rule',
+ rule_id: 'my-prebuilt-rule',
+ version: 1,
+});
+
+const DEPRECATED_ASSET = createDeprecatedRuleAssetSavedObject({
+ rule_id: 'my-prebuilt-rule',
+ version: 2,
+ name: 'My prebuilt rule',
+});
+
+describe(
+ 'Deprecated rules - Rule Management page callout',
+ {
+ tags: ['@ess', '@skipInServerlessMKI'],
+ env: {
+ ftrConfig: {
+ kbnServerArgs: [
+ `--xpack.securitySolution.enableExperimental=${JSON.stringify([
+ 'prebuiltRulesDeprecationUIEnabled',
+ ])}`,
+ ],
+ },
+ },
+ },
+ () => {
+ before(() => {
+ installMockPrebuiltRulesPackage();
+ });
+
+ beforeEach(() => {
+ deleteAlertsAndRules();
+ deletePrebuiltRulesAssets();
+
+ // Clear any callout dismissal state
+ cy.clearLocalStorage(DISMISSAL_STORAGE_KEY);
+ });
+
+ describe('Callout visibility', () => {
+ it('displays the callout when the user has installed deprecated rules', () => {
+ createAndInstallMockedPrebuiltRules([ACTIVE_RULE]);
+ createDeprecatedRuleAssets({ rules: [DEPRECATED_ASSET] });
+
+ login();
+ visitRulesManagementTable();
+
+ cy.get(DEPRECATED_RULES_TABLE_CALLOUT).should('be.visible');
+ cy.get(DEPRECATED_RULES_TABLE_VIEW_BUTTON).should('be.visible');
+ cy.get(DEPRECATED_RULES_TABLE_DELETE_BUTTON).should('be.visible');
+ });
+
+ it('does not display the callout when no deprecated rules are installed', () => {
+ // Install a rule but do NOT add a deprecated asset for it
+ createAndInstallMockedPrebuiltRules([ACTIVE_RULE]);
+
+ login();
+ visitRulesManagementTable();
+
+ cy.get(DEPRECATED_RULES_TABLE_CALLOUT).should('not.exist');
+ });
+ });
+
+ describe('Callout dismissal', () => {
+ beforeEach(() => {
+ createAndInstallMockedPrebuiltRules([ACTIVE_RULE]);
+ createDeprecatedRuleAssets({ rules: [DEPRECATED_ASSET] });
+ login();
+ visitRulesManagementTable();
+ });
+
+ it('dismisses the callout and keeps it hidden after a page refresh', () => {
+ cy.get(DEPRECATED_RULES_TABLE_CALLOUT).should('be.visible');
+
+ // Dismiss the callout via the EuiCallOut dismiss (X) button
+ cy.get(DEPRECATED_RULES_TABLE_CALLOUT_DISMISS_BUTTON).click();
+ cy.get(DEPRECATED_RULES_TABLE_CALLOUT).should('not.exist');
+
+ // Reload the page — callout should remain hidden
+ visitRulesManagementTable();
+ cy.get(DEPRECATED_RULES_TABLE_CALLOUT).should('not.exist');
+ });
+
+ it('shows the callout again after 7 days have passed since dismissal', () => {
+ // Dismiss the callout first
+ cy.get(DEPRECATED_RULES_TABLE_CALLOUT_DISMISS_BUTTON).click();
+ cy.get(DEPRECATED_RULES_TABLE_CALLOUT).should('not.exist');
+
+ // Simulate 8 days having passed by backdating the dismissal timestamp
+ cy.window().then((win) => {
+ const eightDaysAgo = Date.now() - 8 * 24 * 60 * 60 * 1000;
+ win.localStorage.setItem(DISMISSAL_STORAGE_KEY, String(eightDaysAgo));
+ });
+
+ // Reload — callout should now reappear
+ visitRulesManagementTable();
+ cy.get(DEPRECATED_RULES_TABLE_CALLOUT).should('be.visible');
+ });
+ });
+
+ describe('Deprecated rules modal', () => {
+ beforeEach(() => {
+ createAndInstallMockedPrebuiltRules([ACTIVE_RULE]);
+ createDeprecatedRuleAssets({ rules: [DEPRECATED_ASSET] });
+ login();
+ visitRulesManagementTable();
+
+ // Open the modal
+ cy.get(DEPRECATED_RULES_TABLE_VIEW_BUTTON).click();
+ cy.get(DEPRECATED_RULES_MODAL).should('be.visible');
+ });
+
+ it('lists all installed deprecated rules with links to their details pages', () => {
+ cy.get(DEPRECATED_RULES_MODAL).within(() => {
+ // Description should include the count
+ cy.contains('1 deprecated rule').should('be.visible');
+ // Each rule name should be rendered as a clickable link
+ cy.contains('a', 'My prebuilt rule').should('be.visible');
+ });
+ });
+
+ it('closes the modal when the close button is clicked', () => {
+ cy.get(DEPRECATED_RULES_MODAL_CLOSE).click();
+ cy.get(DEPRECATED_RULES_MODAL).should('not.exist');
+ });
+
+ it('deletes all deprecated rules when the delete all button is clicked and confirmed', () => {
+ cy.get(DEPRECATED_RULES_MODAL_DELETE_ALL).click();
+ cy.get(DEPRECATED_RULES_DELETE_CONFIRM_MODAL).should('be.visible');
+ cy.get(DEPRECATED_RULES_DELETE_CONFIRM_MODAL_CONFIRM_BUTTON).click();
+
+ // Modal closes and callout disappears after successful deletion
+ cy.get(DEPRECATED_RULES_MODAL).should('not.exist');
+ cy.get(DEPRECATED_RULES_TABLE_CALLOUT).should('not.exist');
+ });
+ });
+ }
+);
diff --git a/x-pack/solutions/security/test/security_solution_cypress/cypress/helpers/rules.ts b/x-pack/solutions/security/test/security_solution_cypress/cypress/helpers/rules.ts
index 40b3b8e5798b4..3c2eaa6705e43 100644
--- a/x-pack/solutions/security/test/security_solution_cypress/cypress/helpers/rules.ts
+++ b/x-pack/solutions/security/test/security_solution_cypress/cypress/helpers/rules.ts
@@ -96,3 +96,36 @@ export const createRuleAssetSavedObject = (overrideParams: Partial ({
+ 'security-rule': {
+ rule_id,
+ version,
+ name,
+ deprecated: true as const,
+ ...(deprecated_reason != null && { deprecated_reason }),
+ },
+ type: 'security-rule',
+ references: [],
+ coreMigrationVersion: '8.6.0',
+ updated_at: '2022-11-01T12:56:39.717Z',
+ created_at: '2022-11-01T12:56:39.717Z',
+});
diff --git a/x-pack/solutions/security/test/security_solution_cypress/cypress/screens/deprecated_rules.ts b/x-pack/solutions/security/test/security_solution_cypress/cypress/screens/deprecated_rules.ts
new file mode 100644
index 0000000000000..346be3194411f
--- /dev/null
+++ b/x-pack/solutions/security/test/security_solution_cypress/cypress/screens/deprecated_rules.ts
@@ -0,0 +1,36 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+// Rule Management page callout
+export const DEPRECATED_RULES_TABLE_CALLOUT = '[data-test-subj="deprecated-rules-table-callout"]';
+export const DEPRECATED_RULES_TABLE_CALLOUT_DISMISS_BUTTON =
+ '[data-test-subj="deprecated-rules-table-callout"] [data-test-subj="euiDismissCalloutButton"]';
+export const DEPRECATED_RULES_TABLE_VIEW_BUTTON =
+ '[data-test-subj="deprecated-rules-table-view-button"]';
+export const DEPRECATED_RULES_TABLE_DELETE_BUTTON =
+ '[data-test-subj="deprecated-rules-table-delete-button"]';
+
+// Deprecated rules modal (opened from the Rule Management callout)
+export const DEPRECATED_RULES_MODAL = '[data-test-subj="deprecated-rules-modal"]';
+export const DEPRECATED_RULES_MODAL_CLOSE = '[data-test-subj="deprecated-rules-modal-close"]';
+export const DEPRECATED_RULES_MODAL_DELETE_ALL =
+ '[data-test-subj="deprecated-rules-modal-delete-all"]';
+
+// Delete confirmation modal
+export const DEPRECATED_RULES_DELETE_CONFIRM_MODAL =
+ '[data-test-subj="deprecated-rules-delete-confirm-modal"]';
+export const DEPRECATED_RULES_DELETE_CONFIRM_MODAL_CONFIRM_BUTTON =
+ '[data-test-subj="deprecated-rules-delete-confirm-modal"] [data-test-subj="confirmModalConfirmButton"]';
+export const DEPRECATED_RULES_DELETE_CONFIRM_MODAL_CANCEL_BUTTON =
+ '[data-test-subj="deprecated-rules-delete-confirm-modal"] [data-test-subj="confirmModalCancelButton"]';
+
+// Rule Details page callout
+export const DEPRECATED_RULE_DETAILS_CALLOUT = '[data-test-subj="deprecated-rule-details-callout"]';
+export const DEPRECATED_RULE_DELETE_BUTTON = '[data-test-subj="deprecated-rule-delete-button"]';
+export const DEPRECATED_RULE_DUPLICATE_AND_DELETE_BUTTON =
+ '[data-test-subj="deprecated-rule-duplicate-and-delete-button"]';
+export const DEPRECATED_RULE_REASON = '[data-test-subj="deprecated-rule-reason"]';
diff --git a/x-pack/solutions/security/test/security_solution_cypress/cypress/tasks/api_calls/prebuilt_rules.ts b/x-pack/solutions/security/test/security_solution_cypress/cypress/tasks/api_calls/prebuilt_rules.ts
index 3ccd4190ed87c..03def596d1456 100644
--- a/x-pack/solutions/security/test/security_solution_cypress/cypress/tasks/api_calls/prebuilt_rules.ts
+++ b/x-pack/solutions/security/test/security_solution_cypress/cypress/tasks/api_calls/prebuilt_rules.ts
@@ -17,6 +17,7 @@ import {
} from '@kbn/security-solution-plugin/common/detection_engine/constants';
import type { PrePackagedRulesStatusResponse } from '@kbn/security-solution-plugin/public/detection_engine/rule_management/logic/types';
import { getPrebuiltRuleWithExceptionsMock } from '@kbn/security-solution-plugin/server/lib/detection_engine/prebuilt_rules/mocks';
+import type { createDeprecatedRuleAssetSavedObject } from '../../helpers/rules';
import { createRuleAssetSavedObject } from '../../helpers/rules';
import { IS_SERVERLESS } from '../../env_var_names_constants';
import { refreshSavedObjectIndices, rootRequest } from './common';
@@ -80,6 +81,21 @@ export const getInstalledPrebuiltRulesCount = () => {
return getPrebuiltRulesStatus().then(({ body }) => body.rules_installed);
};
+/**
+ * Builds an ndjson bulk-index request body from an array of rule asset objects.
+ * Each element must expose a `'security-rule'` key with at least `rule_id` and `version`.
+ */
+const buildBulkIndexBody = (
+ index: string,
+ rules: T[]
+): string =>
+ rules.reduce((body, rule) => {
+ const documentId = `security-rule:${rule['security-rule'].rule_id}_${rule['security-rule'].version}`;
+ return body.concat(
+ `${JSON.stringify({ index: { _index: index, _id: documentId } })}\n${JSON.stringify(rule)}\n`
+ );
+ }, '');
+
export const bulkCreateRuleAssets = ({
index = '.kibana_security_solution',
rules = [SAMPLE_PREBUILT_RULE],
@@ -92,23 +108,8 @@ export const bulkCreateRuleAssets = ({
rules?.map((rule) => rule['security-rule'].rule_id).join(', ')
);
- const bulkIndexRequestBody = rules.reduce((body, rule) => {
- const document = JSON.stringify(rule);
- const documentId = `security-rule:${rule['security-rule'].rule_id}`;
- const documentIdWithVersion = `${documentId}_${rule['security-rule'].version}`;
-
- const indexHistoricalRuleAsset = `${JSON.stringify({
- index: {
- _index: index,
- _id: documentIdWithVersion,
- },
- })}\n${document}\n`;
-
- return body.concat(indexHistoricalRuleAsset);
- }, '');
-
cy.task('putMapping', index);
- cy.task('bulkInsert', bulkIndexRequestBody);
+ cy.task('bulkInsert', buildBulkIndexBody(index, rules));
};
/* Prevent the installation of the `security_detection_engine` package from Fleet
@@ -234,3 +235,27 @@ const deleteFleetPackage = (
export const deletePrebuiltRulesFleetPackage = (): Cypress.Chainable> =>
deleteFleetPackage(PREBUILT_RULES_PACKAGE_NAME);
+
+/**
+ * Bulk create deprecated rule asset saved objects in ES.
+ * These are minimal stubs with `deprecated: true` that signal a rule has been deprecated.
+ * Use `createDeprecatedRuleAssetSavedObject()` to build each rule asset.
+ *
+ * Use in combination with `preventPrebuiltRulesPackageInstallation` so that only
+ * the mocked assets are present during the test.
+ */
+export const createDeprecatedRuleAssets = ({
+ index = '.kibana_security_solution',
+ rules,
+}: {
+ index?: string;
+ rules: Array>;
+}) => {
+ cy.log(
+ 'Bulk create deprecated rule assets',
+ rules.map((rule) => rule['security-rule'].rule_id).join(', ')
+ );
+
+ cy.task('putMapping', index);
+ cy.task('bulkInsert', buildBulkIndexBody(index, rules));
+};