diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/bootstrap_prebuilt_rules/bootstrap_prebuilt_rules.gen.ts b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/bootstrap_prebuilt_rules/bootstrap_prebuilt_rules.gen.ts index 764f3438d7b54..4b126dcaf1030 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/bootstrap_prebuilt_rules/bootstrap_prebuilt_rules.gen.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/bootstrap_prebuilt_rules/bootstrap_prebuilt_rules.gen.ts @@ -32,10 +32,61 @@ export const PackageInstallStatus = z.object({ status: z.enum(['installed', 'already_installed']), }); +export type RuleBootstrapError = z.infer; +export const RuleBootstrapError = z.object({ + /** + * The list of rules that failed to bootstrap + */ + rules: z.array( + z.object({ + /** + * The ID of the rule that failed to bootstrap + */ + rule_id: z.string(), + }) + ), + /** + * The error message + */ + message: z.string(), +}); + +export type RuleBootstrapResults = z.infer; +export const RuleBootstrapResults = z.object({ + /** + * The total number of rules to be processed. This is a dynamic value and depends on the number of integrations installed that have bootstrappable rules + */ + total: z.number().optional(), + /** + * The number of rules that were installed + */ + installed: z.number(), + /** + * The number of rules that were updated + */ + updated: z.number(), + /** + * The number of rules that were deleted + */ + deleted: z.number(), + /** + * The number of rules that were skipped (already installed rules with no updates) + */ + skipped: z.number().optional(), + /** + * The list of bootstrap errors + */ + errors: z.array(RuleBootstrapError), +}); + export type BootstrapPrebuiltRulesResponse = z.infer; export const BootstrapPrebuiltRulesResponse = z.object({ /** * The list of packages that were installed or upgraded */ packages: z.array(PackageInstallStatus), + /** + * The list of rules that were installed or upgraded + */ + rules: RuleBootstrapResults.optional(), }); diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/bootstrap_prebuilt_rules/bootstrap_prebuilt_rules.schema.yaml b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/bootstrap_prebuilt_rules/bootstrap_prebuilt_rules.schema.yaml index 92cb6ccaf2ad1..8cd5fffa8021d 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/bootstrap_prebuilt_rules/bootstrap_prebuilt_rules.schema.yaml +++ b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/bootstrap_prebuilt_rules/bootstrap_prebuilt_rules.schema.yaml @@ -25,6 +25,9 @@ paths: description: The list of packages that were installed or upgraded items: $ref: '#/components/schemas/PackageInstallStatus' + rules: + description: The list of rules that were installed or upgraded + $ref: '#/components/schemas/RuleBootstrapResults' required: - packages @@ -49,3 +52,53 @@ components: - name - version - status + + RuleBootstrapResults: + type: object + properties: + total: + type: number + description: The total number of rules to be processed. This is a dynamic value and depends on the number of integrations installed that have bootstrappable rules + installed: + type: number + description: The number of rules that were installed + updated: + type: number + description: The number of rules that were updated + deleted: + type: number + description: The number of rules that were deleted + skipped: + type: number + description: The number of rules that were skipped (already installed rules with no updates) + errors: + type: array + description: The list of bootstrap errors + items: + $ref: '#/components/schemas/RuleBootstrapError' + required: + - installed + - updated + - deleted + - errors + + RuleBootstrapError: + type: object + properties: + rules: + type: array + description: The list of rules that failed to bootstrap + items: + type: object + properties: + rule_id: + type: string + description: The ID of the rule that failed to bootstrap + required: + - rule_id + message: + type: string + description: The error message + required: + - rules + - message diff --git a/x-pack/solutions/security/plugins/security_solution/common/constants.ts b/x-pack/solutions/security/plugins/security_solution/common/constants.ts index f48f5d7dfae56..58de49f9620dd 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/constants.ts @@ -538,3 +538,8 @@ export const AI_FOR_SOC_INTEGRATIONS = [ 'sentinel_one', 'crowdstrike', ]; + +/* + * The tag to mark promotion rules that are related to the AI for SOC integrations + */ +export const PROMOTION_RULE_TAG = 'Promotion'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/lib/search_ai_lake/integrations.ts b/x-pack/solutions/security/plugins/security_solution/public/common/lib/search_ai_lake/integrations.ts deleted file mode 100644 index 64dbcd7dceb02..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/common/lib/search_ai_lake/integrations.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/** Allow list of integrations to be available in the AI4DSOC integrations page */ -export const SEARCH_AI_LAKE_ALLOWED_INTEGRATIONS: string[] = [ - 'crowdstrike', - 'google_secops', - 'microsoft_sentinel', - 'sentinel_one', - 'splunk', -]; diff --git a/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/integrations.tsx b/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/integrations.tsx index 2db09437b01a3..89b3b6a78e71c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/integrations.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/integrations.tsx @@ -10,8 +10,7 @@ import { EuiSkeletonLoading } from '@elastic/eui'; import type { AvailablePackagesHookType } from '@kbn/fleet-plugin/public'; import { Routes, Route } from '@kbn/shared-ux-router'; import { Redirect } from 'react-router-dom'; -import { CONFIGURATIONS_PATH } from '../../../common/constants'; -import { SEARCH_AI_LAKE_ALLOWED_INTEGRATIONS } from '../../common/lib/search_ai_lake/integrations'; +import { AI_FOR_SOC_INTEGRATIONS, CONFIGURATIONS_PATH } from '../../../common/constants'; import { useEnhancedIntegrationCards } from '../../common/lib/search_ai_lake/hooks'; import { ConfigurationTabs, IntegrationsFacets } from '../constants'; import { IntegrationsPage, IntegrationsSkeleton } from './integrations/components'; @@ -28,7 +27,7 @@ export const ConfigurationsIntegrationsHome = React.memo( }); const allowedIntegrations = filteredCards.filter((card) => - SEARCH_AI_LAKE_ALLOWED_INTEGRATIONS.includes(card.name) + AI_FOR_SOC_INTEGRATIONS.includes(card.name) ); const { available, installed } = useEnhancedIntegrationCards(allowedIntegrations); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_bootstrap_prebuilt_rules.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_bootstrap_prebuilt_rules.ts index 379e8c9326fec..82c9159f62c71 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_bootstrap_prebuilt_rules.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_bootstrap_prebuilt_rules.ts @@ -13,6 +13,7 @@ import { bootstrapPrebuiltRules } from '../../api'; import { useInvalidateFetchPrebuiltRulesInstallReviewQuery } from './use_fetch_prebuilt_rules_install_review_query'; import { useInvalidateFetchPrebuiltRulesStatusQuery } from './use_fetch_prebuilt_rules_status_query'; import { useInvalidateFetchPrebuiltRulesUpgradeReviewQuery } from './use_fetch_prebuilt_rules_upgrade_review_query'; +import { useInvalidateFindRulesQuery } from '../use_find_rules_query'; export const BOOTSTRAP_PREBUILT_RULES_KEY = ['POST', BOOTSTRAP_PREBUILT_RULES_URL]; @@ -22,16 +23,19 @@ export const useBootstrapPrebuiltRulesMutation = ( const invalidatePrePackagedRulesStatus = useInvalidateFetchPrebuiltRulesStatusQuery(); const invalidatePrebuiltRulesInstallReview = useInvalidateFetchPrebuiltRulesInstallReviewQuery(); const invalidatePrebuiltRulesUpdateReview = useInvalidateFetchPrebuiltRulesUpgradeReviewQuery(); + const invalidateFindRulesQuery = useInvalidateFindRulesQuery(); return useMutation(() => bootstrapPrebuiltRules(), { ...options, mutationKey: BOOTSTRAP_PREBUILT_RULES_KEY, onSuccess: (...args) => { const response = args[0]; - if ( - response?.packages.find((pkg) => pkg.name === PREBUILT_RULES_PACKAGE_NAME)?.status === - 'installed' - ) { + + // 'installed' means that the package was installed or updated to a newer version + const hasInstalledNewPackageVersion = + response.packages.find((pkg) => pkg.name === PREBUILT_RULES_PACKAGE_NAME)?.status === + 'installed'; + if (hasInstalledNewPackageVersion) { // Invalidate other pre-packaged rules related queries. We need to do // that only in case the prebuilt rules package was installed indicating // that there might be new rules to install. @@ -40,6 +44,12 @@ export const useBootstrapPrebuiltRulesMutation = ( invalidatePrebuiltRulesUpdateReview(); } + const hasRuleUpdates = + response.rules?.deleted || response.rules?.installed || response.rules?.updated; + if (hasRuleUpdates) { + invalidateFindRulesQuery(); + } + if (options?.onSuccess) { options.onSuccess(...args); } diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/translations.ts index 56ebc3e133775..4dc3f3f9614d0 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/translations.ts @@ -55,3 +55,10 @@ export const TIMELINE_PREPACKAGED_SUCCESS = i18n.translate( defaultMessage: 'Installed pre-packaged timeline templates from elastic', } ); + +export const BOOTSTRAP_PREBUILT_RULES_FAILURE = i18n.translate( + 'xpack.securitySolution.containers.detectionEngine.bootstrapPrebuiltRulesFailure', + { + defaultMessage: 'Failed to bootstrap prebuilt rules', + } +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/use_upgrade_security_packages.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/use_upgrade_security_packages.ts index 72246b085c40f..d8a6c2f850836 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/use_upgrade_security_packages.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/use_upgrade_security_packages.ts @@ -11,12 +11,27 @@ import { BOOTSTRAP_PREBUILT_RULES_KEY, useBootstrapPrebuiltRulesMutation, } from '../api/hooks/prebuilt_rules/use_bootstrap_prebuilt_rules'; +import { useAppToasts } from '../../../common/hooks/use_app_toasts'; +import * as i18n from './translations'; /** * Install or upgrade the security packages (endpoint and prebuilt rules) */ export const useUpgradeSecurityPackages = () => { - const { mutate: bootstrapPrebuiltRules } = useBootstrapPrebuiltRulesMutation(); + const { addError } = useAppToasts(); + + const { mutate: bootstrapPrebuiltRules } = useBootstrapPrebuiltRulesMutation({ + onError: (error) => { + addError(error, { title: i18n.BOOTSTRAP_PREBUILT_RULES_FAILURE }); + }, + onSuccess: ({ rules }) => { + if (rules?.errors.length) { + addError(new Error(rules.errors.map((error) => error.message).join('; ')), { + title: i18n.BOOTSTRAP_PREBUILT_RULES_FAILURE, + }); + } + }, + }); useEffect(() => { bootstrapPrebuiltRules(); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_fetch_integrations.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_fetch_integrations.ts index 604f99913ecc9..10b22916f848a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_fetch_integrations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_fetch_integrations.ts @@ -8,15 +8,7 @@ import { useMemo } from 'react'; import type { PackageListItem } from '@kbn/fleet-plugin/common'; import { installationStatuses, useGetPackagesQuery } from '@kbn/fleet-plugin/public'; - -// We hardcode these here for now as we currently do not have any other way to filter out all the unwanted integrations. -const AI_FOR_SOC_INTEGRATIONS = [ - 'splunk', // doesnt yet exist - 'google_secops', - 'microsoft_sentinel', - 'sentinel_one', - 'crowdstrike', -]; +import { AI_FOR_SOC_INTEGRATIONS } from '../../../../common/constants'; export interface UseFetchIntegrationsResult { /** diff --git a/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/fleet_integration.ts b/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/fleet_integration.ts index 88fa30b959791..82f5fb769a1c8 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/fleet_integration.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/fleet_integration/fleet_integration.ts @@ -47,7 +47,7 @@ import type { NewPolicyData, PolicyConfig } from '../../common/endpoint/types'; import type { LicenseService } from '../../common/license'; import type { ManifestManager } from '../endpoint/services'; import type { IRequestContextFactory } from '../request_context_factory'; -import { installEndpointSecurityPrebuiltRule } from '../lib/detection_engine/prebuilt_rules/logic/rules_package/install_endpoint_security_prebuilt_rule'; +import { installEndpointSecurityPrebuiltRule } from '../lib/detection_engine/prebuilt_rules/logic/integrations/install_endpoint_security_prebuilt_rule'; import { createPolicyArtifactManifest } from './handlers/create_policy_artifact_manifest'; import { createDefaultPolicy } from './handlers/create_default_policy'; import { validatePolicyAgainstLicense } from './handlers/validate_policy_against_license'; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/bootstrap_prebuilt_rules/bootstrap_prebuilt_rules.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/bootstrap_prebuilt_rules/bootstrap_prebuilt_rules.test.ts index c2982d4b16092..3043bf3a3182b 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/bootstrap_prebuilt_rules/bootstrap_prebuilt_rules.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/bootstrap_prebuilt_rules/bootstrap_prebuilt_rules.test.ts @@ -10,6 +10,7 @@ import { bootstrapPrebuiltRulesRoute } from './bootstrap_prebuilt_rules'; import type { Installation, RegistryPackage } from '@kbn/fleet-plugin/common'; import { requestContextMock, serverMock } from '../../../routes/__mocks__'; import { getBootstrapRulesRequest } from '../../../routes/__mocks__/request_responses'; +import { createProductFeaturesServiceMock } from '../../../../product_features_service/mocks'; const packageMock: RegistryPackage = { name: 'detection_engine', @@ -43,6 +44,7 @@ describe('bootstrap_prebuilt_rules_route', () => { jest.clearAllMocks(); server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); + clients.productFeaturesService = createProductFeaturesServiceMock([]); bootstrapPrebuiltRulesRoute(server.router); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/bootstrap_prebuilt_rules/bootstrap_prebuilt_rules_handler.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/bootstrap_prebuilt_rules/bootstrap_prebuilt_rules_handler.ts index d3bc06139821f..413f2ec6d5493 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/bootstrap_prebuilt_rules/bootstrap_prebuilt_rules_handler.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/bootstrap_prebuilt_rules/bootstrap_prebuilt_rules_handler.ts @@ -6,15 +6,21 @@ */ import type { IKibanaResponse, KibanaRequest, KibanaResponseFactory } from '@kbn/core/server'; +import { ProductFeatureSecurityKey } from '@kbn/security-solution-features/keys'; import { transformError } from '@kbn/securitysolution-es-utils'; -import type { BootstrapPrebuiltRulesResponse } from '../../../../../../common/api/detection_engine/prebuilt_rules/bootstrap_prebuilt_rules/bootstrap_prebuilt_rules.gen'; import { installSecurityAiPromptsPackage } from '../../logic/integrations/install_ai_prompts'; +import type { + BootstrapPrebuiltRulesResponse, + PackageInstallStatus, + RuleBootstrapResults, +} from '../../../../../../common/api/detection_engine/prebuilt_rules/bootstrap_prebuilt_rules/bootstrap_prebuilt_rules.gen'; import type { SecuritySolutionRequestHandlerContext } from '../../../../../types'; import { buildSiemResponse } from '../../../routes/utils'; -import { - installEndpointPackage, - installPrebuiltRulesPackage, -} from '../install_prebuilt_rules_and_timelines/install_prebuilt_rules_package'; +import { installEndpointPackage } from '../../logic/integrations/install_endpoint_package'; +import { installPrebuiltRulesPackage } from '../../logic/integrations/install_prebuilt_rules_package'; +import { installPromotionRules } from '../../logic/integrations/install_promotion_rules'; +import { createPrebuiltRuleAssetsClient } from '../../logic/rule_assets/prebuilt_rule_assets_client'; +import { createPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebuilt_rule_objects_client'; export const bootstrapPrebuiltRulesHandler = async ( context: SecuritySolutionRequestHandlerContext, @@ -24,27 +30,53 @@ export const bootstrapPrebuiltRulesHandler = async ( const siemResponse = buildSiemResponse(response); try { - const ctx = await context.resolve(['securitySolution']); + const ctx = await context.resolve(['securitySolution', 'alerting', 'core']); const securityContext = ctx.securitySolution; const config = securityContext.getConfig(); const securityAIPromptsEnabled = config.experimentalFeatures.securityAIPromptsEnabled; + const savedObjectsClient = ctx.core.savedObjects.client; + const detectionRulesClient = securityContext.getDetectionRulesClient(); + const ruleAssetsClient = createPrebuiltRuleAssetsClient(savedObjectsClient); + const rulesClient = await ctx.alerting.getRulesClient(); + const ruleObjectsClient = createPrebuiltRuleObjectsClient(rulesClient); + + const productFeatureService = securityContext.getProductFeatureService(); + const isExternalDetectionsEnabled = productFeatureService.isEnabled( + ProductFeatureSecurityKey.externalDetections + ); + + const packageResults: PackageInstallStatus[] = []; + + // Install packages sequentially to avoid high memory usage const prebuiltRulesResult = await installPrebuiltRulesPackage(config, securityContext); - const endpointResult = await installEndpointPackage(config, securityContext); + packageResults.push({ + name: prebuiltRulesResult.package.name, + version: prebuiltRulesResult.package.version, + status: prebuiltRulesResult.status, + }); + + let ruleResults: RuleBootstrapResults | undefined; + if (isExternalDetectionsEnabled) { + ruleResults = await installPromotionRules({ + rulesClient, + detectionRulesClient, + ruleAssetsClient, + ruleObjectsClient, + fleetServices: securityContext.getInternalFleetServices(), + }); + } else { + const endpointResult = await installEndpointPackage(securityContext); + packageResults.push({ + name: endpointResult.package.name, + version: endpointResult.package.version, + status: endpointResult.status, + }); + } const responseBody: BootstrapPrebuiltRulesResponse = { - packages: [ - { - name: prebuiltRulesResult.package.name, - version: prebuiltRulesResult.package.version, - status: prebuiltRulesResult.status, - }, - { - name: endpointResult.package.name, - version: endpointResult.package.version, - status: endpointResult.status, - }, - ], + packages: packageResults, + rules: ruleResults, }; const securityAiPromptsResult = securityAIPromptsEnabled diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.test.ts index 5578aeddef88a..46cea2b6c7d4e 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.test.ts @@ -13,16 +13,16 @@ import { getBasicEmptySearchResponse, } from '../../../routes/__mocks__/request_responses'; import { requestContextMock, serverMock } from '../../../routes/__mocks__'; -import { - installPrebuiltRulesAndTimelinesRoute, - createPrepackagedRules, -} from './install_prebuilt_rules_and_timelines_route'; +import { installPrebuiltRulesAndTimelinesRoute } from './install_prebuilt_rules_and_timelines_route'; import { listMock } from '@kbn/lists-plugin/server/mocks'; import type { ExceptionListClient } from '@kbn/lists-plugin/server'; import { installPrepackagedTimelines } from '../../../../timeline/routes/prepackaged_timelines/install_prepackaged_timelines'; import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks'; import { getQueryRuleParams } from '../../../rule_schema/mocks'; +// eslint-disable-next-line no-restricted-imports +import { legacyCreatePrepackagedRules } from './legacy_create_prepackaged_rules'; + jest.mock('../../logic/rule_assets/prebuilt_rule_assets_client', () => { return { createPrebuiltRuleAssetsClient: () => { @@ -235,7 +235,7 @@ describe('add_prepackaged_rules_route', () => { describe('createPrepackagedRules', () => { test('uses exception lists client from context when available', async () => { - await createPrepackagedRules( + await legacyCreatePrepackagedRules( context.securitySolution, clients.rulesClient, mockExceptionsClient @@ -248,7 +248,7 @@ describe('add_prepackaged_rules_route', () => { test('uses passed in exceptions list client when lists client not available in context', async () => { context.securitySolution.getExceptionListClient.mockImplementation(() => null); - await createPrepackagedRules( + await legacyCreatePrepackagedRules( context.securitySolution, clients.rulesClient, mockExceptionsClient diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.ts index f24454726c615..3aba40a54be09 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.ts @@ -5,28 +5,14 @@ * 2.0. */ -import type { RulesClient } from '@kbn/alerting-plugin/server'; -import type { ExceptionListClient } from '@kbn/lists-plugin/server'; import { transformError } from '@kbn/securitysolution-es-utils'; -import { - InstallPrebuiltRulesAndTimelinesResponse, - PREBUILT_RULES_URL, -} from '../../../../../../common/api/detection_engine/prebuilt_rules'; -import type { - SecuritySolutionApiRequestHandlerContext, - SecuritySolutionPluginRouter, -} from '../../../../../types'; +import { PREBUILT_RULES_URL } from '../../../../../../common/api/detection_engine/prebuilt_rules'; +import type { SecuritySolutionPluginRouter } from '../../../../../types'; import { buildSiemResponse } from '../../../routes/utils'; -import { getExistingPrepackagedRules } from '../../../rule_management/logic/search/get_existing_prepackaged_rules'; import { PREBUILT_RULES_OPERATION_SOCKET_TIMEOUT_MS } from '../../constants'; -import { ensureLatestRulesPackageInstalled } from '../../logic/ensure_latest_rules_package_installed'; -import { getRulesToInstall } from '../../logic/get_rules_to_install'; -import { getRulesToUpdate } from '../../logic/get_rules_to_update'; -import { performTimelinesInstallation } from '../../logic/perform_timelines_installation'; -import { createPrebuiltRuleAssetsClient } from '../../logic/rule_assets/prebuilt_rule_assets_client'; -import { createPrebuiltRules } from '../../logic/rule_objects/create_prebuilt_rules'; -import { upgradePrebuiltRules } from '../../logic/rule_objects/upgrade_prebuilt_rules'; -import { rulesToMap } from '../../logic/utils'; + +// eslint-disable-next-line no-restricted-imports +import { legacyCreatePrepackagedRules } from './legacy_create_prepackaged_rules'; export const installPrebuiltRulesAndTimelinesRoute = (router: SecuritySolutionPluginRouter) => { router.versioned @@ -55,7 +41,7 @@ export const installPrebuiltRulesAndTimelinesRoute = (router: SecuritySolutionPl try { const rulesClient = await (await context.alerting).getRulesClient(); - const validated = await createPrepackagedRules( + const validated = await legacyCreatePrepackagedRules( await context.securitySolution, rulesClient, undefined @@ -71,61 +57,3 @@ export const installPrebuiltRulesAndTimelinesRoute = (router: SecuritySolutionPl } ); }; - -export class PrepackagedRulesError extends Error { - public readonly statusCode: number; - constructor(message: string, statusCode: number) { - super(message); - this.statusCode = statusCode; - } -} - -export const createPrepackagedRules = async ( - context: SecuritySolutionApiRequestHandlerContext, - rulesClient: RulesClient, - exceptionsClient?: ExceptionListClient -): Promise => { - const config = context.getConfig(); - const savedObjectsClient = context.core.savedObjects.client; - const siemClient = context.getAppClient(); - const exceptionsListClient = context.getExceptionListClient() ?? exceptionsClient; - const detectionRulesClient = context.getDetectionRulesClient(); - const ruleAssetsClient = createPrebuiltRuleAssetsClient(savedObjectsClient); - - if (!siemClient || !rulesClient) { - throw new PrepackagedRulesError('', 404); - } - - // This will create the endpoint list if it does not exist yet - if (exceptionsListClient != null) { - await exceptionsListClient.createEndpointList(); - } - - const latestPrebuiltRules = await ensureLatestRulesPackageInstalled( - ruleAssetsClient, - config, - context - ); - - const installedPrebuiltRules = rulesToMap(await getExistingPrepackagedRules({ rulesClient })); - const rulesToInstall = getRulesToInstall(latestPrebuiltRules, installedPrebuiltRules); - const rulesToUpdate = getRulesToUpdate(latestPrebuiltRules, installedPrebuiltRules); - - const result = await createPrebuiltRules(detectionRulesClient, rulesToInstall); - if (result.errors.length > 0) { - throw new AggregateError(result.errors, 'Error installing new prebuilt rules'); - } - - const { result: timelinesResult } = await performTimelinesInstallation(context); - - await upgradePrebuiltRules(detectionRulesClient, rulesToUpdate); - - const prebuiltRulesOutput: InstallPrebuiltRulesAndTimelinesResponse = { - rules_installed: rulesToInstall.length, - rules_updated: rulesToUpdate.length, - timelines_installed: timelinesResult?.timelines_installed ?? 0, - timelines_updated: timelinesResult?.timelines_updated ?? 0, - }; - - return InstallPrebuiltRulesAndTimelinesResponse.parse(prebuiltRulesOutput); -}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/install_prebuilt_rules_and_timelines/legacy_create_prepackaged_rules.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/install_prebuilt_rules_and_timelines/legacy_create_prepackaged_rules.ts new file mode 100644 index 0000000000000..5098f5f9e27bd --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/install_prebuilt_rules_and_timelines/legacy_create_prepackaged_rules.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 { RulesClient } from '@kbn/alerting-plugin/server'; +import type { ExceptionListClient } from '@kbn/lists-plugin/server'; +import type { InstallPrebuiltRulesAndTimelinesResponse } from '../../../../../../common/api/detection_engine/prebuilt_rules'; +import type { SecuritySolutionApiRequestHandlerContext } from '../../../../../types'; +import { getExistingPrepackagedRules } from '../../../rule_management/logic/search/get_existing_prepackaged_rules'; +import { ensureLatestRulesPackageInstalled } from '../../logic/integrations/ensure_latest_rules_package_installed'; +import { getRulesToInstall } from '../../logic/get_rules_to_install'; +import { getRulesToUpdate } from '../../logic/get_rules_to_update'; +import { performTimelinesInstallation } from '../../logic/perform_timelines_installation'; +import { createPrebuiltRuleAssetsClient } from '../../logic/rule_assets/prebuilt_rule_assets_client'; +import { createPrebuiltRules } from '../../logic/rule_objects/create_prebuilt_rules'; +import { upgradePrebuiltRules } from '../../logic/rule_objects/upgrade_prebuilt_rules'; +import { rulesToMap } from '../../logic/utils'; + +export class PrepackagedRulesError extends Error { + public readonly statusCode: number; + constructor(message: string, statusCode: number) { + super(message); + this.statusCode = statusCode; + } +} + +/** + * @deprecated This method is incompatible with prebuilt rules customization as + * it upgrades prebuilt rules to their 'target' version erasing any + * customizations. Use createPrebuiltRules instead. + */ +export const legacyCreatePrepackagedRules = async ( + context: SecuritySolutionApiRequestHandlerContext, + rulesClient: RulesClient, + exceptionsClient?: ExceptionListClient +): Promise => { + const config = context.getConfig(); + const savedObjectsClient = context.core.savedObjects.client; + const siemClient = context.getAppClient(); + const exceptionsListClient = context.getExceptionListClient() ?? exceptionsClient; + const detectionRulesClient = context.getDetectionRulesClient(); + const ruleAssetsClient = createPrebuiltRuleAssetsClient(savedObjectsClient); + + if (!siemClient || !rulesClient) { + throw new PrepackagedRulesError('', 404); + } + + // This will create the endpoint list if it does not exist yet + if (exceptionsListClient != null) { + await exceptionsListClient.createEndpointList(); + } + + const latestPrebuiltRules = await ensureLatestRulesPackageInstalled( + ruleAssetsClient, + config, + context + ); + + const installedPrebuiltRules = rulesToMap(await getExistingPrepackagedRules({ rulesClient })); + const rulesToInstall = getRulesToInstall(latestPrebuiltRules, installedPrebuiltRules); + const rulesToUpdate = getRulesToUpdate(latestPrebuiltRules, installedPrebuiltRules); + + const result = await createPrebuiltRules(detectionRulesClient, rulesToInstall); + if (result.errors.length > 0) { + throw new AggregateError(result.errors, 'Error installing new prebuilt rules'); + } + + const { result: timelinesResult } = await performTimelinesInstallation(context); + + await upgradePrebuiltRules(detectionRulesClient, rulesToUpdate); + + return { + rules_installed: rulesToInstall.length, + rules_updated: rulesToUpdate.length, + timelines_installed: timelinesResult?.timelines_installed ?? 0, + timelines_updated: timelinesResult?.timelines_updated ?? 0, + }; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_handler.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_handler.ts index f156e06d3e5e8..5c68db65e5c4d 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_handler.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_handler.ts @@ -16,7 +16,7 @@ import type { import type { SecuritySolutionRequestHandlerContext } from '../../../../../types'; import { buildSiemResponse } from '../../../routes/utils'; import { aggregatePrebuiltRuleErrors } from '../../logic/aggregate_prebuilt_rule_errors'; -import { ensureLatestRulesPackageInstalled } from '../../logic/ensure_latest_rules_package_installed'; +import { ensureLatestRulesPackageInstalled } from '../../logic/integrations/ensure_latest_rules_package_installed'; import { createPrebuiltRuleAssetsClient } from '../../logic/rule_assets/prebuilt_rule_assets_client'; import { createPrebuiltRules } from '../../logic/rule_objects/create_prebuilt_rules'; import { createPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebuilt_rule_objects_client'; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/index.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/index.ts index 56a0ebd88ff86..b331363143248 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -export { createPrepackagedRules } from './api/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route'; export { registerPrebuiltRulesRoutes } from './api/register_routes'; export { prebuiltRuleAssetType } from './logic/rule_assets/prebuilt_rule_assets_type'; export { PrebuiltRuleAsset } from './model/rule_assets/prebuilt_rule_asset'; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/ensure_latest_rules_package_installed.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/integrations/ensure_latest_rules_package_installed.ts similarity index 76% rename from x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/ensure_latest_rules_package_installed.ts rename to x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/integrations/ensure_latest_rules_package_installed.ts index 6ab16635374ee..074f9a07f2a4d 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/ensure_latest_rules_package_installed.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/integrations/ensure_latest_rules_package_installed.ts @@ -5,10 +5,10 @@ * 2.0. */ -import type { ConfigType } from '../../../../config'; -import type { SecuritySolutionApiRequestHandlerContext } from '../../../../types'; -import type { IPrebuiltRuleAssetsClient } from './rule_assets/prebuilt_rule_assets_client'; -import { installPrebuiltRulesPackage } from '../api/install_prebuilt_rules_and_timelines/install_prebuilt_rules_package'; +import type { ConfigType } from '../../../../../config'; +import type { SecuritySolutionApiRequestHandlerContext } from '../../../../../types'; +import type { IPrebuiltRuleAssetsClient } from '../rule_assets/prebuilt_rule_assets_client'; +import { installPrebuiltRulesPackage } from './install_prebuilt_rules_package'; export async function ensureLatestRulesPackageInstalled( ruleAssetsClient: IPrebuiltRuleAssetsClient, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/integrations/find_latest_package_version.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/integrations/find_latest_package_version.ts new file mode 100644 index 0000000000000..1105503824155 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/integrations/find_latest_package_version.ts @@ -0,0 +1,28 @@ +/* + * 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 { SecuritySolutionApiRequestHandlerContext } from '../../../../../types'; + +export async function findLatestPackageVersion( + context: SecuritySolutionApiRequestHandlerContext, + packageName: string +) { + const securityAppClient = context.getAppClient(); + const packageClient = context.getInternalFleetServices().packages; + + // Use prerelease versions in dev environment + const isPrerelease = + securityAppClient.getBuildFlavor() === 'traditional' && + (securityAppClient.getKibanaVersion().includes('-SNAPSHOT') || + securityAppClient.getKibanaBranch() === 'main'); + + const result = await packageClient.fetchFindLatestPackage(packageName, { + prerelease: isPrerelease, + }); + + return result.version; +} diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/integrations/install_endpoint_package.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/integrations/install_endpoint_package.ts new file mode 100644 index 0000000000000..74e1e24067c2e --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/integrations/install_endpoint_package.ts @@ -0,0 +1,19 @@ +/* + * 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 { SecuritySolutionApiRequestHandlerContext } from '../../../../../types'; +import { ENDPOINT_PACKAGE_NAME } from '../../../../../../common/detection_engine/constants'; +import { findLatestPackageVersion } from './find_latest_package_version'; + +export async function installEndpointPackage(context: SecuritySolutionApiRequestHandlerContext) { + const pkgVersion = await findLatestPackageVersion(context, ENDPOINT_PACKAGE_NAME); + + return context.getInternalFleetServices().packages.ensureInstalledPackage({ + pkgName: ENDPOINT_PACKAGE_NAME, + pkgVersion, + }); +} diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rules_package/install_endpoint_security_prebuilt_rule.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/integrations/install_endpoint_security_prebuilt_rule.ts similarity index 100% rename from x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rules_package/install_endpoint_security_prebuilt_rule.ts rename to x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/integrations/install_endpoint_security_prebuilt_rule.ts diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/integrations/install_prebuilt_rules_package.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/integrations/install_prebuilt_rules_package.ts new file mode 100644 index 0000000000000..0fcb20531a34b --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/integrations/install_prebuilt_rules_package.ts @@ -0,0 +1,33 @@ +/* + * 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 { SecuritySolutionApiRequestHandlerContext } from '../../../../../types'; +import { PREBUILT_RULES_PACKAGE_NAME } from '../../../../../../common/detection_engine/constants'; +import type { ConfigType } from '../../../../../config'; +import { findLatestPackageVersion } from './find_latest_package_version'; + +/** + * Installs the prebuilt rules package of the config's specified or latest version. + * + * @param config Kibana config + * @param context Request handler context + */ +export async function installPrebuiltRulesPackage( + config: ConfigType, + context: SecuritySolutionApiRequestHandlerContext +) { + let pkgVersion = config.prebuiltRulesPackageVersion; + + if (!pkgVersion) { + // Find latest package if the version isn't specified in the config + pkgVersion = await findLatestPackageVersion(context, PREBUILT_RULES_PACKAGE_NAME); + } + + return context + .getInternalFleetServices() + .packages.ensureInstalledPackage({ pkgName: PREBUILT_RULES_PACKAGE_NAME, pkgVersion }); +} diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/integrations/install_promotion_rules.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/integrations/install_promotion_rules.ts new file mode 100644 index 0000000000000..4984a50a2e57c --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/integrations/install_promotion_rules.ts @@ -0,0 +1,147 @@ +/* + * 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 { BulkOperationError, RulesClient } from '@kbn/alerting-plugin/server'; +import type { IDetectionRulesClient } from '../../../rule_management/logic/detection_rules_client/detection_rules_client_interface'; +import type { IPrebuiltRuleAssetsClient } from '../rule_assets/prebuilt_rule_assets_client'; +import { createPrebuiltRules } from '../rule_objects/create_prebuilt_rules'; +import type { IPrebuiltRuleObjectsClient } from '../rule_objects/prebuilt_rule_objects_client'; +import { upgradePrebuiltRules } from '../rule_objects/upgrade_prebuilt_rules'; +import type { + RuleBootstrapError, + RuleBootstrapResults, +} from '../../../../../../common/api/detection_engine/prebuilt_rules/bootstrap_prebuilt_rules/bootstrap_prebuilt_rules.gen'; +import { getErrorMessage } from '../../../../../utils/error_helpers'; +import type { EndpointInternalFleetServicesInterface } from '../../../../../endpoint/services/fleet'; +import { AI_FOR_SOC_INTEGRATIONS, PROMOTION_RULE_TAG } from '../../../../../../common/constants'; + +interface InstallPromotionRulesParams { + rulesClient: RulesClient; + detectionRulesClient: IDetectionRulesClient; + ruleAssetsClient: IPrebuiltRuleAssetsClient; + ruleObjectsClient: IPrebuiltRuleObjectsClient; + fleetServices: EndpointInternalFleetServicesInterface; +} + +/** + * Install or upgrade promotion rules. These rules are needed for the AI4SOC + * tier integrations to work (promote alerts from external SIEMs). + * + * Note: due to current limitations, the promotion rules cannot be placed in + * their corresponding packages and are shipped as part of the + * security_detection_engine package. However, the promotion rules should only + * be installed if the corresponding integration package is installed. The logic + * is following: we go through the list of known AI4SOC integrations and check + * if their package are installed. Then for installed integrations, we need to + * find corresponding promotion rules, they are tagged as Promotion, and install + * or upgrade them to latest versions. + * + */ +export async function installPromotionRules({ + rulesClient, + detectionRulesClient, + ruleAssetsClient, + ruleObjectsClient, + fleetServices, +}: InstallPromotionRulesParams): Promise { + // Get the list of installed integrations + const installedIntegrations = new Set( + ( + await Promise.all( + AI_FOR_SOC_INTEGRATIONS.map(async (integration) => { + // We don't care about installation status of the integration as all + // IA4SOC integrations are agentless (don't require setting up an + // integration policy). So the fact that the corresponding package is + // installed is enough. + const installation = await fleetServices.packages.getInstallation(integration); + return installation ? integration : []; + }) + ) + ).flat() + ); + + const latestRuleAssets = await ruleAssetsClient.fetchLatestAssets(); + + const latestPromotionRules = latestRuleAssets.filter((rule) => { + // Rule should be tagged as 'Promotion' and should be related to an enabled integration + return ( + (rule.tags ?? []).includes(PROMOTION_RULE_TAG) && + rule.related_integrations?.some((integration) => + installedIntegrations.has(integration.package) + ) + ); + }); + + const installedRuleVersions = await ruleObjectsClient.fetchInstalledRuleVersions(); + const installedRuleVersionsMap = new Map( + installedRuleVersions.map((version) => [version.rule_id, version]) + ); + + const promotionRulesToInstall = latestPromotionRules.filter(({ rule_id: ruleId }) => { + return !installedRuleVersionsMap.has(ruleId); + }); + const { results: installationResults, errors: installationErrors } = await createPrebuiltRules( + detectionRulesClient, + promotionRulesToInstall.map((asset) => ({ + ...asset, + enabled: true, + })) + ); + + const promotionRulesToUpgrade = latestPromotionRules.filter(({ rule_id: ruleId, version }) => { + const installedVersion = installedRuleVersionsMap.get(ruleId); + return installedVersion && installedVersion.version < version; + }); + const { results: upgradeResults, errors: upgradeErrors } = await upgradePrebuiltRules( + detectionRulesClient, + promotionRulesToUpgrade + ); + + // Cleanup any unknown rules, we don't allow users to install any detection + // rules that are not part of the enabled integrations, although that is not + // enforced via the API + const rulesToDelete = installedRuleVersions.filter(({ rule_id: ruleId }) => { + return !latestPromotionRules.some((rule) => rule.rule_id === ruleId); + }); + const deletionErrors: BulkOperationError[] = []; + if (rulesToDelete.length > 0) { + const bulkDeleteResult = await rulesClient.bulkDeleteRules({ + ids: rulesToDelete.map(({ id }) => id), + }); + deletionErrors.push(...bulkDeleteResult.errors); + } + + const alreadyUpToDate = latestPromotionRules.filter(({ rule_id: ruleId, version }) => { + const installedVersion = installedRuleVersionsMap.get(ruleId); + return installedVersion && installedVersion.version === version; + }); + + const allErrors = [...installationErrors, ...upgradeErrors, ...deletionErrors].reduce( + (errorsMap, currentError) => { + const errorMessage = + 'error' in currentError ? getErrorMessage(currentError.error) : currentError.message; + const ruleId = 'item' in currentError ? currentError.item.rule_id : currentError.rule.id; + const existingError = errorsMap.get(errorMessage); + if (existingError) { + existingError.rules.push({ rule_id: ruleId }); + } else { + errorsMap.set(errorMessage, { rules: [{ rule_id: ruleId }], message: errorMessage }); + } + return errorsMap; + }, + new Map() + ); + + return { + total: latestPromotionRules.length, + installed: installationResults.length, + updated: upgradeResults.length, + deleted: rulesToDelete.length, + skipped: alreadyUpToDate.length, + errors: allErrors.size > 0 ? Array.from(allErrors.values()) : [], + }; +} diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client.ts index a6eb5d7367134..84f46b105c689 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client.ts @@ -75,10 +75,14 @@ export const createPrebuiltRuleAssetsClient = ( }); }, - fetchLatestVersions: (ruleIds: string[] = []): Promise => { + fetchLatestVersions: (ruleIds?: string[]): Promise => { return withSecuritySpan('IPrebuiltRuleAssetsClient.fetchLatestVersions', async () => { + if (ruleIds && ruleIds.length === 0) { + return []; + } + const filter = ruleIds - .map((ruleId) => `${PREBUILT_RULE_ASSETS_SO_TYPE}.attributes.rule_id: ${ruleId}`) + ?.map((ruleId) => `${PREBUILT_RULE_ASSETS_SO_TYPE}.attributes.rule_id: ${ruleId}`) .join(' OR '); const findResult = await savedObjectsClient.find< diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client.ts index 2bf7cf5c44364..729f8ce2dcc2b 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client.ts @@ -7,6 +7,7 @@ import type { RulesClient } from '@kbn/alerting-plugin/server'; import type { + RuleObjectId, RuleResponse, RuleSignatureId, RuleTagArray, @@ -49,15 +50,18 @@ interface FetchInstalledRulesByIdsArgs { sortOrder?: SortOrder; } +type RuleSummary = RuleVersionSpecifier & { + id: RuleObjectId; + tags: RuleTagArray; +}; + export interface IPrebuiltRuleObjectsClient { fetchInstalledRulesByIds(args: FetchInstalledRulesByIdsArgs): Promise; fetchInstalledRules(args?: FetchAllInstalledRulesArgs): Promise; fetchInstalledRuleVersionsByIds( args: FetchInstalledRuleVersionsByIdsArgs - ): Promise>; - fetchInstalledRuleVersions( - args?: FetchAllInstalledRuleVersionsArgs - ): Promise>; + ): Promise; + fetchInstalledRuleVersions(args?: FetchAllInstalledRuleVersionsArgs): Promise; } export const createPrebuiltRuleObjectsClient = ( @@ -125,6 +129,7 @@ export const createPrebuiltRuleObjectsClient = ( fields: ['params.ruleId', 'params.version', 'tags'], }); return rulesData.data.map((rule) => ({ + id: rule.id, rule_id: rule.params.ruleId, version: rule.params.version, tags: rule.tags, @@ -151,6 +156,7 @@ export const createPrebuiltRuleObjectsClient = ( fields: ['params.ruleId', 'params.version', 'tags'], }); return rulesData.data.map((rule) => ({ + id: rule.id, rule_id: rule.params.ruleId, version: rule.params.version, tags: rule.tags, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts index b9d23fc1e6db9..ab8cc7e85ea43 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts @@ -43,6 +43,7 @@ import { detectionRulesClientMock } from '../../rule_management/logic/detection_ import { packageServiceMock } from '@kbn/fleet-plugin/server/services/epm/package_service.mock'; import type { EndpointInternalFleetServicesInterface } from '../../../../endpoint/services/fleet'; import { siemMigrationsServiceMock } from '../../../siem_migrations/__mocks__/mocks'; +import { createProductFeaturesServiceMock } from '../../../product_features_service/mocks'; export const createMockClients = () => { const core = coreMock.createRequestHandlerContext(); @@ -81,6 +82,7 @@ export const createMockClients = () => { }, siemRuleMigrationsClient: siemMigrationsServiceMock.createRulesClient(), getInferenceClient: jest.fn(), + productFeaturesService: createProductFeaturesServiceMock(), }; }; @@ -170,6 +172,7 @@ const createSecuritySolutionRequestContextMock = ( getEntityStoreDataClient: jest.fn(() => clients.entityStoreDataClient), getSiemRuleMigrationsClient: jest.fn(() => clients.siemRuleMigrationsClient), getInferenceClient: jest.fn(() => clients.getInferenceClient()), + getProductFeatureService: jest.fn(() => clients.productFeaturesService), }; }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/rule_source_importer/rule_source_importer.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/rule_source_importer/rule_source_importer.ts index e6900a5ce2683..a22ca4ff7e7b9 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/rule_source_importer/rule_source_importer.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/rule_source_importer/rule_source_importer.ts @@ -21,7 +21,7 @@ import type { } from '../../../../../../../common/api/detection_engine'; import type { PrebuiltRuleAsset } from '../../../../prebuilt_rules'; import type { IPrebuiltRuleAssetsClient } from '../../../../prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client'; -import { ensureLatestRulesPackageInstalled } from '../../../../prebuilt_rules/logic/ensure_latest_rules_package_installed'; +import { ensureLatestRulesPackageInstalled } from '../../../../prebuilt_rules/logic/integrations/ensure_latest_rules_package_installed'; import { calculateRuleSourceForImport } from '../calculate_rule_source_for_import'; import type { CalculatedRuleSource, IRuleSourceImporter } from './rule_source_importer_interface'; import type { IPrebuiltRuleObjectsClient } from '../../../../prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client'; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/configure_saved_object.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/configure_saved_object.test.ts index c70af5c70c7af..813cbb06bca8a 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/configure_saved_object.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/configure_saved_object.test.ts @@ -24,8 +24,6 @@ describe('riskEnginConfigureSavedObjectRoute', () => { let getStartServicesMock: jest.Mock; beforeEach(() => { - jest.resetAllMocks(); - server = serverMock.create(); const { clients } = requestContextMock.createTools(); mockRiskEngineDataClient = riskEngineDataClientMock.create(); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/delete.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/delete.test.ts index 29e43de5d8fb7..691b572615022 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/delete.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/delete.test.ts @@ -24,8 +24,6 @@ describe('risk engine cleanup route', () => { let getStartServicesMock: jest.Mock; beforeEach(() => { - jest.resetAllMocks(); - server = serverMock.create(); const { clients } = requestContextMock.createTools(); mockRiskEngineDataClient = riskEngineDataClientMock.create(); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/disable.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/disable.test.ts index 429b13f3a3738..13d39cd514197 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/disable.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/disable.test.ts @@ -24,8 +24,6 @@ describe('risk score disable route', () => { let getStartServicesMock: jest.Mock; beforeEach(() => { - jest.resetAllMocks(); - server = serverMock.create(); const { clients } = requestContextMock.createTools(); mockRiskEngineDataClient = riskEngineDataClientMock.create(); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/enable.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/enable.test.ts index 4932de0b2d270..2c558199af393 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/enable.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/enable.test.ts @@ -24,8 +24,6 @@ describe('risk score enable route', () => { let getStartServicesMock: jest.Mock; beforeEach(() => { - jest.resetAllMocks(); - server = serverMock.create(); const { clients } = requestContextMock.createTools(); mockRiskEngineDataClient = riskEngineDataClientMock.create(); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_score/routes/entity_calculation.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_score/routes/entity_calculation.test.ts index 54b42d02c5495..44b37c804c9e2 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_score/routes/entity_calculation.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_score/routes/entity_calculation.test.ts @@ -36,8 +36,6 @@ describe('entity risk score calculation route', () => { let getStartServicesMock: jest.Mock; beforeEach(() => { - jest.resetAllMocks(); - getStartServicesMock = jest.fn().mockResolvedValue([ {}, { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_score/routes/preview.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_score/routes/preview.test.ts index 6aa2339b999d6..a40849086891f 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_score/routes/preview.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/risk_score/routes/preview.test.ts @@ -29,8 +29,6 @@ describe('POST risk_engine/preview route', () => { let mockRiskScoreService: ReturnType; beforeEach(() => { - jest.resetAllMocks(); - server = serverMock.create(); logger = loggerMock.create(); ({ clients, context } = requestContextMock.createTools()); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/risk_score/indices/delete_indices_route.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/risk_score/indices/delete_indices_route.test.ts index 701fa8286b073..9b03906417879 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/risk_score/indices/delete_indices_route.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/risk_score/indices/delete_indices_route.test.ts @@ -27,9 +27,6 @@ describe('deleteEsIndicesRoute', () => { let { context } = requestContextMock.createTools(); beforeEach(() => { - jest.resetModules(); - jest.resetAllMocks(); - server = serverMock.create(); ({ context } = requestContextMock.createTools()); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/risk_score/prebuilt_dev_tool_content/routes/read_prebuilt_dev_tool_content_route.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/risk_score/prebuilt_dev_tool_content/routes/read_prebuilt_dev_tool_content_route.test.ts index d1b05736beb49..3d5195cfc5771 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/risk_score/prebuilt_dev_tool_content/routes/read_prebuilt_dev_tool_content_route.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/risk_score/prebuilt_dev_tool_content/routes/read_prebuilt_dev_tool_content_route.test.ts @@ -25,9 +25,6 @@ describe('readPrebuiltDevToolContentRoute', () => { let { context } = requestContextMock.createTools(); beforeEach(() => { - jest.resetModules(); - jest.resetAllMocks(); - server = serverMock.create(); ({ context } = requestContextMock.createTools()); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/risk_score/stored_scripts/delete_script_route.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/risk_score/stored_scripts/delete_script_route.test.ts index 8d95c0854bcc4..1462aa81bb71d 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/risk_score/stored_scripts/delete_script_route.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/risk_score/stored_scripts/delete_script_route.test.ts @@ -27,9 +27,6 @@ describe('deleteStoredScriptRoute', () => { let { context } = requestContextMock.createTools(); beforeEach(() => { - jest.resetModules(); - jest.resetAllMocks(); - server = serverMock.create(); ({ context } = requestContextMock.createTools()); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/draft_timelines/clean_draft_timelines/index.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/draft_timelines/clean_draft_timelines/index.test.ts index ba727f6c0d707..80d6339a95e08 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/draft_timelines/clean_draft_timelines/index.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/draft_timelines/clean_draft_timelines/index.test.ts @@ -34,7 +34,6 @@ describe('clean draft timelines', () => { beforeEach(() => { jest.resetModules(); - jest.resetAllMocks(); jest.clearAllMocks(); server = serverMock.create(); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/draft_timelines/get_draft_timelines/index.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/draft_timelines/get_draft_timelines/index.test.ts index 75c9b361bc78f..c66c7ee466d1b 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/draft_timelines/get_draft_timelines/index.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/draft_timelines/get_draft_timelines/index.test.ts @@ -33,7 +33,6 @@ describe('get draft timelines', () => { beforeEach(() => { jest.resetModules(); - jest.resetAllMocks(); jest.clearAllMocks(); server = serverMock.create(); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines/index.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines/index.test.ts index 9513bd49b4e6d..65f09c8e7663a 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines/index.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines/index.test.ts @@ -40,7 +40,7 @@ describe('installPrepackagedTimelines', () => { beforeEach(() => { jest.resetModules(); - jest.resetAllMocks(); + jest.clearAllMocks(); server = serverMock.create(); context = requestContextMock.createTools().context; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/index.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/index.test.ts index a510f24a35637..d3e337ef1fddf 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/index.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/index.test.ts @@ -44,7 +44,6 @@ describe('create timelines', () => { beforeEach(() => { jest.resetModules(); - jest.resetAllMocks(); jest.clearAllMocks(); server = serverMock.create(); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/get_timeline/index.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/get_timeline/index.test.ts index 781da93355442..0e934f140cb11 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/get_timeline/index.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/get_timeline/index.test.ts @@ -24,7 +24,7 @@ describe('get timeline', () => { beforeEach(() => { jest.resetModules(); - jest.resetAllMocks(); + jest.clearAllMocks(); server = serverMock.create(); context = requestContextMock.createTools().context; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/get_timelines/index.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/get_timelines/index.test.ts index 753ac1eb15aea..6d071f16d3cce 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/get_timelines/index.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/get_timelines/index.test.ts @@ -20,7 +20,7 @@ describe('get all timelines', () => { beforeEach(() => { jest.resetModules(); - jest.resetAllMocks(); + jest.clearAllMocks(); server = serverMock.create(); context = requestContextMock.createTools().context; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/index.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/index.test.ts index 5528d92d34240..9d473b763c43b 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/index.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/index.test.ts @@ -51,7 +51,6 @@ describe('import timelines', () => { beforeEach(() => { jest.resetModules(); - jest.resetAllMocks(); jest.clearAllMocks(); server = serverMock.create(); @@ -469,7 +468,6 @@ describe('import timeline templates', () => { const mockNewTemplateTimelineId = 'new templateTimelineId'; beforeEach(() => { jest.resetModules(); - jest.resetAllMocks(); jest.clearAllMocks(); server = serverMock.create(); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/patch_timelines/index.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/patch_timelines/index.test.ts index bb2ba80526d00..684f2b485c939 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/patch_timelines/index.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/patch_timelines/index.test.ts @@ -41,7 +41,6 @@ describe('update timelines', () => { beforeEach(() => { jest.resetModules(); - jest.resetAllMocks(); jest.clearAllMocks(); server = serverMock.create(); diff --git a/x-pack/solutions/security/plugins/security_solution/server/request_context_factory.ts b/x-pack/solutions/security/plugins/security_solution/server/request_context_factory.ts index 40ef090798741..bd2e7abe9dab0 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/request_context_factory.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/request_context_factory.ts @@ -156,6 +156,8 @@ export class RequestContextFactory implements IRequestContextFactory { getEntityStoreApiKeyManager, + getProductFeatureService: () => productFeaturesService, + getDetectionRulesClient: memoize(() => { const mlAuthz = buildMlAuthz({ license: licensing.license, diff --git a/x-pack/solutions/security/plugins/security_solution/server/types.ts b/x-pack/solutions/security/plugins/security_solution/server/types.ts index 7356e8268ef41..54409e4d1878f 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/types.ts @@ -39,6 +39,7 @@ import type { IDetectionRulesClient } from './lib/detection_engine/rule_manageme import type { EntityStoreDataClient } from './lib/entity_analytics/entity_store/entity_store_data_client'; import type { SiemRuleMigrationsClient } from './lib/siem_migrations/rules/siem_rule_migrations_service'; import type { ApiKeyManager } from './lib/entity_analytics/entity_store/auth/api_key'; +import type { ProductFeaturesService } from './lib/product_features_service'; export { AppClient }; export interface SecuritySolutionApiRequestHandlerContext { @@ -65,6 +66,7 @@ export interface SecuritySolutionApiRequestHandlerContext { getEntityStoreDataClient: () => EntityStoreDataClient; getSiemRuleMigrationsClient: () => SiemRuleMigrationsClient; getInferenceClient: () => InferenceClient; + getProductFeatureService: () => ProductFeaturesService; } export type SecuritySolutionRequestHandlerContext = CustomRequestHandlerContext<{ diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/is_customized_calculation.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/is_customized_calculation.ts new file mode 100644 index 0000000000000..b9f27dc627fbc --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/is_customized_calculation.ts @@ -0,0 +1,200 @@ +/* + * 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 { + BulkActionEditTypeEnum, + BulkActionTypeEnum, +} from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.gen'; +import expect from 'expect'; +import { deleteAllRules } from '../../../../../../../common/utils/security_solution'; +import { FtrProviderContext } from '../../../../../../ftr_provider_context'; +import { + createPrebuiltRuleAssetSavedObjects, + createRuleAssetSavedObject, + deleteAllPrebuiltRuleAssets, + installPrebuiltRules, +} from '../../../../utils'; + +export default ({ getService }: FtrProviderContext): void => { + const es = getService('es'); + const supertest = getService('supertest'); + const securitySolutionApi = getService('securitySolutionApi'); + const log = getService('log'); + + const ruleAsset = createRuleAssetSavedObject({ + rule_id: '000047bb-b27a-47ec-8b62-ef1a5d2c9e19', + tags: ['test-tag'], + }); + + describe('@ess @serverless @skipInServerlessMKI is_customized calculation', () => { + beforeEach(async () => { + await deleteAllRules(supertest, log); + await deleteAllPrebuiltRuleAssets(es, log); + }); + + describe('prebuilt rules', () => { + it('should set is_customized to true on bulk rule modification', async () => { + await createPrebuiltRuleAssetSavedObjects(es, [ruleAsset]); + await installPrebuiltRules(es, supertest); + + const { body: findResult } = await securitySolutionApi + .findRules({ + query: { + per_page: 1, + filter: `alert.attributes.params.immutable: true`, + }, + }) + .expect(200); + const prebuiltRule = findResult.data[0]; + expect(prebuiltRule).toBeDefined(); + expect(prebuiltRule.rule_source.is_customized).toEqual(false); + + const { body: bulkResult } = await securitySolutionApi + .performRulesBulkAction({ + query: {}, + body: { + ids: [prebuiltRule.id], + action: BulkActionTypeEnum.edit, + [BulkActionTypeEnum.edit]: [ + { + type: BulkActionEditTypeEnum.add_tags, + value: ['new-tag'], + }, + ], + }, + }) + .expect(200); + + expect(bulkResult.attributes.summary).toEqual({ + failed: 0, + skipped: 0, + succeeded: 1, + total: 1, + }); + expect(bulkResult.attributes.results.updated[0].rule_source.is_customized).toEqual(true); + }); + + it('should leave is_customized intact if the change has been skipped', async () => { + await createPrebuiltRuleAssetSavedObjects(es, [ruleAsset]); + await installPrebuiltRules(es, supertest); + + const { body: findResult } = await securitySolutionApi + .findRules({ + query: { + per_page: 1, + filter: `alert.attributes.params.immutable: true`, + }, + }) + .expect(200); + const prebuiltRule = findResult.data[0]; + expect(prebuiltRule).toBeDefined(); + expect(prebuiltRule.rule_source.is_customized).toEqual(false); + + const { body: bulkResult } = await securitySolutionApi + .performRulesBulkAction({ + query: {}, + body: { + ids: [prebuiltRule.id], + action: BulkActionTypeEnum.edit, + [BulkActionTypeEnum.edit]: [ + { + type: BulkActionEditTypeEnum.add_tags, + // This tag is already present on the rule, so the change will be skipped + value: [prebuiltRule.tags[0]], + }, + ], + }, + }) + .expect(200); + + expect(bulkResult.attributes.summary).toEqual({ + failed: 0, + skipped: 1, + succeeded: 0, + total: 1, + }); + + // Check that the rule has not been customized + const { body: findResultAfter } = await securitySolutionApi + .findRules({ + query: { + per_page: 1, + filter: `alert.attributes.params.immutable: true`, + }, + }) + .expect(200); + expect(findResultAfter.data[0].rule_source.is_customized).toEqual(false); + }); + + it('should set is_customized to false if the change has been reverted', async () => { + await createPrebuiltRuleAssetSavedObjects(es, [ruleAsset]); + await installPrebuiltRules(es, supertest); + + const { body: findResult } = await securitySolutionApi + .findRules({ + query: { + per_page: 1, + filter: `alert.attributes.params.immutable: true`, + }, + }) + .expect(200); + const prebuiltRule = findResult.data[0]; + expect(prebuiltRule).toBeDefined(); + expect(prebuiltRule.rule_source.is_customized).toEqual(false); + + // Add a tag to the rule + const { body: bulkResult } = await securitySolutionApi + .performRulesBulkAction({ + query: {}, + body: { + ids: [prebuiltRule.id], + action: BulkActionTypeEnum.edit, + [BulkActionTypeEnum.edit]: [ + { + type: BulkActionEditTypeEnum.add_tags, + value: ['new-tag'], + }, + ], + }, + }) + .expect(200); + + expect(bulkResult.attributes.summary).toEqual({ + failed: 0, + skipped: 0, + succeeded: 1, + total: 1, + }); + + // Remove the added tag + const { body: revertResult } = await securitySolutionApi + .performRulesBulkAction({ + query: {}, + body: { + ids: [prebuiltRule.id], + action: BulkActionTypeEnum.edit, + [BulkActionTypeEnum.edit]: [ + { + type: BulkActionEditTypeEnum.delete_tags, + value: ['new-tag'], + }, + ], + }, + }) + .expect(200); + + expect(revertResult.attributes.summary).toEqual({ + failed: 0, + skipped: 0, + succeeded: 1, + total: 1, + }); + + expect(revertResult.attributes.results.updated[0].rule_source.is_customized).toEqual(false); + }); + }); + }); +}; diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/prebuilt_rules.ts b/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/prebuilt_rules.ts index 2a0b601b2dc60..2fd936202902c 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/prebuilt_rules.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/prebuilt_rules.ts @@ -183,7 +183,7 @@ export const getRuleAssets = (index: string | undefined = '.kibana_security_solu /* during e2e tests, and allow for manual installation of mock rules instead. */ export const preventPrebuiltRulesPackageInstallation = () => { cy.log('Prevent prebuilt rules package installation'); - cy.intercept('POST', BOOTSTRAP_PREBUILT_RULES_URL, {}); + cy.intercept('POST', BOOTSTRAP_PREBUILT_RULES_URL, { packages: [] }); }; /**