diff --git a/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/response_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/response_schema.ts index a60311f40b0a2..3d899efdd04bd 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/response_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/response_schema.ts @@ -6,12 +6,8 @@ */ export interface GetPrebuiltRulesStatusResponseBody { - status_code: number; - message: string; - attributes: { - /** Aggregated info about all prebuilt rules */ - stats: PrebuiltRulesStatusStats; - }; + /** Aggregated info about all prebuilt rules */ + stats: PrebuiltRulesStatusStats; } export interface PrebuiltRulesStatusStats { diff --git a/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_request_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_request_schema.ts new file mode 100644 index 0000000000000..0fede224254e6 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_request_schema.ts @@ -0,0 +1,37 @@ +/* + * 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 * as t from 'io-ts'; + +export const RuleVersionSpecifier = t.exact( + t.type({ + rule_id: t.string, + version: t.number, + }) +); +export type RuleVersionSpecifier = t.TypeOf; + +export const InstallSpecificRulesRequest = t.exact( + t.type({ + mode: t.literal(`SPECIFIC_RULES`), + rules: t.array(RuleVersionSpecifier), + }) +); + +export const InstallAllRulesRequest = t.exact( + t.type({ + mode: t.literal(`ALL_RULES`), + }) +); + +export const PerformRuleInstallationRequestBody = t.union([ + InstallAllRulesRequest, + InstallSpecificRulesRequest, +]); + +export type PerformRuleInstallationRequestBody = t.TypeOf< + typeof PerformRuleInstallationRequestBody +>; diff --git a/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_response_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_response_schema.ts new file mode 100644 index 0000000000000..fdd51dc256333 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_response_schema.ts @@ -0,0 +1,32 @@ +/* + * 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 { RuleResponse } from '../../../rule_schema/model/rule_schemas'; +import type { AggregatedPrebuiltRuleError } from '../../model/prebuilt_rules/aggregated_prebuilt_rules_error'; + +export enum SkipRuleInstallReason { + ALREADY_INSTALLED = 'ALREADY_INSTALLED', +} + +export interface SkippedRuleInstall { + rule_id: string; + reason: SkipRuleInstallReason; +} + +export interface PerformRuleInstallationResponseBody { + summary: { + total: number; + succeeded: number; + skipped: number; + failed: number; + }; + results: { + created: RuleResponse[]; + skipped: SkippedRuleInstall[]; + }; + errors: AggregatedPrebuiltRuleError[]; +} diff --git a/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_request_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_request_schema.ts new file mode 100644 index 0000000000000..e0cfe9427dc97 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_request_schema.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 { enumeration } from '@kbn/securitysolution-io-ts-types'; +import * as t from 'io-ts'; + +export enum PickVersionValues { + BASE = 'BASE', + CURRENT = 'CURRENT', + TARGET = 'TARGET', +} + +export const TPickVersionValues = enumeration('PickVersionValues', PickVersionValues); + +export const RuleUpgradeSpecifier = t.exact( + t.intersection([ + t.type({ + rule_id: t.string, + /** + * This parameter is needed for handling race conditions with Optimistic Concurrency Control. + * Two or more users can call installation/_review and installation/_perform endpoints concurrently. + * Also, in general the time between these two calls can be anything. + * The idea is to only allow the user to install a rule if the user has reviewed the exact version + * of it that had been returned from the _review endpoint. If the version changed on the BE, + * installation/_perform endpoint will return a version mismatch error for this rule. + */ + revision: t.number, + /** + * The target version to upgrade to. + */ + version: t.number, + }), + t.partial({ + pick_version: TPickVersionValues, + }), + ]) +); +export type RuleUpgradeSpecifier = t.TypeOf; + +export const UpgradeSpecificRulesRequest = t.exact( + t.intersection([ + t.type({ + mode: t.literal(`SPECIFIC_RULES`), + rules: t.array(RuleUpgradeSpecifier), + }), + t.partial({ + pick_version: TPickVersionValues, + }), + ]) +); + +export const UpgradeAllRulesRequest = t.exact( + t.intersection([ + t.type({ + mode: t.literal(`ALL_RULES`), + }), + t.partial({ + pick_version: TPickVersionValues, + }), + ]) +); + +export const PerformRuleUpgradeRequestBody = t.union([ + UpgradeAllRulesRequest, + UpgradeSpecificRulesRequest, +]); +export type PerformRuleUpgradeRequestBody = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_response_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_response_schema.ts new file mode 100644 index 0000000000000..875d9fe42e790 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_response_schema.ts @@ -0,0 +1,32 @@ +/* + * 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 { RuleResponse } from '../../../rule_schema'; +import type { AggregatedPrebuiltRuleError } from '../../model/prebuilt_rules/aggregated_prebuilt_rules_error'; + +export enum SkipRuleUpgradeReason { + RULE_UP_TO_DATE = 'RULE_UP_TO_DATE', +} + +export interface SkippedRuleUpgrade { + rule_id: string; + reason: SkipRuleUpgradeReason; +} + +export interface PerformRuleUpgradeResponseBody { + summary: { + total: number; + succeeded: number; + skipped: number; + failed: number; + }; + results: { + updated: RuleResponse[]; + skipped: SkippedRuleUpgrade[]; + }; + errors: AggregatedPrebuiltRuleError[]; +} diff --git a/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/api/review_rule_installation/response_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/api/review_rule_installation/response_schema.ts index 999e58c524883..49757b80630a7 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/api/review_rule_installation/response_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/api/review_rule_installation/response_schema.ts @@ -9,15 +9,11 @@ import type { RuleSignatureId, RuleTagArray, RuleVersion } from '../../../rule_s import type { DiffableRule } from '../../model/diff/diffable_rule/diffable_rule'; export interface ReviewRuleInstallationResponseBody { - status_code: number; - message: string; - attributes: { - /** Aggregated info about all rules available for installation */ - stats: RuleInstallationStatsForReview; + /** Aggregated info about all rules available for installation */ + stats: RuleInstallationStatsForReview; - /** Info about individual rules: one object per each rule available for installation */ - rules: RuleInstallationInfoForReview[]; - }; + /** Info about individual rules: one object per each rule available for installation */ + rules: RuleInstallationInfoForReview[]; } export interface RuleInstallationStatsForReview { diff --git a/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/api/review_rule_upgrade/response_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/api/review_rule_upgrade/response_schema.ts index 35994a7963956..06c45008caf6e 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/api/review_rule_upgrade/response_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/api/review_rule_upgrade/response_schema.ts @@ -10,15 +10,11 @@ import type { DiffableRule } from '../../model/diff/diffable_rule/diffable_rule' import type { PartialRuleDiff } from '../../model/diff/rule_diff/rule_diff'; export interface ReviewRuleUpgradeResponseBody { - status_code: number; - message: string; - attributes: { - /** Aggregated info about all rules available for upgrade */ - stats: RuleUpgradeStatsForReview; + /** Aggregated info about all rules available for upgrade */ + stats: RuleUpgradeStatsForReview; - /** Info about individual rules: one object per each rule available for upgrade */ - rules: RuleUpgradeInfoForReview[]; - }; + /** Info about individual rules: one object per each rule available for upgrade */ + rules: RuleUpgradeInfoForReview[]; } export interface RuleUpgradeStatsForReview { diff --git a/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/model/prebuilt_rules/aggregated_prebuilt_rules_error.ts b/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/model/prebuilt_rules/aggregated_prebuilt_rules_error.ts new file mode 100644 index 0000000000000..9a90a69c43fe5 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/model/prebuilt_rules/aggregated_prebuilt_rules_error.ts @@ -0,0 +1,15 @@ +/* + * 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. + */ + +export interface AggregatedPrebuiltRuleError { + message: string; + status_code?: number; + rules: Array<{ + rule_id: string; + name?: string; + }>; +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/generate_assets/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/generate_assets/generate_assets_route.ts similarity index 100% rename from x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/generate_assets/route.ts rename to x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/generate_assets/generate_assets_route.ts diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_and_timelines_status/route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_and_timelines_status/get_prebuilt_rules_and_timelines_status_route.test.ts similarity index 98% rename from x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_and_timelines_status/route.test.ts rename to x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_and_timelines_status/get_prebuilt_rules_and_timelines_status_route.test.ts index 64206b257d9f2..9e9bd77e81a1c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_and_timelines_status/route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_and_timelines_status/get_prebuilt_rules_and_timelines_status_route.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { getPrebuiltRulesAndTimelinesStatusRoute } from './route'; +import { getPrebuiltRulesAndTimelinesStatusRoute } from './get_prebuilt_rules_and_timelines_status_route'; import { getEmptyFindResult, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_and_timelines_status/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_and_timelines_status/get_prebuilt_rules_and_timelines_status_route.ts similarity index 100% rename from x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_and_timelines_status/route.ts rename to x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_and_timelines_status/get_prebuilt_rules_and_timelines_status_route.ts diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/get_prebuilt_rules_status_route.ts similarity index 62% rename from x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/route.ts rename to x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/get_prebuilt_rules_status_route.ts index d1bdcfc80e757..0c64c496e5611 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/get_prebuilt_rules_status_route.ts @@ -6,19 +6,13 @@ */ import { transformError } from '@kbn/securitysolution-es-utils'; - import { GET_PREBUILT_RULES_STATUS_URL } from '../../../../../../common/detection_engine/prebuilt_rules'; -import type { - GetPrebuiltRulesStatusResponseBody, - PrebuiltRulesStatusStats, -} from '../../../../../../common/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/response_schema'; - +import type { GetPrebuiltRulesStatusResponseBody } from '../../../../../../common/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/response_schema'; 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 type { VersionBuckets } from '../../model/rule_versions/get_version_buckets'; +import { fetchRuleVersionsTriad } from '../../logic/rule_versions/fetch_rule_versions_triad'; import { getVersionBuckets } from '../../model/rule_versions/get_version_buckets'; export const getPrebuiltRulesStatusRoute = (router: SecuritySolutionPluginRouter) => { @@ -40,23 +34,18 @@ export const getPrebuiltRulesStatusRoute = (router: SecuritySolutionPluginRouter const ruleAssetsClient = createPrebuiltRuleAssetsClient(soClient); const ruleObjectsClient = createPrebuiltRuleObjectsClient(rulesClient); - const [latestVersions, { installedVersions }] = await Promise.all([ - ruleAssetsClient.fetchLatestVersions(), - ruleObjectsClient.fetchInstalledRules(), - ]); - - const versionBuckets = getVersionBuckets({ - latestVersions, - installedVersions, + const ruleVersionsMap = await fetchRuleVersionsTriad({ + ruleAssetsClient, + ruleObjectsClient, }); - - const stats = calculateRuleStats(versionBuckets); + const { currentRules, installableRules, upgradeableRules } = + getVersionBuckets(ruleVersionsMap); const body: GetPrebuiltRulesStatusResponseBody = { - status_code: 200, - message: 'OK', - attributes: { - stats, + stats: { + num_prebuilt_rules_installed: currentRules.length, + num_prebuilt_rules_to_install: installableRules.length, + num_prebuilt_rules_to_upgrade: upgradeableRules.length, }, }; @@ -71,13 +60,3 @@ export const getPrebuiltRulesStatusRoute = (router: SecuritySolutionPluginRouter } ); }; - -const calculateRuleStats = (buckets: VersionBuckets): PrebuiltRulesStatusStats => { - const { installedVersions, latestVersionsToInstall, installedVersionsToUpgrade } = buckets; - - return { - num_prebuilt_rules_installed: installedVersions.length, - num_prebuilt_rules_to_install: latestVersionsToInstall.length, - num_prebuilt_rules_to_upgrade: installedVersionsToUpgrade.length, - }; -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/install_prebuilt_rules_and_timelines/route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.test.ts similarity index 98% rename from x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/install_prebuilt_rules_and_timelines/route.test.ts rename to x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.test.ts index 38e98266aee7a..5578aeddef88a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/install_prebuilt_rules_and_timelines/route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.test.ts @@ -13,7 +13,10 @@ import { getBasicEmptySearchResponse, } from '../../../routes/__mocks__/request_responses'; import { requestContextMock, serverMock } from '../../../routes/__mocks__'; -import { installPrebuiltRulesAndTimelinesRoute, createPrepackagedRules } from './route'; +import { + installPrebuiltRulesAndTimelinesRoute, + createPrepackagedRules, +} 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'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/install_prebuilt_rules_and_timelines/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.ts similarity index 88% rename from x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/install_prebuilt_rules_and_timelines/route.ts rename to x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.ts index 23209a613f597..8677bcbd63ecf 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/install_prebuilt_rules_and_timelines/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.ts @@ -5,33 +5,30 @@ * 2.0. */ -import moment from 'moment'; -import { transformError } from '@kbn/securitysolution-es-utils'; -import { validate } from '@kbn/securitysolution-io-ts-utils'; import type { RulesClient } from '@kbn/alerting-plugin/server'; import type { ExceptionListClient } from '@kbn/lists-plugin/server'; -import type { - SecuritySolutionApiRequestHandlerContext, - SecuritySolutionPluginRouter, -} from '../../../../../types'; - +import { transformError } from '@kbn/securitysolution-es-utils'; +import { validate } from '@kbn/securitysolution-io-ts-utils'; +import moment from 'moment'; import { - PREBUILT_RULES_URL, InstallPrebuiltRulesAndTimelinesResponse, + PREBUILT_RULES_URL, } from '../../../../../../common/detection_engine/prebuilt_rules'; import { importTimelineResultSchema } from '../../../../../../common/types/timeline'; - +import type { + SecuritySolutionApiRequestHandlerContext, + SecuritySolutionPluginRouter, +} from '../../../../../types'; +import { installPrepackagedTimelines } from '../../../../timeline/routes/prepackaged_timelines/install_prepackaged_timelines'; +import { buildSiemResponse } from '../../../routes/utils'; import { getExistingPrepackagedRules } from '../../../rule_management/logic/search/get_existing_prepackaged_rules'; -import { createPrebuiltRules } from '../../logic/rule_objects/create_prebuilt_rules'; -import { updatePrebuiltRules } from '../../logic/rule_objects/update_prebuilt_rules'; import { getRulesToInstall } from '../../logic/get_rules_to_install'; import { getRulesToUpdate } from '../../logic/get_rules_to_update'; 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'; - -import { installPrepackagedTimelines } from '../../../../timeline/routes/prepackaged_timelines/install_prepackaged_timelines'; -import { buildSiemResponse } from '../../../routes/utils'; -import { installPrebuiltRulesPackage } from './install_prebuilt_rules_package'; +import { ensureLatestRulesPackageInstalled } from '../../logic/ensure_latest_rules_package_installed'; export const installPrebuiltRulesAndTimelinesRoute = (router: SecuritySolutionPluginRouter) => { router.put( @@ -103,20 +100,20 @@ export const createPrepackagedRules = async ( await exceptionsListClient.createEndpointList(); } - let latestPrebuiltRules = await ruleAssetsClient.fetchLatestAssets(); - if (latestPrebuiltRules.length === 0) { - // Seems no packages with prepackaged rules were installed, try to install the default rules package - await installPrebuiltRulesPackage(config, context); - - // Try to get the prepackaged rules again - latestPrebuiltRules = await ruleAssetsClient.fetchLatestAssets(); - } + const latestPrebuiltRules = await ensureLatestRulesPackageInstalled( + ruleAssetsClient, + config, + context + ); const installedPrebuiltRules = rulesToMap(await getExistingPrepackagedRules({ rulesClient })); const rulesToInstall = getRulesToInstall(latestPrebuiltRules, installedPrebuiltRules); const rulesToUpdate = getRulesToUpdate(latestPrebuiltRules, installedPrebuiltRules); - await createPrebuiltRules(rulesClient, rulesToInstall); + const result = await createPrebuiltRules(rulesClient, rulesToInstall); + if (result.errors.length > 0) { + throw new AggregateError(result.errors, 'Error installing new prebuilt rules'); + } const timeline = await installPrepackagedTimelines( maxTimelineImportExportSize, @@ -128,7 +125,7 @@ export const createPrepackagedRules = async ( importTimelineResultSchema ); - await updatePrebuiltRules(rulesClient, savedObjectsClient, rulesToUpdate); + await upgradePrebuiltRules(rulesClient, rulesToUpdate); const prebuiltRulesOutput: InstallPrebuiltRulesAndTimelinesResponse = { rules_installed: rulesToInstall.length, 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 new file mode 100644 index 0000000000000..ba25f63234a9c --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_route.ts @@ -0,0 +1,127 @@ +/* + * 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 { transformError } from '@kbn/securitysolution-es-utils'; +import { PERFORM_RULE_INSTALLATION_URL } from '../../../../../../common/detection_engine/prebuilt_rules'; +import { PerformRuleInstallationRequestBody } from '../../../../../../common/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_request_schema'; +import type { + PerformRuleInstallationResponseBody, + SkippedRuleInstall, +} from '../../../../../../common/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_response_schema'; +import { SkipRuleInstallReason } from '../../../../../../common/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_response_schema'; +import type { SecuritySolutionPluginRouter } from '../../../../../types'; +import { buildRouteValidation } from '../../../../../utils/build_validation/route_validation'; +import type { PromisePoolError } from '../../../../../utils/promise_pool'; +import { buildSiemResponse } from '../../../routes/utils'; +import { internalRuleToAPIResponse } from '../../../rule_management/normalization/rule_converters'; +import { aggregatePrebuiltRuleErrors } from '../../logic/aggregate_prebuilt_rule_errors'; +import { ensureLatestRulesPackageInstalled } from '../../logic/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'; +import { fetchRuleVersionsTriad } from '../../logic/rule_versions/fetch_rule_versions_triad'; +import { getVersionBuckets } from '../../model/rule_versions/get_version_buckets'; + +export const performRuleInstallationRoute = (router: SecuritySolutionPluginRouter) => { + router.post( + { + path: PERFORM_RULE_INSTALLATION_URL, + validate: { + body: buildRouteValidation(PerformRuleInstallationRequestBody), + }, + options: { + tags: ['access:securitySolution'], + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + + try { + const ctx = await context.resolve(['core', 'alerting', 'securitySolution']); + const config = ctx.securitySolution.getConfig(); + const soClient = ctx.core.savedObjects.client; + const rulesClient = ctx.alerting.getRulesClient(); + const ruleAssetsClient = createPrebuiltRuleAssetsClient(soClient); + const ruleObjectsClient = createPrebuiltRuleObjectsClient(rulesClient); + const exceptionsListClient = ctx.securitySolution.getExceptionListClient(); + + const { mode } = request.body; + + // This will create the endpoint list if it does not exist yet + await exceptionsListClient?.createEndpointList(); + + // If this API is used directly without hitting any detection engine + // pages first, the rules package might be missing. + await ensureLatestRulesPackageInstalled(ruleAssetsClient, config, ctx.securitySolution); + + const fetchErrors: Array> = []; + const skippedRules: SkippedRuleInstall[] = []; + + const ruleVersionsMap = await fetchRuleVersionsTriad({ + ruleAssetsClient, + ruleObjectsClient, + versionSpecifiers: mode === 'ALL_RULES' ? undefined : request.body.rules, + }); + const { currentRules, installableRules } = getVersionBuckets(ruleVersionsMap); + + // Perform all the checks we can before we start the upgrade process + if (mode === 'SPECIFIC_RULES') { + const currentRuleIds = new Set(currentRules.map((rule) => rule.rule_id)); + const installableRuleIds = new Set(installableRules.map((rule) => rule.rule_id)); + request.body.rules.forEach((rule) => { + // Check that the requested rule is not installed yet + if (currentRuleIds.has(rule.rule_id)) { + skippedRules.push({ + rule_id: rule.rule_id, + reason: SkipRuleInstallReason.ALREADY_INSTALLED, + }); + return; + } + + // Check that the requested rule is installable + if (!installableRuleIds.has(rule.rule_id)) { + fetchErrors.push({ + error: new Error( + `Rule with ID "${rule.rule_id}" and version "${rule.version}" not found` + ), + item: rule, + }); + } + }); + } + + const { results: installedRules, errors: installationErrors } = await createPrebuiltRules( + rulesClient, + installableRules + ); + const combinedErrors = [...fetchErrors, ...installationErrors]; + + const body: PerformRuleInstallationResponseBody = { + summary: { + total: installedRules.length + skippedRules.length + combinedErrors.length, + succeeded: installedRules.length, + skipped: skippedRules.length, + failed: combinedErrors.length, + }, + results: { + created: installedRules.map(({ result }) => internalRuleToAPIResponse(result)), + skipped: skippedRules, + }, + errors: aggregatePrebuiltRuleErrors(combinedErrors), + }; + + return response.ok({ body }); + } 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_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 new file mode 100644 index 0000000000000..4efa95eb733aa --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_route.ts @@ -0,0 +1,176 @@ +/* + * 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 { transformError } from '@kbn/securitysolution-es-utils'; +import { PERFORM_RULE_UPGRADE_URL } from '../../../../../../common/detection_engine/prebuilt_rules'; +import { + PerformRuleUpgradeRequestBody, + PickVersionValues, +} from '../../../../../../common/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_request_schema'; +import type { + PerformRuleUpgradeResponseBody, + SkippedRuleUpgrade, +} from '../../../../../../common/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_response_schema'; +import { SkipRuleUpgradeReason } from '../../../../../../common/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_response_schema'; +import { assertUnreachable } from '../../../../../../common/utility_types'; +import type { SecuritySolutionPluginRouter } from '../../../../../types'; +import { buildRouteValidation } from '../../../../../utils/build_validation/route_validation'; +import type { PromisePoolError } from '../../../../../utils/promise_pool'; +import { buildSiemResponse } from '../../../routes/utils'; +import { internalRuleToAPIResponse } from '../../../rule_management/normalization/rule_converters'; +import { aggregatePrebuiltRuleErrors } from '../../logic/aggregate_prebuilt_rule_errors'; +import { createPrebuiltRuleAssetsClient } from '../../logic/rule_assets/prebuilt_rule_assets_client'; +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 type { PrebuiltRuleAsset } from '../../model/rule_assets/prebuilt_rule_asset'; +import { getVersionBuckets } from '../../model/rule_versions/get_version_buckets'; + +export const performRuleUpgradeRoute = (router: SecuritySolutionPluginRouter) => { + router.post( + { + path: PERFORM_RULE_UPGRADE_URL, + validate: { + body: buildRouteValidation(PerformRuleUpgradeRequestBody), + }, + options: { + tags: ['access:securitySolution'], + }, + }, + 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 { mode, pick_version: globalPickVersion = PickVersionValues.TARGET } = request.body; + + const fetchErrors: Array> = []; + const targetRules: PrebuiltRuleAsset[] = []; + const skippedRules: SkippedRuleUpgrade[] = []; + + const versionSpecifiers = mode === 'ALL_RULES' ? undefined : request.body.rules; + const versionSpecifiersMap = new Map( + versionSpecifiers?.map((rule) => [rule.rule_id, rule]) + ); + const ruleVersionsMap = await fetchRuleVersionsTriad({ + ruleAssetsClient, + ruleObjectsClient, + versionSpecifiers, + }); + const versionBuckets = getVersionBuckets(ruleVersionsMap); + const { currentRules } = versionBuckets; + // The upgradeable rules list is mutable; we can remove rules from it because of version mismatch + let upgradeableRules = versionBuckets.upgradeableRules; + + // Perform all the checks we can before we start the upgrade process + if (mode === 'SPECIFIC_RULES') { + const installedRuleIds = new Set(currentRules.map((rule) => rule.rule_id)); + const upgradeableRuleIds = new Set( + upgradeableRules.map(({ current }) => current.rule_id) + ); + request.body.rules.forEach((rule) => { + // Check that the requested rule was found + if (!installedRuleIds.has(rule.rule_id)) { + fetchErrors.push({ + error: new Error( + `Rule with ID "${rule.rule_id}" and version "${rule.version}" not found` + ), + item: rule, + }); + return; + } + + // Check that the requested rule is upgradeable + if (!upgradeableRuleIds.has(rule.rule_id)) { + skippedRules.push({ + rule_id: rule.rule_id, + reason: SkipRuleUpgradeReason.RULE_UP_TO_DATE, + }); + return; + } + + // Check that rule revisions match (no update slipped in since the user reviewed the list) + const currentRevision = ruleVersionsMap.get(rule.rule_id)?.current?.revision; + if (rule.revision !== currentRevision) { + fetchErrors.push({ + error: new Error( + `Revision mismatch for rule ID ${rule.rule_id}: expected ${rule.revision}, got ${currentRevision}` + ), + item: rule, + }); + // Remove the rule from the list of upgradeable rules + upgradeableRules = upgradeableRules.filter( + ({ current }) => current.rule_id !== rule.rule_id + ); + } + }); + } + + // Construct the list of target rule versions + upgradeableRules.forEach(({ current, target }) => { + const rulePickVersion = + versionSpecifiersMap?.get(current.rule_id)?.pick_version ?? globalPickVersion; + switch (rulePickVersion) { + case PickVersionValues.BASE: + const baseVersion = ruleVersionsMap.get(current.rule_id)?.base; + if (baseVersion) { + targetRules.push({ ...baseVersion, version: target.version }); + } else { + fetchErrors.push({ + error: new Error(`Could not find base version for rule ${current.rule_id}`), + item: current, + }); + } + break; + case PickVersionValues.CURRENT: + targetRules.push({ ...current, version: target.version }); + break; + case PickVersionValues.TARGET: + targetRules.push(target); + break; + default: + assertUnreachable(rulePickVersion); + } + }); + + // Perform the upgrade + const { results: updatedRules, errors: installationErrors } = await upgradePrebuiltRules( + rulesClient, + targetRules + ); + const combinedErrors = [...fetchErrors, ...installationErrors]; + + const body: PerformRuleUpgradeResponseBody = { + summary: { + total: updatedRules.length + skippedRules.length + combinedErrors.length, + skipped: skippedRules.length, + succeeded: updatedRules.length, + failed: combinedErrors.length, + }, + results: { + updated: updatedRules.map(({ result }) => internalRuleToAPIResponse(result)), + skipped: skippedRules, + }, + errors: aggregatePrebuiltRuleErrors(combinedErrors), + }; + + return response.ok({ body }); + } 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/register_routes.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/register_routes.ts index da4bd66748a6b..35fd65338a1e4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/register_routes.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/register_routes.ts @@ -9,12 +9,14 @@ import type { ConfigType } from '../../../../config'; import type { SetupPlugins } from '../../../../plugin_contract'; import type { SecuritySolutionPluginRouter } from '../../../../types'; -import { getPrebuiltRulesAndTimelinesStatusRoute } from './get_prebuilt_rules_and_timelines_status/route'; -import { getPrebuiltRulesStatusRoute } from './get_prebuilt_rules_status/route'; -import { installPrebuiltRulesAndTimelinesRoute } from './install_prebuilt_rules_and_timelines/route'; -import { generateAssetsRoute } from './generate_assets/route'; -import { reviewRuleInstallationRoute } from './review_rule_installation/route'; -import { reviewRuleUpgradeRoute } from './review_rule_upgrade/route'; +import { getPrebuiltRulesAndTimelinesStatusRoute } from './get_prebuilt_rules_and_timelines_status/get_prebuilt_rules_and_timelines_status_route'; +import { getPrebuiltRulesStatusRoute } from './get_prebuilt_rules_status/get_prebuilt_rules_status_route'; +import { installPrebuiltRulesAndTimelinesRoute } from './install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route'; +import { generateAssetsRoute } from './generate_assets/generate_assets_route'; +import { reviewRuleInstallationRoute } from './review_rule_installation/review_rule_installation_route'; +import { reviewRuleUpgradeRoute } from './review_rule_upgrade/review_rule_upgrade_route'; +import { performRuleInstallationRoute } from './perform_rule_installation/perform_rule_installation_route'; +import { performRuleUpgradeRoute } from './perform_rule_upgrade/perform_rule_upgrade_route'; export const registerPrebuiltRulesRoutes = ( router: SecuritySolutionPluginRouter, @@ -30,6 +32,8 @@ export const registerPrebuiltRulesRoutes = ( if (prebuiltRulesNewUpgradeAndInstallationWorkflowsEnabled) { // New endpoints for the rule upgrade and installation workflows getPrebuiltRulesStatusRoute(router); + performRuleInstallationRoute(router); + performRuleUpgradeRoute(router); reviewRuleInstallationRoute(router); reviewRuleUpgradeRoute(router); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/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 similarity index 82% rename from x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_installation/route.ts rename to x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_installation/review_rule_installation_route.ts index 8eb8b91af8fbb..b76f5c6f00391 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/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 @@ -6,21 +6,19 @@ */ import { transformError } from '@kbn/securitysolution-es-utils'; - -import type { PrebuiltRuleAsset } from '../../model/rule_assets/prebuilt_rule_asset'; import { REVIEW_RULE_INSTALLATION_URL } from '../../../../../../common/detection_engine/prebuilt_rules'; import type { ReviewRuleInstallationResponseBody, RuleInstallationInfoForReview, RuleInstallationStatsForReview, } from '../../../../../../common/detection_engine/prebuilt_rules/api/review_rule_installation/response_schema'; - import type { SecuritySolutionPluginRouter } from '../../../../../types'; import { buildSiemResponse } from '../../../routes/utils'; - import { convertRuleToDiffable } from '../../logic/diff/normalization/convert_rule_to_diffable'; 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 { getVersionBuckets } from '../../model/rule_versions/get_version_buckets'; export const reviewRuleInstallationRoute = (router: SecuritySolutionPluginRouter) => { @@ -42,27 +40,15 @@ export const reviewRuleInstallationRoute = (router: SecuritySolutionPluginRouter const ruleAssetsClient = createPrebuiltRuleAssetsClient(soClient); const ruleObjectsClient = createPrebuiltRuleObjectsClient(rulesClient); - const [latestVersions, { installedVersions }] = await Promise.all([ - ruleAssetsClient.fetchLatestVersions(), - ruleObjectsClient.fetchInstalledRules(), - ]); - - const versionBuckets = getVersionBuckets({ - latestVersions, - installedVersions, + const ruleVersionsMap = await fetchRuleVersionsTriad({ + ruleAssetsClient, + ruleObjectsClient, }); - - const rulesToInstall = await ruleAssetsClient.fetchAssetsByVersionInfo( - versionBuckets.latestVersionsToInstall - ); + const { installableRules } = getVersionBuckets(ruleVersionsMap); const body: ReviewRuleInstallationResponseBody = { - status_code: 200, - message: 'OK', - attributes: { - stats: calculateRuleStats(rulesToInstall), - rules: calculateRuleInfos(rulesToInstall), - }, + stats: calculateRuleStats(installableRules), + rules: calculateRuleInfos(installableRules), }; return response.ok({ body }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/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 similarity index 55% rename from x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/route.ts rename to x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_route.ts index 626ee8f0bbf7a..844e0a4c188c8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/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,32 +5,24 @@ * 2.0. */ -import { pickBy } from 'lodash'; import { transformError } from '@kbn/securitysolution-es-utils'; - -import type { PrebuiltRuleAsset } from '../../model/rule_assets/prebuilt_rule_asset'; +import { pickBy } from 'lodash'; import { REVIEW_RULE_UPGRADE_URL } from '../../../../../../common/detection_engine/prebuilt_rules'; import type { ReviewRuleUpgradeResponseBody, RuleUpgradeInfoForReview, RuleUpgradeStatsForReview, } from '../../../../../../common/detection_engine/prebuilt_rules/api/review_rule_upgrade/response_schema'; -import type { PrebuiltRuleVersionInfo } from '../../model/rule_versions/prebuilt_rule_version_info'; -import type { - CalculateRuleDiffArgs, - CalculateRuleDiffResult, -} from '../../logic/diff/calculate_rule_diff'; -import { calculateRuleDiff } from '../../logic/diff/calculate_rule_diff'; import type { ThreeWayDiff } from '../../../../../../common/detection_engine/prebuilt_rules/model/diff/three_way_diff/three_way_diff'; -import type { RuleResponse } from '../../../../../../common/detection_engine/rule_schema'; - +import { invariant } from '../../../../../../common/utils/invariant'; 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 { getVersionBuckets } from '../../model/rule_versions/get_version_buckets'; -import { invariant } from '../../../../../../common/utils/invariant'; export const reviewRuleUpgradeRoute = (router: SecuritySolutionPluginRouter) => { router.post( @@ -51,38 +43,21 @@ export const reviewRuleUpgradeRoute = (router: SecuritySolutionPluginRouter) => const ruleAssetsClient = createPrebuiltRuleAssetsClient(soClient); const ruleObjectsClient = createPrebuiltRuleObjectsClient(rulesClient); - const [latestVersions, { installedVersions, installedRules }] = await Promise.all([ - ruleAssetsClient.fetchLatestVersions(), - ruleObjectsClient.fetchInstalledRules(), - ]); - - const versionBuckets = getVersionBuckets({ - latestVersions, - installedVersions, + const ruleVersionsMap = await fetchRuleVersionsTriad({ + ruleAssetsClient, + ruleObjectsClient, }); + const { upgradeableRules } = getVersionBuckets(ruleVersionsMap); - const [baseRules, latestRules] = await Promise.all([ - ruleAssetsClient.fetchAssetsByVersionInfo(versionBuckets.installedVersionsToUpgrade), - ruleAssetsClient.fetchAssetsByVersionInfo(versionBuckets.latestVersionsToUpgrade), - ]); - - const ruleDiffCalculationArgs = getRuleDiffCalculationArgs( - versionBuckets.installedVersionsToUpgrade, - installedRules, - baseRules, - latestRules - ); - const ruleDiffCalculationResults = ruleDiffCalculationArgs.map((args) => { - return calculateRuleDiff(args); + const ruleDiffCalculationResults = upgradeableRules.map(({ current }) => { + const ruleVersions = ruleVersionsMap.get(current.rule_id); + invariant(ruleVersions != null, 'ruleVersions not found'); + return calculateRuleDiff(ruleVersions); }); const body: ReviewRuleUpgradeResponseBody = { - status_code: 200, - message: 'OK', - attributes: { - stats: calculateRuleStats(ruleDiffCalculationResults), - rules: calculateRuleInfos(ruleDiffCalculationResults), - }, + stats: calculateRuleStats(ruleDiffCalculationResults), + rules: calculateRuleInfos(ruleDiffCalculationResults), }; return response.ok({ body }); @@ -97,41 +72,9 @@ export const reviewRuleUpgradeRoute = (router: SecuritySolutionPluginRouter) => ); }; -const getRuleDiffCalculationArgs = ( - installedVersionsToUpgrade: PrebuiltRuleVersionInfo[], - installedRules: RuleResponse[], - baseRules: PrebuiltRuleAsset[], - latestRules: PrebuiltRuleAsset[] -): CalculateRuleDiffArgs[] => { - const installedRulesMap = new Map(installedRules.map((r) => [r.rule_id, r])); - const baseRulesMap = new Map(baseRules.map((r) => [r.rule_id, r])); - const latestRulesMap = new Map(latestRules.map((r) => [r.rule_id, r])); - - const result: CalculateRuleDiffArgs[] = []; - - installedVersionsToUpgrade.forEach((versionToUpgrade) => { - const ruleId = versionToUpgrade.rule_id; - const installedRule = installedRulesMap.get(ruleId); - const baseRule = baseRulesMap.get(ruleId); - const latestRule = latestRulesMap.get(ruleId); - - // baseRule can be undefined if the rule has no historical versions, but other versions should always be present - invariant(installedRule != null, `installedRule is not found for rule_id: ${ruleId}`); - invariant(latestRule != null, `latestRule is not found for rule_id: ${ruleId}`); - - result.push({ - currentVersion: installedRule, - baseVersion: baseRule, - targetVersion: latestRule, - }); - }); - - return result; -}; - const calculateRuleStats = (results: CalculateRuleDiffResult[]): RuleUpgradeStatsForReview => { const allTags = new Set( - results.flatMap((result) => result.ruleVersions.input.current.tags) + results.flatMap((result) => result.ruleVersions.input.current?.tags ?? []) ); return { num_rules_to_upgrade_total: results.length, @@ -144,6 +87,7 @@ const calculateRuleInfos = (results: CalculateRuleDiffResult[]): RuleUpgradeInfo const { ruleDiff, ruleVersions } = result; const installedCurrentVersion = ruleVersions.input.current; const diffableCurrentVersion = ruleVersions.output.current; + invariant(installedCurrentVersion != null, 'installedCurrentVersion not found'); return { id: installedCurrentVersion.id, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/index.ts index 1181e5b174480..56a0ebd88ff86 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -export { createPrepackagedRules } from './api/install_prebuilt_rules_and_timelines/route'; +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/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/aggregate_prebuilt_rule_errors.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/aggregate_prebuilt_rule_errors.ts new file mode 100644 index 0000000000000..802b0c302141d --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/aggregate_prebuilt_rule_errors.ts @@ -0,0 +1,37 @@ +/* + * 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 { AggregatedPrebuiltRuleError } from '../../../../../common/detection_engine/prebuilt_rules/model/prebuilt_rules/aggregated_prebuilt_rules_error'; +import { getErrorMessage, getErrorStatusCode } from '../../../../utils/error_helpers'; +import type { PromisePoolError } from '../../../../utils/promise_pool'; + +export function aggregatePrebuiltRuleErrors( + errors: Array> +) { + const errorsByMessage: Record = {}; + + errors.forEach(({ error, item }) => { + const message = getErrorMessage(error); + const statusCode = getErrorStatusCode(error); + const failedRule = { + rule_id: item.rule_id, + name: item.name, + }; + + if (errorsByMessage[message]) { + errorsByMessage[message].rules.push(failedRule); + } else { + errorsByMessage[message] = { + message, + status_code: statusCode, + rules: [failedRule], + }; + } + }); + + return Object.values(errorsByMessage); +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculate_rule_diff.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculate_rule_diff.ts index 061c1d1082bdd..fb0551c280aff 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculate_rule_diff.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculate_rule_diff.ts @@ -10,25 +10,22 @@ import type { FullRuleDiff } from '../../../../../../common/detection_engine/pre import type { ThreeWayDiff } from '../../../../../../common/detection_engine/prebuilt_rules/model/diff/three_way_diff/three_way_diff'; import { MissingVersion } from '../../../../../../common/detection_engine/prebuilt_rules/model/diff/three_way_diff/three_way_diff'; import type { RuleResponse } from '../../../../../../common/detection_engine/rule_schema'; +import { invariant } from '../../../../../../common/utils/invariant'; import type { PrebuiltRuleAsset } from '../../model/rule_assets/prebuilt_rule_asset'; import { calculateRuleFieldsDiff } from './calculation/calculate_rule_fields_diff'; import { convertRuleToDiffable } from './normalization/convert_rule_to_diffable'; -export interface CalculateRuleDiffArgs { - currentVersion: RuleResponse; - baseVersion?: PrebuiltRuleAsset; - targetVersion: PrebuiltRuleAsset; +export interface RuleVersions { + current?: RuleResponse; + base?: PrebuiltRuleAsset; + target?: PrebuiltRuleAsset; } export interface CalculateRuleDiffResult { ruleDiff: FullRuleDiff; ruleVersions: { - input: { - current: RuleResponse; - base?: PrebuiltRuleAsset; - target: PrebuiltRuleAsset; - }; + input: RuleVersions; output: { current: DiffableRule; base?: DiffableRule; @@ -39,11 +36,11 @@ export interface CalculateRuleDiffResult { /** * Calculates a rule diff for a given set of 3 versions of the rule: - * - currenly installed version + * - currently installed version * - base version that is the corresponding stock rule content * - target version which is the stock rule content the user wants to update the rule to */ -export const calculateRuleDiff = (args: CalculateRuleDiffArgs): CalculateRuleDiffResult => { +export const calculateRuleDiff = (args: RuleVersions): CalculateRuleDiffResult => { /* 1. Convert current, base and target versions to `DiffableRule`. 2. Calculate a `RuleFieldsDiff`. For every top-level field of `DiffableRule`: @@ -59,11 +56,16 @@ export const calculateRuleDiff = (args: CalculateRuleDiffArgs): CalculateRuleDif 3. Create and return a result based on the `RuleFieldsDiff`. */ - const { baseVersion, currentVersion, targetVersion } = args; + const { base, current, target } = args; + + invariant(current != null, 'current version is required'); + const diffableCurrentVersion = convertRuleToDiffable(current); + + invariant(target != null, 'target version is required'); + const diffableTargetVersion = convertRuleToDiffable(target); - const diffableBaseVersion = baseVersion ? convertRuleToDiffable(baseVersion) : undefined; - const diffableCurrentVersion = convertRuleToDiffable(currentVersion); - const diffableTargetVersion = convertRuleToDiffable(targetVersion); + // Base version is optional + const diffableBaseVersion = base ? convertRuleToDiffable(base) : undefined; const fieldsDiff = calculateRuleFieldsDiff({ base_version: diffableBaseVersion || MissingVersion, @@ -82,9 +84,9 @@ export const calculateRuleDiff = (args: CalculateRuleDiffArgs): CalculateRuleDif }, ruleVersions: { input: { - current: currentVersion, - base: baseVersion, - target: targetVersion, + current, + base, + target, }, output: { current: diffableCurrentVersion, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/ensure_latest_rules_package_installed.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/ensure_latest_rules_package_installed.ts new file mode 100644 index 0000000000000..6ab16635374ee --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/ensure_latest_rules_package_installed.ts @@ -0,0 +1,27 @@ +/* + * 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 { 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'; + +export async function ensureLatestRulesPackageInstalled( + ruleAssetsClient: IPrebuiltRuleAssetsClient, + config: ConfigType, + securityContext: SecuritySolutionApiRequestHandlerContext +) { + let latestPrebuiltRules = await ruleAssetsClient.fetchLatestAssets(); + if (latestPrebuiltRules.length === 0) { + // Seems no packages with prepackaged rules were installed, try to install the default rules package + await installPrebuiltRulesPackage(config, securityContext); + + // Try to get the prepackaged rules again + latestPrebuiltRules = await ruleAssetsClient.fetchLatestAssets(); + } + return latestPrebuiltRules; +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client.ts index 96cb684a152d9..7c4b2d2616ca7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client.ts @@ -14,7 +14,7 @@ import { withSecuritySpan } from '../../../../../utils/with_security_span'; import type { PrebuiltRuleAsset } from '../../model/rule_assets/prebuilt_rule_asset'; import { validatePrebuiltRuleAssets } from './prebuilt_rule_assets_validation'; import { PREBUILT_RULE_ASSETS_SO_TYPE } from './prebuilt_rule_assets_type'; -import type { PrebuiltRuleVersionInfo } from '../../model/rule_versions/prebuilt_rule_version_info'; +import type { RuleVersionSpecifier } from '../../model/rule_versions/rule_version_specifier'; const MAX_PREBUILT_RULES_COUNT = 10_000; const MAX_ASSETS_PER_BULK_CREATE_REQUEST = 500; @@ -22,9 +22,9 @@ const MAX_ASSETS_PER_BULK_CREATE_REQUEST = 500; export interface IPrebuiltRuleAssetsClient { fetchLatestAssets: () => Promise; - fetchLatestVersions(): Promise; + fetchLatestVersions(): Promise; - fetchAssetsByVersionInfo(versions: PrebuiltRuleVersionInfo[]): Promise; + fetchAssetsByVersion(versions: RuleVersionSpecifier[]): Promise; bulkCreateAssets(assets: PrebuiltRuleAsset[]): Promise; } @@ -76,7 +76,7 @@ export const createPrebuiltRuleAssetsClient = ( }); }, - fetchLatestVersions: (): Promise => { + fetchLatestVersions: (): Promise => { return withSecuritySpan('IPrebuiltRuleAssetsClient.fetchLatestVersions', async () => { const findResult = await savedObjectsClient.find< PrebuiltRuleAsset, @@ -119,7 +119,7 @@ export const createPrebuiltRuleAssetsClient = ( return buckets.map((bucket) => { const hit = bucket.latest_version.hits.hits[0]; const soAttributes = hit._source[PREBUILT_RULE_ASSETS_SO_TYPE]; - const versionInfo: PrebuiltRuleVersionInfo = { + const versionInfo: RuleVersionSpecifier = { rule_id: soAttributes.rule_id, version: soAttributes.version, }; @@ -128,10 +128,8 @@ export const createPrebuiltRuleAssetsClient = ( }); }, - fetchAssetsByVersionInfo: ( - versions: PrebuiltRuleVersionInfo[] - ): Promise => { - return withSecuritySpan('IPrebuiltRuleAssetsClient.fetchAssetsByVersionInfo', async () => { + fetchAssetsByVersion: (versions: RuleVersionSpecifier[]): Promise => { + return withSecuritySpan('IPrebuiltRuleAssetsClient.fetchAssetsByVersion', async () => { if (versions.length === 0) { // NOTE: without early return it would build incorrect filter and fetch all existing saved objects return []; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/create_prebuilt_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/create_prebuilt_rules.ts index 2fd8ef266e6b6..54b4361bf868b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/create_prebuilt_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/create_prebuilt_rules.ts @@ -27,7 +27,5 @@ export const createPrebuiltRules = (rulesClient: RulesClient, rules: PrebuiltRul }, }); - if (result.errors.length > 0) { - throw new AggregateError(result.errors, 'Error installing new prebuilt rules'); - } + return result; }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client.ts index f0d978376d330..c22995f2d12f1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client.ts @@ -6,43 +6,46 @@ */ import type { RulesClient } from '@kbn/alerting-plugin/server'; -import type { RuleResponse } from '../../../../../../common/detection_engine/rule_schema'; +import type { + RuleResponse, + RuleSignatureId, +} from '../../../../../../common/detection_engine/rule_schema'; import { withSecuritySpan } from '../../../../../utils/with_security_span'; +import { findRules } from '../../../rule_management/logic/search/find_rules'; import { getExistingPrepackagedRules } from '../../../rule_management/logic/search/get_existing_prepackaged_rules'; import { internalRuleToAPIResponse } from '../../../rule_management/normalization/rule_converters'; -import type { PrebuiltRuleVersionInfo } from '../../model/rule_versions/prebuilt_rule_version_info'; export interface IPrebuiltRuleObjectsClient { - fetchInstalledRules(): Promise; -} - -export interface FetchInstalledRulesResult { - installedRules: RuleResponse[]; - installedVersions: PrebuiltRuleVersionInfo[]; + fetchAllInstalledRules(): Promise; + fetchInstalledRulesByIds(ruleIds: string[]): Promise; } export const createPrebuiltRuleObjectsClient = ( rulesClient: RulesClient ): IPrebuiltRuleObjectsClient => { return { - fetchInstalledRules: (): Promise => { + fetchAllInstalledRules: (): Promise => { return withSecuritySpan('IPrebuiltRuleObjectsClient.fetchInstalledRules', async () => { const rulesData = await getExistingPrepackagedRules({ rulesClient }); const rules = rulesData.map((rule) => internalRuleToAPIResponse(rule)); - const versions = rules.map((rule) => convertRuleToVersionInfo(rule)); - return { - installedRules: rules, - installedVersions: versions, - }; + return rules; }); }, - }; -}; + fetchInstalledRulesByIds: (ruleIds: RuleSignatureId[]): Promise => { + return withSecuritySpan('IPrebuiltRuleObjectsClient.fetchInstalledRulesByIds', async () => { + const { data } = await findRules({ + rulesClient, + perPage: ruleIds.length, + page: 1, + sortField: 'createdAt', + sortOrder: 'desc', + fields: undefined, + filter: `alert.attributes.params.ruleId:(${ruleIds.join(' or ')})`, + }); -const convertRuleToVersionInfo = (rule: RuleResponse): PrebuiltRuleVersionInfo => { - const versionInfo: PrebuiltRuleVersionInfo = { - rule_id: rule.rule_id, - version: rule.version, + const rules = data.map((rule) => internalRuleToAPIResponse(rule)); + return rules; + }); + }, }; - return versionInfo; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/update_prebuilt_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/update_prebuilt_rules.ts deleted file mode 100644 index cbd9f32069a95..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/update_prebuilt_rules.ts +++ /dev/null @@ -1,100 +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. - */ - -import { chunk } from 'lodash/fp'; -import type { SavedObjectsClientContract } from '@kbn/core/server'; -import type { RulesClient, PartialRule } from '@kbn/alerting-plugin/server'; - -import { transformAlertToRuleAction } from '../../../../../../common/detection_engine/transform_actions'; -import { MAX_RULES_TO_UPDATE_IN_PARALLEL } from '../../../../../../common/constants'; - -import { createRules } from '../../../rule_management/logic/crud/create_rules'; -import { readRules } from '../../../rule_management/logic/crud/read_rules'; -import { patchRules } from '../../../rule_management/logic/crud/patch_rules'; -import { deleteRules } from '../../../rule_management/logic/crud/delete_rules'; - -import type { RuleParams } from '../../../rule_schema'; - -import { PrepackagedRulesError } from '../../api/install_prebuilt_rules_and_timelines/route'; -import type { PrebuiltRuleAsset } from '../../model/rule_assets/prebuilt_rule_asset'; - -/** - * Updates existing prebuilt rules given a set of rules and output index. - * This implements a chunked approach to not saturate network connections and - * avoid being a "noisy neighbor". - * @param rulesClient Alerting client - * @param spaceId Current user spaceId - * @param rules The rules to apply the update for - */ -export const updatePrebuiltRules = async ( - rulesClient: RulesClient, - savedObjectsClient: SavedObjectsClientContract, - rules: PrebuiltRuleAsset[] -): Promise => { - const ruleChunks = chunk(MAX_RULES_TO_UPDATE_IN_PARALLEL, rules); - for (const ruleChunk of ruleChunks) { - const rulePromises = createPromises(rulesClient, savedObjectsClient, ruleChunk); - await Promise.all(rulePromises); - } -}; - -/** - * Creates promises of the rules and returns them. - * @param rulesClient Alerting client - * @param spaceId Current user spaceId - * @param rules The rules to apply the update for - * @returns Promise of what was updated. - */ -const createPromises = ( - rulesClient: RulesClient, - savedObjectsClient: SavedObjectsClientContract, - rules: PrebuiltRuleAsset[] -): Array | null>> => { - return rules.map(async (rule) => { - const existingRule = await readRules({ - rulesClient, - ruleId: rule.rule_id, - id: undefined, - }); - - if (!existingRule) { - throw new PrepackagedRulesError(`Failed to find rule ${rule.rule_id}`, 500); - } - - // If we're trying to change the type of a prepackaged rule, we need to delete the old one - // and replace it with the new rule, keeping the enabled setting, actions, throttle, id, - // and exception lists from the old rule - if (rule.type !== existingRule.params.type) { - await deleteRules({ - ruleId: existingRule.id, - rulesClient, - }); - - return createRules({ - rulesClient, - params: { - ...rule, - // Force the prepackaged rule to use the enabled state from the existing rule, - // regardless of what the prepackaged rule says - enabled: existingRule.enabled, - actions: existingRule.actions.map(transformAlertToRuleAction), - }, - }); - } else { - return patchRules({ - rulesClient, - existingRule, - nextParams: { - ...rule, - // Force enabled to use the enabled state from the existing rule by passing in undefined to patchRules - enabled: undefined, - actions: undefined, - }, - }); - } - }); -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/update_prebuilt_rules.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/upgrade_prebuilt_rules.test.ts similarity index 84% rename from x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/update_prebuilt_rules.test.ts rename to x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/upgrade_prebuilt_rules.test.ts index 5afe5a4753c10..2990533dc0f89 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/update_prebuilt_rules.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/upgrade_prebuilt_rules.test.ts @@ -6,12 +6,11 @@ */ import { rulesClientMock } from '@kbn/alerting-plugin/server/mocks'; -import { savedObjectsClientMock } from '@kbn/core/server/mocks'; import { getRuleMock, getFindResultWithSingleHit, } from '../../../routes/__mocks__/request_responses'; -import { updatePrebuiltRules } from './update_prebuilt_rules'; +import { upgradePrebuiltRules } from './upgrade_prebuilt_rules'; import { patchRules } from '../../../rule_management/logic/crud/patch_rules'; import { getPrebuiltRuleMock, getPrebuiltThreatMatchRuleMock } from '../../mocks'; import { getThreatRuleParams } from '../../../rule_schema/mocks'; @@ -20,11 +19,9 @@ jest.mock('../../../rule_management/logic/crud/patch_rules'); describe('updatePrebuiltRules', () => { let rulesClient: ReturnType; - let savedObjectsClient: ReturnType; beforeEach(() => { rulesClient = rulesClientMock.create(); - savedObjectsClient = savedObjectsClientMock.create(); }); it('should omit actions and enabled when calling patchRules', async () => { @@ -39,7 +36,7 @@ describe('updatePrebuiltRules', () => { const prepackagedRule = getPrebuiltRuleMock(); rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); - await updatePrebuiltRules(rulesClient, savedObjectsClient, [{ ...prepackagedRule, actions }]); + await upgradePrebuiltRules(rulesClient, [{ ...prepackagedRule, actions }]); expect(patchRules).toHaveBeenCalledWith( expect.objectContaining({ @@ -70,9 +67,7 @@ describe('updatePrebuiltRules', () => { data: [getRuleMock(getThreatRuleParams())], }); - await updatePrebuiltRules(rulesClient, savedObjectsClient, [ - { ...prepackagedRule, ...updatedThreatParams }, - ]); + await upgradePrebuiltRules(rulesClient, [{ ...prepackagedRule, ...updatedThreatParams }]); expect(patchRules).toHaveBeenCalledWith( expect.objectContaining({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/upgrade_prebuilt_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/upgrade_prebuilt_rules.ts new file mode 100644 index 0000000000000..3b21254b3d0dd --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/upgrade_prebuilt_rules.ts @@ -0,0 +1,106 @@ +/* + * 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 { SanitizedRule } from '@kbn/alerting-plugin/common'; +import type { RulesClient } from '@kbn/alerting-plugin/server'; +import { MAX_RULES_TO_UPDATE_IN_PARALLEL } from '../../../../../../common/constants'; +import { transformAlertToRuleAction } from '../../../../../../common/detection_engine/transform_actions'; +import { initPromisePool } from '../../../../../utils/promise_pool'; +import { withSecuritySpan } from '../../../../../utils/with_security_span'; +import { createRules } from '../../../rule_management/logic/crud/create_rules'; +import { deleteRules } from '../../../rule_management/logic/crud/delete_rules'; +import { patchRules } from '../../../rule_management/logic/crud/patch_rules'; +import { readRules } from '../../../rule_management/logic/crud/read_rules'; +import type { RuleParams } from '../../../rule_schema'; +import { PrepackagedRulesError } from '../../api/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route'; +import type { PrebuiltRuleAsset } from '../../model/rule_assets/prebuilt_rule_asset'; + +/** + * Upgrades existing prebuilt rules given a set of rules and output index. + * This implements a chunked approach to not saturate network connections and + * avoid being a "noisy neighbor". + * @param rulesClient Alerting client + * @param rules The rules to apply the update for + */ +export const upgradePrebuiltRules = async (rulesClient: RulesClient, rules: PrebuiltRuleAsset[]) => + withSecuritySpan('upgradePrebuiltRules', async () => { + const result = await initPromisePool({ + concurrency: MAX_RULES_TO_UPDATE_IN_PARALLEL, + items: rules, + executor: async (rule) => { + return upgradeRule(rulesClient, rule); + }, + }); + + return result; + }); + +/** + * Upgrades a rule + * + * @param rulesClient Alerting client + * @param rule The rule to apply the update for + * @returns Promise of what was updated. + */ +const upgradeRule = async ( + rulesClient: RulesClient, + rule: PrebuiltRuleAsset +): Promise> => { + const existingRule = await readRules({ + rulesClient, + ruleId: rule.rule_id, + id: undefined, + }); + + if (!existingRule) { + throw new PrepackagedRulesError(`Failed to find rule ${rule.rule_id}`, 500); + } + + // If we're trying to change the type of a prepackaged rule, we need to delete the old one + // and replace it with the new rule, keeping the enabled setting, actions, throttle, id, + // and exception lists from the old rule + if (rule.type !== existingRule.params.type) { + await deleteRules({ + ruleId: existingRule.id, + rulesClient, + }); + + return createRules({ + rulesClient, + params: { + ...rule, + // Force the prepackaged rule to use the enabled state from the existing rule, + // regardless of what the prepackaged rule says + enabled: existingRule.enabled, + actions: existingRule.actions.map(transformAlertToRuleAction), + }, + }); + } else { + await patchRules({ + rulesClient, + existingRule, + nextParams: { + ...rule, + // Force enabled to use the enabled state from the existing rule by passing in undefined to patchRules + enabled: undefined, + actions: undefined, + }, + }); + + const updatedRule = await readRules({ + rulesClient, + ruleId: rule.rule_id, + id: undefined, + }); + + if (!updatedRule) { + throw new PrepackagedRulesError(`Rule ${rule.rule_id} not found after upgrade`, 500); + } + + return updatedRule; + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_versions/fetch_rule_versions_triad.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_versions/fetch_rule_versions_triad.ts new file mode 100644 index 0000000000000..ae7bdc6b391b4 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_versions/fetch_rule_versions_triad.ts @@ -0,0 +1,37 @@ +/* + * 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 { RuleVersions } from '../diff/calculate_rule_diff'; +import type { IPrebuiltRuleAssetsClient } from '../rule_assets/prebuilt_rule_assets_client'; +import type { IPrebuiltRuleObjectsClient } from '../rule_objects/prebuilt_rule_objects_client'; +import type { RuleVersionSpecifier } from '../../model/rule_versions/rule_version_specifier'; +import { zipRuleVersions } from './zip_rule_versions'; + +interface GetRuleVersionsMapArgs { + ruleObjectsClient: IPrebuiltRuleObjectsClient; + ruleAssetsClient: IPrebuiltRuleAssetsClient; + versionSpecifiers?: RuleVersionSpecifier[]; +} + +export async function fetchRuleVersionsTriad({ + ruleObjectsClient, + ruleAssetsClient, + versionSpecifiers, +}: GetRuleVersionsMapArgs): Promise> { + const [currentRules, latestRules] = await Promise.all([ + versionSpecifiers + ? ruleObjectsClient.fetchInstalledRulesByIds( + versionSpecifiers.map(({ rule_id: ruleId }) => ruleId) + ) + : ruleObjectsClient.fetchAllInstalledRules(), + versionSpecifiers + ? ruleAssetsClient.fetchAssetsByVersion(versionSpecifiers) + : ruleAssetsClient.fetchLatestAssets(), + ]); + const baseRules = await ruleAssetsClient.fetchAssetsByVersion(currentRules); + return zipRuleVersions(currentRules, baseRules, latestRules); +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_versions/zip_rule_versions.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_versions/zip_rule_versions.ts new file mode 100644 index 0000000000000..dbfe798c2405b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_versions/zip_rule_versions.ts @@ -0,0 +1,43 @@ +/* + * 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 { RuleResponse } from '../../../../../../common/detection_engine/rule_schema'; +import type { PrebuiltRuleAsset } from '../../model/rule_assets/prebuilt_rule_asset'; +import type { RuleVersions } from '../diff/calculate_rule_diff'; + +export const zipRuleVersions = ( + installedRules: RuleResponse[], + baseRules: PrebuiltRuleAsset[], + latestRules: PrebuiltRuleAsset[] +): Map => { + const baseRulesMap = new Map(baseRules.map((r) => [r.rule_id, r])); + const latestRulesMap = new Map(latestRules.map((r) => [r.rule_id, r])); + const currentRulesMap = new Map(installedRules.map((r) => [r.rule_id, r])); + + const uniqueRuleIds = new Set([ + ...Array.from(baseRulesMap.keys()), + ...Array.from(latestRulesMap.keys()), + ...Array.from(currentRulesMap.keys()), + ]); + + return new Map( + [...uniqueRuleIds].map((ruleId) => { + const base = baseRulesMap.get(ruleId); + const target = latestRulesMap.get(ruleId); + const current = currentRulesMap.get(ruleId); + + return [ + ruleId, + { + current, + base, + target, + }, + ]; + }) + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_versions/get_version_buckets.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_versions/get_version_buckets.ts index 9dab6da397b0d..96e1f76248da6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_versions/get_version_buckets.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_versions/get_version_buckets.ts @@ -5,50 +5,62 @@ * 2.0. */ -import type { PrebuiltRuleVersionInfo } from './prebuilt_rule_version_info'; - -export interface GetVersionBucketsArgs { - latestVersions: PrebuiltRuleVersionInfo[]; - installedVersions: PrebuiltRuleVersionInfo[]; -} +import type { RuleResponse } from '../../../../../../common/detection_engine/rule_schema'; +import type { RuleVersions } from '../../logic/diff/calculate_rule_diff'; +import type { PrebuiltRuleAsset } from '../rule_assets/prebuilt_rule_asset'; export interface VersionBuckets { - latestVersions: PrebuiltRuleVersionInfo[]; - installedVersions: PrebuiltRuleVersionInfo[]; - latestVersionsToInstall: PrebuiltRuleVersionInfo[]; - latestVersionsToUpgrade: PrebuiltRuleVersionInfo[]; - installedVersionsToUpgrade: PrebuiltRuleVersionInfo[]; + /** + * Rules that are currently installed in Kibana + */ + currentRules: RuleResponse[]; + /** + * Rules that are ready to be installed + */ + installableRules: PrebuiltRuleAsset[]; + /** + * Rules that are installed but outdated + */ + upgradeableRules: Array<{ + /** + * The currently installed version + */ + current: RuleResponse; + /** + * The latest available version + */ + target: PrebuiltRuleAsset; + }>; } -export const getVersionBuckets = (args: GetVersionBucketsArgs): VersionBuckets => { - const { latestVersions, installedVersions } = args; - - const installedVersionsMap = new Map(installedVersions.map((item) => [item.rule_id, item])); +export const getVersionBuckets = (ruleVersionsMap: Map): VersionBuckets => { + const currentRules: RuleResponse[] = []; + const installableRules: PrebuiltRuleAsset[] = []; + const upgradeableRules: VersionBuckets['upgradeableRules'] = []; - const latestVersionsToInstall: PrebuiltRuleVersionInfo[] = []; - const latestVersionsToUpgrade: PrebuiltRuleVersionInfo[] = []; - const installedVersionsToUpgrade: PrebuiltRuleVersionInfo[] = []; - - latestVersions.forEach((latestVersion) => { - const installedVersion = installedVersionsMap.get(latestVersion.rule_id); + ruleVersionsMap.forEach(({ current, target }) => { + if (current != null) { + // If this rule is installed + currentRules.push(current); + } - if (installedVersion == null) { + if (current == null && target != null) { // If this rule is not installed - latestVersionsToInstall.push(latestVersion); + installableRules.push(target); } - if (installedVersion != null && installedVersion.version < latestVersion.version) { + if (current != null && target != null && current.version < target.version) { // If this rule is installed but outdated - latestVersionsToUpgrade.push(latestVersion); - installedVersionsToUpgrade.push(installedVersion); + upgradeableRules.push({ + current, + target, + }); } }); return { - latestVersions, - installedVersions, - latestVersionsToInstall, - latestVersionsToUpgrade, - installedVersionsToUpgrade, + currentRules, + installableRules, + upgradeableRules, }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_versions/prebuilt_rule_version_info.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_versions/rule_version_specifier.ts similarity index 90% rename from x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_versions/prebuilt_rule_version_info.ts rename to x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_versions/rule_version_specifier.ts index bd130917be980..d54d6239cba03 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_versions/prebuilt_rule_version_info.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_versions/rule_version_specifier.ts @@ -10,7 +10,7 @@ import type { RuleVersion, } from '../../../../../../common/detection_engine/rule_schema'; -export interface PrebuiltRuleVersionInfo { +export interface RuleVersionSpecifier { rule_id: RuleSignatureId; version: RuleVersion; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/search/get_existing_prepackaged_rules.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/search/get_existing_prepackaged_rules.test.ts index 338a0239b6250..d45cfbbc2d96c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/search/get_existing_prepackaged_rules.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/search/get_existing_prepackaged_rules.test.ts @@ -48,16 +48,6 @@ describe('get_existing_prepackaged_rules', () => { result3.params.immutable = true; result3.id = 'f3e1bf0b-b95f-43da-b1de-5d2f0af2287a'; - // first result mock which is for returning the total - rulesClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ - data: [result1], - perPage: 1, - page: 1, - total: 3, - }) - ); - // second mock which will return all the data on a single page rulesClient.find.mockResolvedValueOnce( getFindResultWithMultiHits({ @@ -90,16 +80,6 @@ describe('get_existing_prepackaged_rules', () => { const result2 = getRuleMock(getQueryRuleParams()); result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - // first result mock which is for returning the total - rulesClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ - data: [result1], - perPage: 1, - page: 1, - total: 2, - }) - ); - // second mock which will return all the data on a single page rulesClient.find.mockResolvedValueOnce( getFindResultWithMultiHits({ data: [result1, result2], perPage: 2, page: 1, total: 2 }) @@ -121,16 +101,6 @@ describe('get_existing_prepackaged_rules', () => { const result3 = getRuleMock(getQueryRuleParams()); result3.id = 'f3e1bf0b-b95f-43da-b1de-5d2f0af2287a'; - // first result mock which is for returning the total - rulesClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ - data: [result1], - perPage: 3, - page: 1, - total: 3, - }) - ); - // second mock which will return all the data on a single page rulesClient.find.mockResolvedValueOnce( getFindResultWithMultiHits({ @@ -163,16 +133,6 @@ describe('get_existing_prepackaged_rules', () => { const result2 = getRuleMock(getQueryRuleParams()); result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - // first result mock which is for returning the total - rulesClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ - data: [result1], - perPage: 1, - page: 1, - total: 2, - }) - ); - // second mock which will return all the data on a single page rulesClient.find.mockResolvedValueOnce( getFindResultWithMultiHits({ data: [result1, result2], perPage: 2, page: 1, total: 2 }) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/search/get_existing_prepackaged_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/search/get_existing_prepackaged_rules.ts index 469de8544a13a..6ef0a2e2e9525 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/search/get_existing_prepackaged_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/search/get_existing_prepackaged_rules.ts @@ -10,6 +10,7 @@ import { withSecuritySpan } from '../../../../../utils/with_security_span'; import { findRules } from './find_rules'; import type { RuleAlertType } from '../../../rule_schema'; +export const MAX_PREBUILT_RULES_COUNT = 10_000; export const FILTER_NON_PREPACKED_RULES = 'alert.attributes.params.immutable: false'; export const FILTER_PREPACKED_RULES = 'alert.attributes.params.immutable: true'; @@ -50,11 +51,10 @@ export const getRules = async ({ filter: string; }): Promise => withSecuritySpan('getRules', async () => { - const count = await getRulesCount({ rulesClient, filter }); const rules = await findRules({ rulesClient, filter, - perPage: count, + perPage: MAX_PREBUILT_RULES_COUNT, page: 1, sortField: 'createdAt', sortOrder: 'desc', diff --git a/x-pack/plugins/security_solution/server/utils/error_helpers.ts b/x-pack/plugins/security_solution/server/utils/error_helpers.ts new file mode 100644 index 0000000000000..b07cfd4fb7f4d --- /dev/null +++ b/x-pack/plugins/security_solution/server/utils/error_helpers.ts @@ -0,0 +1,38 @@ +/* + * 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. + */ + +/** + * Extracts error message from an error + * + * @param error Unknown error + * @returns error message + */ +export const getErrorMessage = (error: unknown) => { + if (error instanceof Error) { + return error.message; + } else if (typeof error === 'string') { + return error; + } else { + return 'Unknown error'; + } +}; + +const hasStatusCode = (error: unknown): error is { statusCode: unknown } => + typeof error === 'object' && error !== null && 'statusCode' in error; + +/** + * Extracts status code from an error + * + * @param error Unknown error + * @returns Stats code if it exists + */ +export const getErrorStatusCode = (error: unknown): number | undefined => { + if (hasStatusCode(error)) { + return Number(error.statusCode); + } + return undefined; +};