diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/capped_exponential_backoff.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/capped_exponential_backoff.ts new file mode 100644 index 0000000000000..9de7219398e06 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/capped_exponential_backoff.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ + +const MAX_BACKOFF = 30000; + +/** + * Calculates a backoff delay using an exponential growth formula, capped by a + * predefined maximum value. + * + * @param failedAttempts - The number of consecutive failed attempts. + * @returns The calculated backoff delay, in milliseconds. + */ +export const cappedExponentialBackoff = (failedAttempts: number) => { + const backoff = Math.min(1000 * 2 ** failedAttempts, MAX_BACKOFF); + return backoff; +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/retry_on_rate_limited_error.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/retry_on_rate_limited_error.ts new file mode 100644 index 0000000000000..0d83d324da436 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/retry_on_rate_limited_error.ts @@ -0,0 +1,22 @@ +/* + * 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 { get } from 'lodash'; + +/* + Prebuilt rule operations like install and upgrade are rate limited to one at a + time. In most cases it is fine to wait for the other operation to finish using + the retry logic. + + 429 can be caused by a user clicking multiple times on the install or upgrade + rule buttons and in most cases the operations can be performed in succession + without any conflicts. + */ +export const retryOnRateLimitedError = (failureCount: number, error: unknown) => { + const statusCode = get(error, 'response.status'); + return statusCode === 429; +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_bootstrap_prebuilt_rules.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_bootstrap_prebuilt_rules.ts similarity index 68% rename from x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_bootstrap_prebuilt_rules.ts rename to x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_bootstrap_prebuilt_rules.ts index 9f2b810138a6c..379e8c9326fec 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_bootstrap_prebuilt_rules.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_bootstrap_prebuilt_rules.ts @@ -6,18 +6,18 @@ */ import type { UseMutationOptions } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query'; -import { BOOTSTRAP_PREBUILT_RULES_URL } from '../../../../../common/api/detection_engine'; -import type { BootstrapPrebuiltRulesResponse } from '../../../../../common/api/detection_engine/prebuilt_rules/bootstrap_prebuilt_rules/bootstrap_prebuilt_rules.gen'; -import { PREBUILT_RULES_PACKAGE_NAME } from '../../../../../common/detection_engine/constants'; -import { bootstrapPrebuiltRules } from '../api'; -import { useInvalidateFetchPrebuiltRulesInstallReviewQuery } from './prebuilt_rules/use_fetch_prebuilt_rules_install_review_query'; -import { useInvalidateFetchPrebuiltRulesStatusQuery } from './prebuilt_rules/use_fetch_prebuilt_rules_status_query'; -import { useInvalidateFetchPrebuiltRulesUpgradeReviewQuery } from './prebuilt_rules/use_fetch_prebuilt_rules_upgrade_review_query'; +import { BOOTSTRAP_PREBUILT_RULES_URL } from '../../../../../../common/api/detection_engine'; +import type { BootstrapPrebuiltRulesResponse } from '../../../../../../common/api/detection_engine/prebuilt_rules/bootstrap_prebuilt_rules/bootstrap_prebuilt_rules.gen'; +import { PREBUILT_RULES_PACKAGE_NAME } from '../../../../../../common/detection_engine/constants'; +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'; export const BOOTSTRAP_PREBUILT_RULES_KEY = ['POST', BOOTSTRAP_PREBUILT_RULES_URL]; export const useBootstrapPrebuiltRulesMutation = ( - options?: UseMutationOptions + options?: UseMutationOptions ) => { const invalidatePrePackagedRulesStatus = useInvalidateFetchPrebuiltRulesStatusQuery(); const invalidatePrebuiltRulesInstallReview = useInvalidateFetchPrebuiltRulesInstallReviewQuery(); @@ -26,7 +26,7 @@ export const useBootstrapPrebuiltRulesMutation = ( return useMutation(() => bootstrapPrebuiltRules(), { ...options, mutationKey: BOOTSTRAP_PREBUILT_RULES_KEY, - onSettled: (...args) => { + onSuccess: (...args) => { const response = args[0]; if ( response?.packages.find((pkg) => pkg.name === PREBUILT_RULES_PACKAGE_NAME)?.status === @@ -40,8 +40,8 @@ export const useBootstrapPrebuiltRulesMutation = ( invalidatePrebuiltRulesUpdateReview(); } - if (options?.onSettled) { - options.onSettled(...args); + if (options?.onSuccess) { + options.onSuccess(...args); } }, }); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_install_review_query.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_install_review_query.ts index 0f7f5b512b0fa..9d88e8984962f 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_install_review_query.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_install_review_query.ts @@ -11,6 +11,8 @@ import { reviewRuleInstall } from '../../api'; import { REVIEW_RULE_INSTALLATION_URL } from '../../../../../../common/api/detection_engine/prebuilt_rules/urls'; import type { ReviewRuleInstallationResponseBody } from '../../../../../../common/api/detection_engine/prebuilt_rules'; import { DEFAULT_QUERY_OPTIONS } from '../constants'; +import { retryOnRateLimitedError } from './retry_on_rate_limited_error'; +import { cappedExponentialBackoff } from './capped_exponential_backoff'; export const REVIEW_RULE_INSTALLATION_QUERY_KEY = ['POST', REVIEW_RULE_INSTALLATION_URL]; @@ -26,6 +28,8 @@ export const useFetchPrebuiltRulesInstallReviewQuery = ( { ...DEFAULT_QUERY_OPTIONS, ...options, + retry: retryOnRateLimitedError, + retryDelay: cappedExponentialBackoff, } ); }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_upgrade_review_query.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_upgrade_review_query.ts index 78e5cc26d6a8c..532114b1d4b62 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_upgrade_review_query.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_upgrade_review_query.ts @@ -11,6 +11,8 @@ import { reviewRuleUpgrade } from '../../api'; import { REVIEW_RULE_UPGRADE_URL } from '../../../../../../common/api/detection_engine/prebuilt_rules/urls'; import type { ReviewRuleUpgradeResponseBody } from '../../../../../../common/api/detection_engine/prebuilt_rules'; import { DEFAULT_QUERY_OPTIONS } from '../constants'; +import { retryOnRateLimitedError } from './retry_on_rate_limited_error'; +import { cappedExponentialBackoff } from './capped_exponential_backoff'; export const REVIEW_RULE_UPGRADE_QUERY_KEY = ['POST', REVIEW_RULE_UPGRADE_URL]; @@ -26,6 +28,8 @@ export const useFetchPrebuiltRulesUpgradeReviewQuery = ( { ...DEFAULT_QUERY_OPTIONS, ...options, + retry: retryOnRateLimitedError, + retryDelay: cappedExponentialBackoff, } ); }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_all_rules_install_mutation.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_all_rules_install_mutation.ts index 6e46f25015b2a..0d9b49d550716 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_all_rules_install_mutation.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_all_rules_install_mutation.ts @@ -8,13 +8,15 @@ import type { UseMutationOptions } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query'; import type { PerformRuleInstallationResponseBody } from '../../../../../../common/api/detection_engine/prebuilt_rules'; import { PERFORM_RULE_INSTALLATION_URL } from '../../../../../../common/api/detection_engine/prebuilt_rules/urls'; -import { useInvalidateFetchPrebuiltRulesStatusQuery } from './use_fetch_prebuilt_rules_status_query'; -import { useInvalidateFindRulesQuery } from '../use_find_rules_query'; +import { performInstallAllRules } from '../../api'; +import { useInvalidateFetchCoverageOverviewQuery } from '../use_fetch_coverage_overview_query'; import { useInvalidateFetchRuleManagementFiltersQuery } from '../use_fetch_rule_management_filters_query'; import { useInvalidateFetchRulesSnoozeSettingsQuery } from '../use_fetch_rules_snooze_settings_query'; +import { useInvalidateFindRulesQuery } from '../use_find_rules_query'; +import { retryOnRateLimitedError } from './retry_on_rate_limited_error'; import { useInvalidateFetchPrebuiltRulesInstallReviewQuery } from './use_fetch_prebuilt_rules_install_review_query'; -import { performInstallAllRules } from '../../api'; -import { useInvalidateFetchCoverageOverviewQuery } from '../use_fetch_coverage_overview_query'; +import { useInvalidateFetchPrebuiltRulesStatusQuery } from './use_fetch_prebuilt_rules_status_query'; +import { cappedExponentialBackoff } from './capped_exponential_backoff'; export const PERFORM_ALL_RULES_INSTALLATION_KEY = [ 'POST', @@ -23,7 +25,7 @@ export const PERFORM_ALL_RULES_INSTALLATION_KEY = [ ]; export const usePerformAllRulesInstallMutation = ( - options?: UseMutationOptions + options?: UseMutationOptions ) => { const invalidateFindRulesQuery = useInvalidateFindRulesQuery(); const invalidateFetchRulesSnoozeSettings = useInvalidateFetchRulesSnoozeSettingsQuery(); @@ -33,7 +35,7 @@ export const usePerformAllRulesInstallMutation = ( const invalidateRuleStatus = useInvalidateFetchPrebuiltRulesStatusQuery(); const invalidateFetchCoverageOverviewQuery = useInvalidateFetchCoverageOverviewQuery(); - return useMutation(() => performInstallAllRules(), { + return useMutation(() => performInstallAllRules(), { ...options, mutationKey: PERFORM_ALL_RULES_INSTALLATION_KEY, onSettled: (...args) => { @@ -49,5 +51,7 @@ export const usePerformAllRulesInstallMutation = ( options.onSettled(...args); } }, + retry: retryOnRateLimitedError, + retryDelay: cappedExponentialBackoff, }); }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_specific_rules_install_mutation.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_specific_rules_install_mutation.ts index 3b448219d6e01..913654809b607 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_specific_rules_install_mutation.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_specific_rules_install_mutation.ts @@ -11,15 +11,17 @@ import type { PerformRuleInstallationResponseBody, } from '../../../../../../common/api/detection_engine/prebuilt_rules'; import { PERFORM_RULE_INSTALLATION_URL } from '../../../../../../common/api/detection_engine/prebuilt_rules/urls'; -import { useInvalidateFetchPrebuiltRulesStatusQuery } from './use_fetch_prebuilt_rules_status_query'; -import { useInvalidateFindRulesQuery } from '../use_find_rules_query'; -import { useInvalidateFetchRuleManagementFiltersQuery } from '../use_fetch_rule_management_filters_query'; -import { useInvalidateFetchRulesSnoozeSettingsQuery } from '../use_fetch_rules_snooze_settings_query'; -import { useInvalidateFetchPrebuiltRulesInstallReviewQuery } from './use_fetch_prebuilt_rules_install_review_query'; import type { BulkAction } from '../../api'; import { performInstallSpecificRules } from '../../api'; -import { useInvalidateFetchCoverageOverviewQuery } from '../use_fetch_coverage_overview_query'; import { useBulkActionMutation } from '../use_bulk_action_mutation'; +import { useInvalidateFetchCoverageOverviewQuery } from '../use_fetch_coverage_overview_query'; +import { useInvalidateFetchRuleManagementFiltersQuery } from '../use_fetch_rule_management_filters_query'; +import { useInvalidateFetchRulesSnoozeSettingsQuery } from '../use_fetch_rules_snooze_settings_query'; +import { useInvalidateFindRulesQuery } from '../use_find_rules_query'; +import { retryOnRateLimitedError } from './retry_on_rate_limited_error'; +import { useInvalidateFetchPrebuiltRulesInstallReviewQuery } from './use_fetch_prebuilt_rules_install_review_query'; +import { useInvalidateFetchPrebuiltRulesStatusQuery } from './use_fetch_prebuilt_rules_status_query'; +import { cappedExponentialBackoff } from './capped_exponential_backoff'; export const PERFORM_SPECIFIC_RULES_INSTALLATION_KEY = [ 'POST', @@ -35,7 +37,7 @@ export interface UsePerformSpecificRulesInstallParams { export const usePerformSpecificRulesInstallMutation = ( options?: UseMutationOptions< PerformRuleInstallationResponseBody, - Error, + unknown, UsePerformSpecificRulesInstallParams > ) => { @@ -51,7 +53,7 @@ export const usePerformSpecificRulesInstallMutation = ( return useMutation< PerformRuleInstallationResponseBody, - Error, + unknown, UsePerformSpecificRulesInstallParams >( (rulesToInstall: UsePerformSpecificRulesInstallParams) => @@ -81,6 +83,8 @@ export const usePerformSpecificRulesInstallMutation = ( options.onSettled(...args); } }, + retry: retryOnRateLimitedError, + retryDelay: cappedExponentialBackoff, } ); }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_specific_rules_upgrade_mutation.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_specific_rules_upgrade_mutation.ts index 08338ab9a932c..cf9fff9dbd9b8 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_specific_rules_upgrade_mutation.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_specific_rules_upgrade_mutation.ts @@ -12,13 +12,15 @@ import type { UpgradeSpecificRulesRequest, } from '../../../../../../common/api/detection_engine/prebuilt_rules'; import { PERFORM_RULE_UPGRADE_URL } from '../../../../../../common/api/detection_engine/prebuilt_rules/urls'; -import { useInvalidateFetchPrebuiltRulesStatusQuery } from './use_fetch_prebuilt_rules_status_query'; -import { useInvalidateFindRulesQuery } from '../use_find_rules_query'; +import { performUpgradeSpecificRules } from '../../api'; +import { useInvalidateFetchCoverageOverviewQuery } from '../use_fetch_coverage_overview_query'; import { useInvalidateFetchRuleManagementFiltersQuery } from '../use_fetch_rule_management_filters_query'; import { useInvalidateFetchRulesSnoozeSettingsQuery } from '../use_fetch_rules_snooze_settings_query'; -import { performUpgradeSpecificRules } from '../../api'; +import { useInvalidateFindRulesQuery } from '../use_find_rules_query'; +import { useInvalidateFetchPrebuiltRulesStatusQuery } from './use_fetch_prebuilt_rules_status_query'; import { useInvalidateFetchPrebuiltRulesUpgradeReviewQuery } from './use_fetch_prebuilt_rules_upgrade_review_query'; -import { useInvalidateFetchCoverageOverviewQuery } from '../use_fetch_coverage_overview_query'; +import { retryOnRateLimitedError } from './retry_on_rate_limited_error'; +import { cappedExponentialBackoff } from './capped_exponential_backoff'; export const PERFORM_SPECIFIC_RULES_UPGRADE_KEY = [ 'POST', @@ -64,6 +66,8 @@ export const usePerformSpecificRulesUpgradeMutation = ( options.onSettled(...args); } }, + retry: retryOnRateLimitedError, + retryDelay: cappedExponentialBackoff, } ); }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_upgrade_security_packages.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_upgrade_security_packages.ts index e19cf2bcacf94..72246b085c40f 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_upgrade_security_packages.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_upgrade_security_packages.ts @@ -10,7 +10,7 @@ import { useEffect } from 'react'; import { BOOTSTRAP_PREBUILT_RULES_KEY, useBootstrapPrebuiltRulesMutation, -} from '../api/hooks/use_bootstrap_prebuilt_rules'; +} from '../api/hooks/prebuilt_rules/use_bootstrap_prebuilt_rules'; /** * Install or upgrade the security packages (endpoint and prebuilt rules) diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx index cf5ba8aa5967b..bb949ba436995 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx @@ -166,6 +166,8 @@ export const AddPrebuiltRulesTableContextProvider = ({ rules: [{ rule_id: ruleId, version: rule.version }], enable, }); + } catch { + // Error is handled by the mutation's onError callback, so no need to do anything here } finally { setLoadingRules((prev) => prev.filter((id) => id !== ruleId)); } @@ -182,6 +184,8 @@ export const AddPrebuiltRulesTableContextProvider = ({ setLoadingRules((prev) => [...prev, ...rulesToUpgrade.map((r) => r.rule_id)]); try { await installSpecificRulesRequest({ rules: rulesToUpgrade, enable }); + } catch { + // Error is handled by the mutation's onError callback, so no need to do anything here } finally { setLoadingRules((prev) => prev.filter((id) => !rulesToUpgrade.some((r) => r.rule_id === id)) @@ -197,6 +201,8 @@ export const AddPrebuiltRulesTableContextProvider = ({ setLoadingRules((prev) => [...prev, ...rules.map((r) => r.rule_id)]); try { await installAllRulesRequest(); + } catch { + // Error is handled by the mutation's onError callback, so no need to do anything here } finally { setLoadingRules([]); setSelectedRules([]); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/bootstrap_prebuilt_rules/bootstrap_prebuilt_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/bootstrap_prebuilt_rules/bootstrap_prebuilt_rules.ts index 8d3788a2cf7f8..28c86adfe230c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/bootstrap_prebuilt_rules/bootstrap_prebuilt_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/bootstrap_prebuilt_rules/bootstrap_prebuilt_rules.ts @@ -5,16 +5,11 @@ * 2.0. */ -import { transformError } from '@kbn/securitysolution-es-utils'; -import type { IKibanaResponse } from '@kbn/core/server'; import { BOOTSTRAP_PREBUILT_RULES_URL } from '../../../../../../common/api/detection_engine/prebuilt_rules'; -import type { BootstrapPrebuiltRulesResponse } from '../../../../../../common/api/detection_engine/prebuilt_rules/bootstrap_prebuilt_rules/bootstrap_prebuilt_rules.gen'; import type { SecuritySolutionPluginRouter } from '../../../../../types'; -import { buildSiemResponse } from '../../../routes/utils'; -import { - installEndpointPackage, - installPrebuiltRulesPackage, -} from '../install_prebuilt_rules_and_timelines/install_prebuilt_rules_package'; +import { PREBUILT_RULES_OPERATION_SOCKET_TIMEOUT_MS } from '../../constants'; +import { bootstrapPrebuiltRulesHandler } from './bootstrap_prebuilt_rules_handler'; +import { throttleRequests } from '../../../../../utils/throttle_requests'; export const bootstrapPrebuiltRulesRoute = (router: SecuritySolutionPluginRouter) => { router.versioned @@ -26,43 +21,17 @@ export const bootstrapPrebuiltRulesRoute = (router: SecuritySolutionPluginRouter requiredPrivileges: ['securitySolution'], }, }, + options: { + timeout: { + idleSocket: PREBUILT_RULES_OPERATION_SOCKET_TIMEOUT_MS, + }, + }, }) .addVersion( { version: '1', validate: {}, }, - async (context, _, response): Promise> => { - const siemResponse = buildSiemResponse(response); - - try { - const ctx = await context.resolve(['securitySolution']); - const securityContext = ctx.securitySolution; - const config = securityContext.getConfig(); - - const results = await Promise.all([ - installPrebuiltRulesPackage(config, securityContext), - installEndpointPackage(config, securityContext), - ]); - - const responseBody: BootstrapPrebuiltRulesResponse = { - packages: results.map((result) => ({ - name: result.package.name, - version: result.package.version, - status: result.status, - })), - }; - - return response.ok({ - body: responseBody, - }); - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); - } - } + throttleRequests(bootstrapPrebuiltRulesHandler) ); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/bootstrap_prebuilt_rules/bootstrap_prebuilt_rules_handler.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/bootstrap_prebuilt_rules/bootstrap_prebuilt_rules_handler.ts new file mode 100644 index 0000000000000..77e401689fb8f --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/bootstrap_prebuilt_rules/bootstrap_prebuilt_rules_handler.ts @@ -0,0 +1,58 @@ +/* + * 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 { IKibanaResponse, KibanaRequest, KibanaResponseFactory } from '@kbn/core/server'; +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 type { SecuritySolutionRequestHandlerContext } from '../../../../../types'; +import { buildSiemResponse } from '../../../routes/utils'; +import { + installEndpointPackage, + installPrebuiltRulesPackage, +} from '../install_prebuilt_rules_and_timelines/install_prebuilt_rules_package'; + +export const bootstrapPrebuiltRulesHandler = async ( + context: SecuritySolutionRequestHandlerContext, + _: KibanaRequest, + response: KibanaResponseFactory +): Promise> => { + const siemResponse = buildSiemResponse(response); + + try { + const ctx = await context.resolve(['securitySolution']); + const securityContext = ctx.securitySolution; + const config = securityContext.getConfig(); + + const prebuiltRulesResult = await installPrebuiltRulesPackage(config, securityContext); + const endpointResult = await installEndpointPackage(config, securityContext); + + 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, + }, + ], + }; + + return response.ok({ + body: responseBody, + }); + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_route.ts index 8b4d38bd2f4a4..3dd23b41c6aa8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_route.ts @@ -26,8 +26,12 @@ import { createPrebuiltRules } from '../../logic/rule_objects/create_prebuilt_ru import { createPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebuilt_rule_objects_client'; import { fetchRuleVersionsTriad } from '../../logic/rule_versions/fetch_rule_versions_triad'; import { performTimelinesInstallation } from '../../logic/perform_timelines_installation'; -import { PREBUILT_RULES_OPERATION_SOCKET_TIMEOUT_MS } from '../../constants'; +import { + PREBUILT_RULES_OPERATION_SOCKET_TIMEOUT_MS, + PREBUILT_RULES_OPERATION_CONCURRENCY, +} from '../../constants'; import { getRuleGroups } from '../../model/rule_groups/get_rule_groups'; +import { routeLimitedConcurrencyTag } from '../../../../../utils/route_limited_concurrency_tag'; export const performRuleInstallationRoute = (router: SecuritySolutionPluginRouter) => { router.versioned @@ -40,6 +44,7 @@ export const performRuleInstallationRoute = (router: SecuritySolutionPluginRoute }, }, options: { + tags: [routeLimitedConcurrencyTag(PREBUILT_RULES_OPERATION_CONCURRENCY)], timeout: { idleSocket: PREBUILT_RULES_OPERATION_SOCKET_TIMEOUT_MS, }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_route.ts index db5f7a186d303..dd0fd60bc2d9e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_route.ts @@ -21,11 +21,15 @@ import { createPrebuiltRuleAssetsClient } from '../../logic/rule_assets/prebuilt import { createPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebuilt_rule_objects_client'; import { upgradePrebuiltRules } from '../../logic/rule_objects/upgrade_prebuilt_rules'; import { fetchRuleVersionsTriad } from '../../logic/rule_versions/fetch_rule_versions_triad'; -import { PREBUILT_RULES_OPERATION_SOCKET_TIMEOUT_MS } from '../../constants'; +import { + PREBUILT_RULES_OPERATION_SOCKET_TIMEOUT_MS, + PREBUILT_RULES_OPERATION_CONCURRENCY, +} from '../../constants'; import { getUpgradeableRules } from './get_upgradeable_rules'; import { createModifiedPrebuiltRuleAssets } from './create_upgradeable_rules_payload'; import { getRuleGroups } from '../../model/rule_groups/get_rule_groups'; import type { ConfigType } from '../../../../../config'; +import { routeLimitedConcurrencyTag } from '../../../../../utils/route_limited_concurrency_tag'; export const performRuleUpgradeRoute = ( router: SecuritySolutionPluginRouter, @@ -41,6 +45,7 @@ export const performRuleUpgradeRoute = ( }, }, options: { + tags: [routeLimitedConcurrencyTag(PREBUILT_RULES_OPERATION_CONCURRENCY)], timeout: { idleSocket: PREBUILT_RULES_OPERATION_SOCKET_TIMEOUT_MS, }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_installation/review_rule_installation_handler.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_installation/review_rule_installation_handler.ts new file mode 100644 index 0000000000000..4d35b98718a98 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_installation/review_rule_installation_handler.ts @@ -0,0 +1,71 @@ +/* + * 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 { KibanaRequest, KibanaResponseFactory } from '@kbn/core/server'; +import { transformError } from '@kbn/securitysolution-es-utils'; +import type { + ReviewRuleInstallationResponseBody, + RuleInstallationStatsForReview, +} from '../../../../../../common/api/detection_engine/prebuilt_rules'; +import type { SecuritySolutionRequestHandlerContext } from '../../../../../types'; +import { buildSiemResponse } from '../../../routes/utils'; +import { convertPrebuiltRuleAssetToRuleResponse } from '../../../rule_management/logic/detection_rules_client/converters/convert_prebuilt_rule_asset_to_rule_response'; +import { createPrebuiltRuleAssetsClient } from '../../logic/rule_assets/prebuilt_rule_assets_client'; +import { createPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebuilt_rule_objects_client'; +import { fetchRuleVersionsTriad } from '../../logic/rule_versions/fetch_rule_versions_triad'; +import type { PrebuiltRuleAsset } from '../../model/rule_assets/prebuilt_rule_asset'; +import { getRuleGroups } from '../../model/rule_groups/get_rule_groups'; + +export const reviewRuleInstallationHandler = async ( + context: SecuritySolutionRequestHandlerContext, + request: KibanaRequest, + response: KibanaResponseFactory +) => { + const siemResponse = buildSiemResponse(response); + + try { + const ctx = await context.resolve(['core', 'alerting']); + const soClient = ctx.core.savedObjects.client; + const rulesClient = await ctx.alerting.getRulesClient(); + const ruleAssetsClient = createPrebuiltRuleAssetsClient(soClient); + const ruleObjectsClient = createPrebuiltRuleObjectsClient(rulesClient); + + const ruleVersionsMap = await fetchRuleVersionsTriad({ + ruleAssetsClient, + ruleObjectsClient, + }); + const { installableRules } = getRuleGroups(ruleVersionsMap); + + const body: ReviewRuleInstallationResponseBody = { + stats: calculateRuleStats(installableRules), + rules: installableRules.map((prebuiltRuleAsset) => + convertPrebuiltRuleAssetToRuleResponse(prebuiltRuleAsset) + ), + }; + + return response.ok({ body }); + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } +}; +const getAggregatedTags = (rules: PrebuiltRuleAsset[]): string[] => { + const set = new Set(rules.flatMap((rule) => rule.tags || [])); + return Array.from(set.values()).sort((a, b) => a.localeCompare(b)); +}; +const calculateRuleStats = ( + rulesToInstall: PrebuiltRuleAsset[] +): RuleInstallationStatsForReview => { + const tagsOfRulesToInstall = getAggregatedTags(rulesToInstall); + return { + num_rules_to_install: rulesToInstall.length, + tags: tagsOfRulesToInstall, + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_installation/review_rule_installation_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_installation/review_rule_installation_route.ts index c1c45532f61bb..7ae1e70c43a7d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_installation/review_rule_installation_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_installation/review_rule_installation_route.ts @@ -5,21 +5,14 @@ * 2.0. */ -import { transformError } from '@kbn/securitysolution-es-utils'; import { REVIEW_RULE_INSTALLATION_URL } from '../../../../../../common/api/detection_engine/prebuilt_rules'; -import type { - ReviewRuleInstallationResponseBody, - RuleInstallationStatsForReview, -} from '../../../../../../common/api/detection_engine/prebuilt_rules'; import type { SecuritySolutionPluginRouter } from '../../../../../types'; -import { buildSiemResponse } from '../../../routes/utils'; -import { createPrebuiltRuleAssetsClient } from '../../logic/rule_assets/prebuilt_rule_assets_client'; -import { createPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebuilt_rule_objects_client'; -import { fetchRuleVersionsTriad } from '../../logic/rule_versions/fetch_rule_versions_triad'; -import type { PrebuiltRuleAsset } from '../../model/rule_assets/prebuilt_rule_asset'; -import { convertPrebuiltRuleAssetToRuleResponse } from '../../../rule_management/logic/detection_rules_client/converters/convert_prebuilt_rule_asset_to_rule_response'; -import { PREBUILT_RULES_OPERATION_SOCKET_TIMEOUT_MS } from '../../constants'; -import { getRuleGroups } from '../../model/rule_groups/get_rule_groups'; +import { routeLimitedConcurrencyTag } from '../../../../../utils/route_limited_concurrency_tag'; +import { + PREBUILT_RULES_OPERATION_CONCURRENCY, + PREBUILT_RULES_OPERATION_SOCKET_TIMEOUT_MS, +} from '../../constants'; +import { reviewRuleInstallationHandler } from './review_rule_installation_handler'; export const reviewRuleInstallationRoute = (router: SecuritySolutionPluginRouter) => { router.versioned @@ -32,6 +25,7 @@ export const reviewRuleInstallationRoute = (router: SecuritySolutionPluginRouter }, }, options: { + tags: [routeLimitedConcurrencyTag(PREBUILT_RULES_OPERATION_CONCURRENCY)], timeout: { idleSocket: PREBUILT_RULES_OPERATION_SOCKET_TIMEOUT_MS, }, @@ -42,52 +36,6 @@ export const reviewRuleInstallationRoute = (router: SecuritySolutionPluginRouter version: '1', validate: {}, }, - async (context, request, response) => { - const siemResponse = buildSiemResponse(response); - - try { - const ctx = await context.resolve(['core', 'alerting']); - const soClient = ctx.core.savedObjects.client; - const rulesClient = ctx.alerting.getRulesClient(); - const ruleAssetsClient = createPrebuiltRuleAssetsClient(soClient); - const ruleObjectsClient = createPrebuiltRuleObjectsClient(rulesClient); - - const ruleVersionsMap = await fetchRuleVersionsTriad({ - ruleAssetsClient, - ruleObjectsClient, - }); - const { installableRules } = getRuleGroups(ruleVersionsMap); - - const body: ReviewRuleInstallationResponseBody = { - stats: calculateRuleStats(installableRules), - rules: installableRules.map((prebuiltRuleAsset) => - convertPrebuiltRuleAssetToRuleResponse(prebuiltRuleAsset) - ), - }; - - return response.ok({ body }); - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); - } - } + reviewRuleInstallationHandler ); }; - -const getAggregatedTags = (rules: PrebuiltRuleAsset[]): string[] => { - const set = new Set(rules.flatMap((rule) => rule.tags || [])); - return Array.from(set.values()).sort((a, b) => a.localeCompare(b)); -}; - -const calculateRuleStats = ( - rulesToInstall: PrebuiltRuleAsset[] -): RuleInstallationStatsForReview => { - const tagsOfRulesToInstall = getAggregatedTags(rulesToInstall); - return { - num_rules_to_install: rulesToInstall.length, - tags: tagsOfRulesToInstall, - }; -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_handler.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_handler.ts new file mode 100644 index 0000000000000..9f4fa7ddc766e --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_handler.ts @@ -0,0 +1,138 @@ +/* + * 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 { KibanaRequest, KibanaResponseFactory } from '@kbn/core/server'; +import { transformError } from '@kbn/securitysolution-es-utils'; +import { pickBy } from 'lodash'; +import type { RuleResponse } from '../../../../../../common/api/detection_engine/model/rule_schema'; +import type { + ReviewRuleUpgradeResponseBody, + RuleUpgradeInfoForReview, + RuleUpgradeStatsForReview, + ThreeWayDiff, +} from '../../../../../../common/api/detection_engine/prebuilt_rules'; +import { ThreeWayDiffOutcome } from '../../../../../../common/api/detection_engine/prebuilt_rules'; +import { invariant } from '../../../../../../common/utils/invariant'; +import type { SecuritySolutionRequestHandlerContext } from '../../../../../types'; +import { buildSiemResponse } from '../../../routes/utils'; +import { convertPrebuiltRuleAssetToRuleResponse } from '../../../rule_management/logic/detection_rules_client/converters/convert_prebuilt_rule_asset_to_rule_response'; +import type { CalculateRuleDiffResult } from '../../logic/diff/calculate_rule_diff'; +import { calculateRuleDiff } from '../../logic/diff/calculate_rule_diff'; +import { createPrebuiltRuleAssetsClient } from '../../logic/rule_assets/prebuilt_rule_assets_client'; +import { createPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebuilt_rule_objects_client'; +import { fetchRuleVersionsTriad } from '../../logic/rule_versions/fetch_rule_versions_triad'; +import { getRuleGroups } from '../../model/rule_groups/get_rule_groups'; + +export const reviewRuleUpgradeHandler = async ( + context: SecuritySolutionRequestHandlerContext, + request: KibanaRequest, + response: KibanaResponseFactory +) => { + const siemResponse = buildSiemResponse(response); + + try { + const ctx = await context.resolve(['core', 'alerting']); + const soClient = ctx.core.savedObjects.client; + const rulesClient = await ctx.alerting.getRulesClient(); + const ruleAssetsClient = createPrebuiltRuleAssetsClient(soClient); + const ruleObjectsClient = createPrebuiltRuleObjectsClient(rulesClient); + + const ruleVersionsMap = await fetchRuleVersionsTriad({ + ruleAssetsClient, + ruleObjectsClient, + }); + const { upgradeableRules } = getRuleGroups(ruleVersionsMap); + + const ruleDiffCalculationResults = upgradeableRules.map(({ current }) => { + const ruleVersions = ruleVersionsMap.get(current.rule_id); + invariant(ruleVersions != null, 'ruleVersions not found'); + return calculateRuleDiff(ruleVersions); + }); + + const body: ReviewRuleUpgradeResponseBody = { + stats: calculateRuleStats(ruleDiffCalculationResults), + rules: calculateRuleInfos(ruleDiffCalculationResults), + }; + + return response.ok({ body }); + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } +}; +const calculateRuleStats = (results: CalculateRuleDiffResult[]): RuleUpgradeStatsForReview => { + const allTags = new Set(); + + const stats = results.reduce( + (acc, result) => { + acc.num_rules_to_upgrade_total += 1; + + if (result.ruleDiff.num_fields_with_conflicts > 0) { + acc.num_rules_with_conflicts += 1; + } + + if (result.ruleDiff.num_fields_with_non_solvable_conflicts > 0) { + acc.num_rules_with_non_solvable_conflicts += 1; + } + + result.ruleVersions.input.current?.tags.forEach((tag) => allTags.add(tag)); + + return acc; + }, + { + num_rules_to_upgrade_total: 0, + num_rules_with_conflicts: 0, + num_rules_with_non_solvable_conflicts: 0, + } + ); + + return { + ...stats, + tags: Array.from(allTags), + }; +}; +const calculateRuleInfos = (results: CalculateRuleDiffResult[]): RuleUpgradeInfoForReview[] => { + return results.map((result) => { + const { ruleDiff, ruleVersions } = result; + const installedCurrentVersion = ruleVersions.input.current; + const targetVersion = ruleVersions.input.target; + invariant(installedCurrentVersion != null, 'installedCurrentVersion not found'); + invariant(targetVersion != null, 'targetVersion not found'); + + const targetRule: RuleResponse = { + ...convertPrebuiltRuleAssetToRuleResponse(targetVersion), + id: installedCurrentVersion.id, + revision: installedCurrentVersion.revision + 1, + created_at: installedCurrentVersion.created_at, + created_by: installedCurrentVersion.created_by, + updated_at: new Date().toISOString(), + updated_by: installedCurrentVersion.updated_by, + }; + + return { + id: installedCurrentVersion.id, + rule_id: installedCurrentVersion.rule_id, + revision: installedCurrentVersion.revision, + current_rule: installedCurrentVersion, + target_rule: targetRule, + diff: { + fields: pickBy>( + ruleDiff.fields, + (fieldDiff) => + fieldDiff.diff_outcome !== ThreeWayDiffOutcome.StockValueNoUpdate && + fieldDiff.diff_outcome !== ThreeWayDiffOutcome.MissingBaseNoUpdate + ), + num_fields_with_updates: ruleDiff.num_fields_with_updates, + num_fields_with_conflicts: ruleDiff.num_fields_with_conflicts, + num_fields_with_non_solvable_conflicts: ruleDiff.num_fields_with_non_solvable_conflicts, + }, + }; + }); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_route.ts index 3da62bd9bb21d..54f51752b7ab8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_route.ts @@ -5,30 +5,14 @@ * 2.0. */ -import { transformError } from '@kbn/securitysolution-es-utils'; -import { pickBy } from 'lodash'; -import { - REVIEW_RULE_UPGRADE_URL, - ThreeWayDiffOutcome, -} from '../../../../../../common/api/detection_engine/prebuilt_rules'; -import type { - ReviewRuleUpgradeResponseBody, - RuleUpgradeInfoForReview, - RuleUpgradeStatsForReview, - ThreeWayDiff, -} from '../../../../../../common/api/detection_engine/prebuilt_rules'; -import { invariant } from '../../../../../../common/utils/invariant'; -import type { RuleResponse } from '../../../../../../common/api/detection_engine/model/rule_schema'; +import { REVIEW_RULE_UPGRADE_URL } from '../../../../../../common/api/detection_engine/prebuilt_rules'; import type { SecuritySolutionPluginRouter } from '../../../../../types'; -import { buildSiemResponse } from '../../../routes/utils'; -import type { CalculateRuleDiffResult } from '../../logic/diff/calculate_rule_diff'; -import { calculateRuleDiff } from '../../logic/diff/calculate_rule_diff'; -import { createPrebuiltRuleAssetsClient } from '../../logic/rule_assets/prebuilt_rule_assets_client'; -import { createPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebuilt_rule_objects_client'; -import { fetchRuleVersionsTriad } from '../../logic/rule_versions/fetch_rule_versions_triad'; -import { convertPrebuiltRuleAssetToRuleResponse } from '../../../rule_management/logic/detection_rules_client/converters/convert_prebuilt_rule_asset_to_rule_response'; -import { PREBUILT_RULES_OPERATION_SOCKET_TIMEOUT_MS } from '../../constants'; -import { getRuleGroups } from '../../model/rule_groups/get_rule_groups'; +import { routeLimitedConcurrencyTag } from '../../../../../utils/route_limited_concurrency_tag'; +import { + PREBUILT_RULES_OPERATION_CONCURRENCY, + PREBUILT_RULES_OPERATION_SOCKET_TIMEOUT_MS, +} from '../../constants'; +import { reviewRuleUpgradeHandler } from './review_rule_upgrade_handler'; export const reviewRuleUpgradeRoute = (router: SecuritySolutionPluginRouter) => { router.versioned @@ -41,6 +25,7 @@ export const reviewRuleUpgradeRoute = (router: SecuritySolutionPluginRouter) => }, }, options: { + tags: [routeLimitedConcurrencyTag(PREBUILT_RULES_OPERATION_CONCURRENCY)], timeout: { idleSocket: PREBUILT_RULES_OPERATION_SOCKET_TIMEOUT_MS, }, @@ -51,112 +36,6 @@ export const reviewRuleUpgradeRoute = (router: SecuritySolutionPluginRouter) => version: '1', validate: {}, }, - async (context, request, response) => { - const siemResponse = buildSiemResponse(response); - - try { - const ctx = await context.resolve(['core', 'alerting']); - const soClient = ctx.core.savedObjects.client; - const rulesClient = ctx.alerting.getRulesClient(); - const ruleAssetsClient = createPrebuiltRuleAssetsClient(soClient); - const ruleObjectsClient = createPrebuiltRuleObjectsClient(rulesClient); - - const ruleVersionsMap = await fetchRuleVersionsTriad({ - ruleAssetsClient, - ruleObjectsClient, - }); - const { upgradeableRules } = getRuleGroups(ruleVersionsMap); - - const ruleDiffCalculationResults = upgradeableRules.map(({ current }) => { - const ruleVersions = ruleVersionsMap.get(current.rule_id); - invariant(ruleVersions != null, 'ruleVersions not found'); - return calculateRuleDiff(ruleVersions); - }); - - const body: ReviewRuleUpgradeResponseBody = { - stats: calculateRuleStats(ruleDiffCalculationResults), - rules: calculateRuleInfos(ruleDiffCalculationResults), - }; - - return response.ok({ body }); - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); - } - } + reviewRuleUpgradeHandler ); }; - -const calculateRuleStats = (results: CalculateRuleDiffResult[]): RuleUpgradeStatsForReview => { - const allTags = new Set(); - - const stats = results.reduce( - (acc, result) => { - acc.num_rules_to_upgrade_total += 1; - - if (result.ruleDiff.num_fields_with_conflicts > 0) { - acc.num_rules_with_conflicts += 1; - } - - if (result.ruleDiff.num_fields_with_non_solvable_conflicts > 0) { - acc.num_rules_with_non_solvable_conflicts += 1; - } - - result.ruleVersions.input.current?.tags.forEach((tag) => allTags.add(tag)); - - return acc; - }, - { - num_rules_to_upgrade_total: 0, - num_rules_with_conflicts: 0, - num_rules_with_non_solvable_conflicts: 0, - } - ); - - return { - ...stats, - tags: Array.from(allTags), - }; -}; - -const calculateRuleInfos = (results: CalculateRuleDiffResult[]): RuleUpgradeInfoForReview[] => { - return results.map((result) => { - const { ruleDiff, ruleVersions } = result; - const installedCurrentVersion = ruleVersions.input.current; - const targetVersion = ruleVersions.input.target; - invariant(installedCurrentVersion != null, 'installedCurrentVersion not found'); - invariant(targetVersion != null, 'targetVersion not found'); - - const targetRule: RuleResponse = { - ...convertPrebuiltRuleAssetToRuleResponse(targetVersion), - id: installedCurrentVersion.id, - revision: installedCurrentVersion.revision + 1, - created_at: installedCurrentVersion.created_at, - created_by: installedCurrentVersion.created_by, - updated_at: new Date().toISOString(), - updated_by: installedCurrentVersion.updated_by, - }; - - return { - id: installedCurrentVersion.id, - rule_id: installedCurrentVersion.rule_id, - revision: installedCurrentVersion.revision, - current_rule: installedCurrentVersion, - target_rule: targetRule, - diff: { - fields: pickBy>( - ruleDiff.fields, - (fieldDiff) => - fieldDiff.diff_outcome !== ThreeWayDiffOutcome.StockValueNoUpdate && - fieldDiff.diff_outcome !== ThreeWayDiffOutcome.MissingBaseNoUpdate - ), - num_fields_with_updates: ruleDiff.num_fields_with_updates, - num_fields_with_conflicts: ruleDiff.num_fields_with_conflicts, - num_fields_with_non_solvable_conflicts: ruleDiff.num_fields_with_non_solvable_conflicts, - }, - }; - }); -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/constants.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/constants.ts index 1024b3e0726cd..7d281ae96e321 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/constants.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/constants.ts @@ -5,4 +5,8 @@ * 2.0. */ -export const PREBUILT_RULES_OPERATION_SOCKET_TIMEOUT_MS = 1800000 as const; // 30 minutes +export const PREBUILT_RULES_OPERATION_SOCKET_TIMEOUT_MS = 1_800_000 as const; // 30 minutes + +// Only one rule installation or upgrade request can be processed at a time. +// Multiple requests can lead to high memory usage and unexpected behavior. +export const PREBUILT_RULES_OPERATION_CONCURRENCY = 1; diff --git a/x-pack/plugins/security_solution/server/utils/throttle_requests.test.ts b/x-pack/plugins/security_solution/server/utils/throttle_requests.test.ts new file mode 100644 index 0000000000000..3389e48e6ebd3 --- /dev/null +++ b/x-pack/plugins/security_solution/server/utils/throttle_requests.test.ts @@ -0,0 +1,96 @@ +/* + * 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 { IKibanaResponse, KibanaRequest, KibanaResponseFactory } from '@kbn/core/server'; +import type { MaybePromise } from '@kbn/utility-types'; +import type { + SecuritySolutionApiRequestHandlerContext, + SecuritySolutionRequestHandlerContext, +} from '../types'; +import { throttleRequests } from './throttle_requests'; + +describe('throttleRequests', () => { + let mockContext: SecuritySolutionRequestHandlerContext; + let mockRequest: KibanaRequest; + let mockResponse: KibanaResponseFactory; + let mockHandler: jest.Mock>; + + beforeEach(() => { + mockContext = {} as SecuritySolutionRequestHandlerContext; + mockRequest = {} as KibanaRequest; + mockResponse = {} as KibanaResponseFactory; + mockHandler = jest.fn(); + }); + + it('should call the route handler if no request is running', async () => { + const throttledHandler = throttleRequests(mockHandler); + mockHandler.mockResolvedValueOnce({} as IKibanaResponse); + + await throttledHandler(mockContext, mockRequest, mockResponse); + + expect(mockHandler).toHaveBeenCalledWith(mockContext, mockRequest, mockResponse); + }); + + it('should not call the route handler if a request is already running', async () => { + const throttledHandler = throttleRequests(mockHandler); + mockHandler.mockResolvedValueOnce( + new Promise((resolve) => setTimeout(() => resolve({} as IKibanaResponse), 100)) + ); + + // Call the handler concurrently + void throttledHandler(mockContext, mockRequest, mockResponse); + await throttledHandler(mockContext, mockRequest, mockResponse); + + expect(mockHandler).toHaveBeenCalledTimes(1); + }); + + it('should call the route handler multiple times on consecutive requests', async () => { + const throttledHandler = throttleRequests(mockHandler); + mockHandler.mockResolvedValueOnce({} as IKibanaResponse); + + // Call the handler sequentially + await throttledHandler(mockContext, mockRequest, mockResponse); + await throttledHandler(mockContext, mockRequest, mockResponse); + + expect(mockHandler).toHaveBeenCalledTimes(2); + }); + + it('should handle a failed route handler', async () => { + const throttledHandler = throttleRequests(mockHandler); + const error = new Error('Handler failed'); + mockHandler.mockRejectedValueOnce(error); + + await expect(throttledHandler(mockContext, mockRequest, mockResponse)).rejects.toThrow( + 'Handler failed' + ); + + // Ensure that the next call can proceed after a failure + const resolvedValue = {} as IKibanaResponse; + mockHandler.mockResolvedValueOnce(resolvedValue); + const result = await throttledHandler(mockContext, mockRequest, mockResponse); + + expect(mockHandler).toHaveBeenCalledTimes(2); + expect(result).toBe(resolvedValue); + }); + + it('should not throttle requests across different spaces when spaceAware is true', async () => { + const mockSpaceId1 = 'space-1'; + const mockSpaceId2 = 'space-2'; + mockContext.securitySolution = { + getSpaceId: jest.fn().mockResolvedValueOnce(mockSpaceId1).mockResolvedValueOnce(mockSpaceId2), + } as unknown as Promise; + + const throttledHandler = throttleRequests(mockHandler, { spaceAware: true }); + mockHandler.mockResolvedValue({} as IKibanaResponse); + + // Call the handler concurrently with different space IDs + void throttledHandler(mockContext, mockRequest, mockResponse); + await throttledHandler(mockContext, mockRequest, mockResponse); + + expect(mockHandler).toHaveBeenCalledTimes(2); + }); +}); diff --git a/x-pack/plugins/security_solution/server/utils/throttle_requests.ts b/x-pack/plugins/security_solution/server/utils/throttle_requests.ts new file mode 100644 index 0000000000000..85d895eef3569 --- /dev/null +++ b/x-pack/plugins/security_solution/server/utils/throttle_requests.ts @@ -0,0 +1,61 @@ +/* + * 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 { IKibanaResponse, KibanaRequest, KibanaResponseFactory } from '@kbn/core/server'; +import type { MaybePromise } from '@kbn/utility-types'; +import type { SecuritySolutionRequestHandlerContext } from '../types'; + +type RouteHandlerParams = [ + context: SecuritySolutionRequestHandlerContext, + request: KibanaRequest, + response: KibanaResponseFactory +]; + +interface ThrottleOptions { + spaceAware?: boolean; +} + +/** + * Throttles requests to ensure that only one request is processed at a time. + * Concurrent requests will be deduplicated and will share the same response. + * + * Optionally, it can be space-aware, meaning it will throttle requests based on + * the space ID. + * + * Note: This function is not suitable for routes that accept parameters in the + * request body. It might also lead to high memory usage in case of big response + * payloads and many concurrent requests. + * + * @param routeHandler - The route handler function to be throttled. + * @param options - Throttle options. + * @param options.spaceAware - If true, throttles requests based on the space + * ID. + * @returns A throttled version of the route handler. + */ +export const throttleRequests = ( + routeHandler: (...params: RouteHandlerParams) => MaybePromise, + { spaceAware = false }: ThrottleOptions = {} +) => { + const runningRequests = new Map>(); + + return async (...params: RouteHandlerParams) => { + const spaceId = spaceAware ? (await params[0].securitySolution).getSpaceId() : 'default'; + + let currentRequest = runningRequests.get(spaceId); + if (!currentRequest) { + // There is no running request for this space, so we can start a new one + currentRequest = routeHandler(...params); + runningRequests.set(spaceId, currentRequest); + } + + try { + return await currentRequest; + } finally { + runningRequests.delete(spaceId); + } + }; +};