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)); +};