diff --git a/.buildkite/ftr_security_serverless_configs.yml b/.buildkite/ftr_security_serverless_configs.yml index 329ffa3ff0e1d..d7f36403547e5 100644 --- a/.buildkite/ftr_security_serverless_configs.yml +++ b/.buildkite/ftr_security_serverless_configs.yml @@ -70,6 +70,7 @@ disabled: - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/configs/serverless.config.ts - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/upgrade_prebuilt_rules/diffable_rule_fields/common_fields/configs/serverless.config.ts - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/upgrade_prebuilt_rules/diffable_rule_fields/type_specific_fields/configs/serverless.config.ts + - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/configs/serverless_essentials_tier.config.ts - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/configs/serverless.config.ts - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_delete/trial_license_complete_tier/configs/serverless.config.ts - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_delete/basic_license_essentials_tier/configs/serverless.config.ts diff --git a/.buildkite/ftr_security_stateful_configs.yml b/.buildkite/ftr_security_stateful_configs.yml index af92456c009ee..75c81c07d0531 100644 --- a/.buildkite/ftr_security_stateful_configs.yml +++ b/.buildkite/ftr_security_stateful_configs.yml @@ -61,6 +61,7 @@ enabled: - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/upgrade_prebuilt_rules/diffable_rule_fields/type_specific_fields/configs/ess.config.ts - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/prebuilt_rules_package/air_gapped/configs/ess_air_gapped_large_package.config.ts - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/customization_enabled/prebuilt_rules_package/air_gapped/configs/ess_air_gapped.config.ts + - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/configs/ess_basic_license.config.ts - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/configs/ess.config.ts - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_delete/trial_license_complete_tier/configs/ess.config.ts - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_delete/basic_license_essentials_tier/configs/ess.config.ts @@ -104,4 +105,4 @@ enabled: - x-pack/test/cloud_security_posture_functional/config.agentless.ts - x-pack/test/cloud_security_posture_functional/data_views/config.ts - x-pack/test/automatic_import_api_integration/apis/config_basic.ts - - x-pack/test/automatic_import_api_integration/apis/config_graphs.ts \ No newline at end of file + - x-pack/test/automatic_import_api_integration/apis/config_graphs.ts diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/perform_rule_upgrade/perform_rule_upgrade_route.test.ts b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/perform_rule_upgrade/perform_rule_upgrade_route.test.ts index 7f7a64006adbf..0ab96975ffe47 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/perform_rule_upgrade/perform_rule_upgrade_route.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/perform_rule_upgrade/perform_rule_upgrade_route.test.ts @@ -261,7 +261,7 @@ describe('Perform Rule Upgrade Route Schemas', () => { ); }); - test('rejects paylaod with missing rules array', () => { + test('rejects payload with missing rules array', () => { const invalid = { ...validRequest, rules: undefined }; const result = UpgradeSpecificRulesRequest.safeParse(invalid); expectParseError(result); diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/utils.ts b/x-pack/solutions/security/plugins/security_solution/public/common/components/utils.ts index abe74b8830bd3..4c1c7afb10fa4 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/utils.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/utils.ts @@ -10,6 +10,7 @@ import { useMemo, useState } from 'react'; import useResizeObserver from 'use-resize-observer/polyfilled'; import { niceTimeFormatByDay, timeFormatter } from '@elastic/charts'; import moment from 'moment-timezone'; +import type { IToasts } from '@kbn/core/public'; export const getDaysDiff = (minDate: moment.Moment, maxDate: moment.Moment) => { const diff = maxDate.diff(minDate, 'days'); @@ -35,3 +36,32 @@ export const useThrottledResizeObserver = (wait = 100) => { return { ref, ...size }; }; + +/** + * Displays an error toast with a specified title and a short message. + * Also allows to set a detailed message that will appear in the modal when user clicks the "See full error" button. + * + * @param title The title of the toast notification. Appears in both the toast header and the modal header. + * @param shortMessage An optional short message. Appears under toast header. + * @param fullMessage The full error message. Appears in the modal when user clicks the "See full error" button. + * @param toasts The toasts service instance. + */ +export function showErrorToast({ + title, + shortMessage, + fullMessage, + toasts, +}: { + title: string; + shortMessage?: string; + fullMessage: string; + toasts: IToasts; +}) { + const error = new Error('Error details'); + error.stack = fullMessage; + toasts.addError(error, { + title, + // Fall back to a space to ensure that the toast component does not render its default message + toastMessage: shortMessage ?? ' ', + }); +} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/translations.ts index 87580af582a16..6d061696515de 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/translations.ts @@ -16,7 +16,7 @@ export const RULE_INSTALLATION_FAILED = i18n.translate( export const INSTALL_RULE_SUCCESS = (succeeded: number) => i18n.translate('xpack.securitySolution.detectionEngine.prebuiltRules.toast.installRuleSuccess', { - defaultMessage: '{succeeded, plural, one {# rule} other {# rules}} installed successfully.', + defaultMessage: '{succeeded, plural, one {# rule} other {# rules}} installed successfully', values: { succeeded }, }); @@ -29,7 +29,7 @@ export const INSTALL_RULE_SKIPPED = (skipped: number) => export const INSTALL_RULE_FAILED = (failed: number) => i18n.translate('xpack.securitySolution.detectionEngine.prebuiltRules.toast.installRuleFailed', { - defaultMessage: '{failed, plural, one {# rule} other {# rules}} failed to install.', + defaultMessage: '{failed, plural, one {# rule} other {# rules}} failed to install', values: { failed }, }); @@ -42,7 +42,7 @@ export const RULE_UPGRADE_FAILED = i18n.translate( export const UPGRADE_RULE_SUCCESS = (succeeded: number) => i18n.translate('xpack.securitySolution.detectionEngine.prebuiltRules.toast.upgradeRuleSuccess', { - defaultMessage: '{succeeded, plural, one {# rule} other {# rules}} updated successfully.', + defaultMessage: '{succeeded, plural, one {# rule} other {# rules}} updated successfully', values: { succeeded }, }); @@ -55,6 +55,6 @@ export const UPGRADE_RULE_SKIPPED = (skipped: number) => export const UPGRADE_RULE_FAILED = (failed: number) => i18n.translate('xpack.securitySolution.detectionEngine.prebuiltRules.toast.upgradeRuleFailed', { - defaultMessage: '{failed, plural, one {# rule} other {# rules}} failed to update.', + defaultMessage: '{failed, plural, one {# rule} other {# rules}} failed to update', values: { failed }, }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_perform_rule_install.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_perform_rule_install.ts index 26e565ce9ff6d..5665499c6652f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_perform_rule_install.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_perform_rule_install.ts @@ -4,49 +4,75 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; +import type { IToasts } from '@kbn/core/public'; +import type { PerformRuleInstallationResponseBody } from '../../../../../common/api/detection_engine'; +import { useToasts } from '../../../../common/lib/kibana'; import { usePerformAllRulesInstallMutation } from '../../api/hooks/prebuilt_rules/use_perform_all_rules_install_mutation'; import { usePerformSpecificRulesInstallMutation } from '../../api/hooks/prebuilt_rules/use_perform_specific_rules_install_mutation'; import * as i18n from './translations'; +import { showErrorToast } from '../../../../common/components/utils'; export const usePerformInstallAllRules = () => { - const { addError, addSuccess } = useAppToasts(); + const toasts = useToasts(); return usePerformAllRulesInstallMutation({ - onError: (err) => { - addError(err, { title: i18n.RULE_INSTALLATION_FAILED }); + onError: (error) => { + handleErrorResponse(error, toasts); }, onSuccess: (result) => { - addSuccess(getSuccessToastMessage(result)); + handleSuccessResponse(result, toasts); }, }); }; export const usePerformInstallSpecificRules = () => { - const { addError, addSuccess } = useAppToasts(); + const toasts = useToasts(); return usePerformSpecificRulesInstallMutation({ - onError: (err) => { - addError(err, { title: i18n.RULE_INSTALLATION_FAILED }); + onError: (error) => { + handleErrorResponse(error, toasts); }, onSuccess: (result) => { - addSuccess(getSuccessToastMessage(result)); + handleSuccessResponse(result, toasts); }, }); }; -const getSuccessToastMessage = (result: { +function handleErrorResponse(error: unknown, toasts: IToasts) { + showErrorToast({ + title: i18n.RULE_INSTALLATION_FAILED, + fullMessage: JSON.stringify(error, null, 2), + toasts, + }); +} + +function handleSuccessResponse(result: PerformRuleInstallationResponseBody, toasts: IToasts) { + const successToastMessage = getSuccessToastMessage(result); + if (successToastMessage) { + toasts.addSuccess(successToastMessage); + } + + if (result.summary.failed > 0) { + showErrorToast({ + title: i18n.INSTALL_RULE_FAILED(result.summary.failed), + fullMessage: JSON.stringify(result.errors, null, 2), + toasts, + }); + } +} + +function getSuccessToastMessage(result: { summary: { total: number; succeeded: number; skipped: number; failed: number; }; -}) => { +}): string { const toastMessages: string[] = []; const { - summary: { succeeded, skipped, failed }, + summary: { succeeded, skipped }, } = result; if (succeeded > 0) { toastMessages.push(i18n.INSTALL_RULE_SUCCESS(succeeded)); @@ -54,8 +80,5 @@ const getSuccessToastMessage = (result: { if (skipped > 0) { toastMessages.push(i18n.INSTALL_RULE_SKIPPED(skipped)); } - if (failed > 0) { - toastMessages.push(i18n.INSTALL_RULE_FAILED(failed)); - } return toastMessages.join(' '); -}; +} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_perform_rule_upgrade.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_perform_rule_upgrade.ts index e1ae2768dacc6..c541828d060a0 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_perform_rule_upgrade.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_perform_rule_upgrade.ts @@ -4,24 +4,42 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; + +import { showErrorToast } from '../../../../common/components/utils'; +import { useToasts } from '../../../../common/lib/kibana'; import { usePerformRulesUpgradeMutation } from '../../api/hooks/prebuilt_rules/use_perform_rules_upgrade_mutation'; import * as i18n from './translations'; export const usePerformUpgradeRules = () => { - const { addError, addSuccess } = useAppToasts(); + const toasts = useToasts(); return usePerformRulesUpgradeMutation({ - onError: (err) => { - addError(err, { title: i18n.RULE_UPGRADE_FAILED }); + onError: (error) => { + showErrorToast({ + title: i18n.RULE_UPGRADE_FAILED, + fullMessage: JSON.stringify(error, null, 2), + toasts, + }); }, onSuccess: (result, vars) => { if (vars.dry_run) { // This is a preflight check, no need to show toast return; } - addSuccess(getSuccessToastMessage(result)); + + const successToastMessage = getSuccessToastMessage(result); + if (successToastMessage) { + toasts.addSuccess(getSuccessToastMessage(result)); + } + + if (result.summary.failed > 0) { + showErrorToast({ + title: i18n.UPGRADE_RULE_FAILED(result.summary.failed), + fullMessage: JSON.stringify(result.errors, null, 2), + toasts, + }); + } }, }); }; @@ -36,7 +54,7 @@ const getSuccessToastMessage = (result: { }) => { const toastMessage: string[] = []; const { - summary: { succeeded, skipped, failed }, + summary: { succeeded, skipped }, } = result; if (succeeded > 0) { toastMessage.push(i18n.UPGRADE_RULE_SUCCESS(succeeded)); @@ -44,8 +62,5 @@ const getSuccessToastMessage = (result: { if (skipped > 0) { toastMessage.push(i18n.UPGRADE_RULE_SKIPPED(skipped)); } - if (failed > 0) { - toastMessage.push(i18n.UPGRADE_RULE_FAILED(failed)); - } return toastMessage.join(' '); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_import_modal/utils.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_import_modal/utils.ts index cb17f776572a7..688011267ee8d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_import_modal/utils.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_import_modal/utils.ts @@ -11,6 +11,7 @@ import type { IToasts } from '@kbn/core/public'; import * as i18n from './translations'; import type { ErrorSchema, ImportRulesResponse } from '../../../../../common/api/detection_engine'; +import { showErrorToast } from '../../../../common/components/utils'; export function getFailedConnectorsCount(actionConnectorsErrors: ErrorSchema[]) { const connectorIds = new Set( @@ -46,25 +47,6 @@ function getUserFriendlyConnectorMessages(actionConnectorsErrors: ErrorSchema[]) return mappedErrors; } -function showErrorToast({ - title, - shortMessage, - fullMessage, - toasts, -}: { - title: string; - shortMessage: string; - fullMessage: string; - toasts: IToasts; -}) { - const error = new Error('Error details'); - error.stack = fullMessage; - toasts.addError(error, { - title, - toastMessage: shortMessage, - }); -} - export function showToast({ importResponse, toasts, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/get_prebuilt_rules_status_route.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/get_prebuilt_rules_status_route.ts index adccc7ea6488d..e1fa7d9dc13c9 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/get_prebuilt_rules_status_route.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/get_prebuilt_rules_status_route.ts @@ -12,6 +12,7 @@ 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 { excludeLicenseRestrictedRules, getPossibleUpgrades } from '../../logic/utils'; export const getPrebuiltRulesStatusRoute = (router: SecuritySolutionPluginRouter) => { router.versioned @@ -33,11 +34,12 @@ export const getPrebuiltRulesStatusRoute = (router: SecuritySolutionPluginRouter const siemResponse = buildSiemResponse(response); try { - const ctx = await context.resolve(['core', 'alerting']); + const ctx = await context.resolve(['core', 'alerting', 'securitySolution']); const soClient = ctx.core.savedObjects.client; const rulesClient = await ctx.alerting.getRulesClient(); const ruleAssetsClient = createPrebuiltRuleAssetsClient(soClient); const ruleObjectsClient = createPrebuiltRuleObjectsClient(rulesClient); + const mlAuthz = ctx.securitySolution.getMlAuthz(); const currentRuleVersions = await ruleObjectsClient.fetchInstalledRuleVersions(); const latestRuleVersions = await ruleAssetsClient.fetchLatestVersions(); @@ -47,24 +49,39 @@ export const getPrebuiltRulesStatusRoute = (router: SecuritySolutionPluginRouter const latestRuleVersionsMap = new Map( latestRuleVersions.map((rule) => [rule.rule_id, rule]) ); - const installableRules = latestRuleVersions.filter( + const allInstallableRules = latestRuleVersions.filter( (rule) => !currentRuleVersionsMap.has(rule.rule_id) ); - const upgradeableRules = currentRuleVersions.filter((rule) => { - const latestVersion = latestRuleVersionsMap.get(rule.rule_id); - return latestVersion != null && rule.version < latestVersion.version; - }); + + const installableRuleAssets = await excludeLicenseRestrictedRules( + allInstallableRules, + mlAuthz + ); + + const upgradableRules = await getPossibleUpgrades( + currentRuleVersions, + latestRuleVersionsMap, + mlAuthz + ); + + const upgradeableRulesTags = upgradableRules.reduce((tags, rule) => { + const ruleTags = currentRuleVersionsMap.get(rule.rule_id)?.tags; + if (ruleTags) { + tags.push(...ruleTags); + } + return tags; + }, []); const body: GetPrebuiltRulesStatusResponseBody = { stats: { num_prebuilt_rules_installed: currentRuleVersions.length, - num_prebuilt_rules_to_install: installableRules.length, - num_prebuilt_rules_to_upgrade: upgradeableRules.length, + num_prebuilt_rules_to_install: installableRuleAssets.length, + num_prebuilt_rules_to_upgrade: upgradableRules.length, num_prebuilt_rules_total_in_package: latestRuleVersions.length, }, aggregated_fields: { upgradeable_rules: { - tags: [...new Set(upgradeableRules.flatMap((rule) => rule.tags))], + tags: [...new Set(upgradeableRulesTags)], }, }, }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_handler.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_handler.ts index f156e06d3e5e8..3fa0aba78503e 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_handler.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_handler.ts @@ -22,6 +22,7 @@ import { createPrebuiltRules } from '../../logic/rule_objects/create_prebuilt_ru import { createPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebuilt_rule_objects_client'; import { performTimelinesInstallation } from '../../logic/perform_timelines_installation'; import type { RuleSignatureId, RuleVersion } from '../../../../../../common/api/detection_engine'; +import { excludeLicenseRestrictedRules } from '../../logic/utils'; export const performRuleInstallationHandler = async ( context: SecuritySolutionRequestHandlerContext, @@ -39,6 +40,7 @@ export const performRuleInstallationHandler = async ( const ruleAssetsClient = createPrebuiltRuleAssetsClient(soClient); const ruleObjectsClient = createPrebuiltRuleObjectsClient(rulesClient); const exceptionsListClient = ctx.securitySolution.getExceptionListClient(); + const mlAuthz = ctx.securitySolution.getMlAuthz(); const { mode } = request.body; @@ -55,10 +57,9 @@ export const performRuleInstallationHandler = async ( currentRuleVersions.map((version) => [version.rule_id, version]) ); - const allInstallableRules = allLatestVersions.filter((latestVersion) => { - const currentVersion = currentRuleVersionsMap.get(latestVersion.rule_id); - return !currentVersion; - }); + const allInstallableRules = allLatestVersions.filter( + (latestVersion) => !currentRuleVersionsMap.has(latestVersion.rule_id) + ); const ruleInstallQueue: Array<{ rule_id: RuleSignatureId; @@ -95,7 +96,7 @@ export const performRuleInstallationHandler = async ( ruleInstallQueue.push(rule); }); } else if (mode === 'ALL_RULES') { - ruleInstallQueue.push(...allInstallableRules); + ruleInstallQueue.push(...(await excludeLicenseRestrictedRules(allInstallableRules, mlAuthz))); } const BATCH_SIZE = 100; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_handler.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_handler.ts index 3a9a88870f8f2..bf62755544820 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_handler.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_handler.ts @@ -41,6 +41,7 @@ import { zipRuleVersions } from '../../logic/rule_versions/zip_rule_versions'; import type { RuleVersions } from '../../logic/diff/calculate_rule_diff'; import { calculateRuleDiff } from '../../logic/diff/calculate_rule_diff'; import type { RuleTriad } from '../../model/rule_groups/get_rule_groups'; +import { getPossibleUpgrades } from '../../logic/utils'; export const performRuleUpgradeHandler = async ( context: SecuritySolutionRequestHandlerContext, @@ -56,6 +57,7 @@ export const performRuleUpgradeHandler = async ( const detectionRulesClient = ctx.securitySolution.getDetectionRulesClient(); const ruleAssetsClient = createPrebuiltRuleAssetsClient(soClient); const ruleObjectsClient = createPrebuiltRuleObjectsClient(rulesClient); + const mlAuthz = ctx.securitySolution.getMlAuthz(); const { isRulesCustomizationEnabled } = detectionRulesClient.getRuleCustomizationStatus(); const defaultPickVersion = isRulesCustomizationEnabled @@ -92,15 +94,13 @@ export const performRuleUpgradeHandler = async ( filter, }); - allCurrentVersions.forEach((current) => { - const latest = latestVersionsMap.get(current.rule_id); - if (latest && latest.version > current.version) { - ruleUpgradeQueue.push({ - rule_id: current.rule_id, - version: latest.version, - }); - } - }); + const upgradableRules = await getPossibleUpgrades( + allCurrentVersions, + latestVersionsMap, + mlAuthz + ); + + ruleUpgradeQueue.push(...upgradableRules); } else if (mode === ModeEnum.SPECIFIC_RULES) { ruleUpgradeQueue.push(...request.body.rules); } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_installation/review_rule_installation_handler.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_installation/review_rule_installation_handler.ts index 0bf1ee5cfc80a..ea9c0e3fc65a3 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_installation/review_rule_installation_handler.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_installation/review_rule_installation_handler.ts @@ -17,6 +17,7 @@ import { convertPrebuiltRuleAssetToRuleResponse } from '../../../rule_management import { createPrebuiltRuleAssetsClient } from '../../logic/rule_assets/prebuilt_rule_assets_client'; import { createPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebuilt_rule_objects_client'; import type { PrebuiltRuleAsset } from '../../model/rule_assets/prebuilt_rule_asset'; +import { excludeLicenseRestrictedRules } from '../../logic/utils'; export const reviewRuleInstallationHandler = async ( context: SecuritySolutionRequestHandlerContext, @@ -26,11 +27,12 @@ export const reviewRuleInstallationHandler = async ( const siemResponse = buildSiemResponse(response); try { - const ctx = await context.resolve(['core', 'alerting']); + const ctx = await context.resolve(['core', 'alerting', 'securitySolution']); const soClient = ctx.core.savedObjects.client; const rulesClient = await ctx.alerting.getRulesClient(); const ruleAssetsClient = createPrebuiltRuleAssetsClient(soClient); const ruleObjectsClient = createPrebuiltRuleObjectsClient(rulesClient); + const mlAuthz = ctx.securitySolution.getMlAuthz(); const allLatestVersions = await ruleAssetsClient.fetchLatestVersions(); const currentRuleVersions = await ruleObjectsClient.fetchInstalledRuleVersions(); @@ -38,11 +40,15 @@ export const reviewRuleInstallationHandler = async ( currentRuleVersions.map((version) => [version.rule_id, version]) ); - const installableRules = allLatestVersions.filter((latestVersion) => { - const currentVersion = currentRuleVersionsMap.get(latestVersion.rule_id); - return !currentVersion; - }); - const installableRuleAssets = await ruleAssetsClient.fetchAssetsByVersion(installableRules); + const allInstallableRules = allLatestVersions.filter( + (latestVersion) => !currentRuleVersionsMap.has(latestVersion.rule_id) + ); + + const nonInstalledRuleAssets = await ruleAssetsClient.fetchAssetsByVersion(allInstallableRules); + const installableRuleAssets = await excludeLicenseRestrictedRules( + nonInstalledRuleAssets, + mlAuthz + ); const body: ReviewRuleInstallationResponseBody = { stats: calculateRuleStats(installableRuleAssets), diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_handler.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_handler.ts index 05a52dbf0ed41..46f3df1a4ee5b 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_handler.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_handler.ts @@ -20,9 +20,11 @@ import type { IPrebuiltRuleAssetsClient } from '../../logic/rule_assets/prebuilt import { createPrebuiltRuleAssetsClient } from '../../logic/rule_assets/prebuilt_rule_assets_client'; import type { IPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebuilt_rule_objects_client'; import { createPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebuilt_rule_objects_client'; -import type { RuleVersionSpecifier } from '../../logic/rule_versions/rule_version_specifier'; +import { type RuleVersionSpecifier } from '../../logic/rule_versions/rule_version_specifier'; import { zipRuleVersions } from '../../logic/rule_versions/zip_rule_versions'; import { calculateRuleUpgradeInfo } from './calculate_rule_upgrade_info'; +import type { MlAuthz } from '../../../../machine_learning/authz'; +import { getPossibleUpgrades } from '../../logic/utils'; const DEFAULT_SORT: ReviewRuleUpgradeSort = { field: 'name', @@ -38,15 +40,17 @@ export const reviewRuleUpgradeHandler = async ( const { page = 1, per_page: perPage = 20, sort = DEFAULT_SORT, filter } = request.body ?? {}; try { - const ctx = await context.resolve(['core', 'alerting']); + const ctx = await context.resolve(['core', 'alerting', 'securitySolution']); const soClient = ctx.core.savedObjects.client; const rulesClient = await ctx.alerting.getRulesClient(); const ruleAssetsClient = createPrebuiltRuleAssetsClient(soClient); const ruleObjectsClient = createPrebuiltRuleObjectsClient(rulesClient); + const mlAuthz = ctx.securitySolution.getMlAuthz(); const { diffResults, totalUpgradeableRules } = await calculateUpgradeableRulesDiff({ ruleAssetsClient, ruleObjectsClient, + mlAuthz, page, perPage, sort, @@ -79,6 +83,7 @@ export const reviewRuleUpgradeHandler = async ( interface CalculateUpgradeableRulesDiffArgs { ruleAssetsClient: IPrebuiltRuleAssetsClient; ruleObjectsClient: IPrebuiltRuleObjectsClient; + mlAuthz: MlAuthz; page: number; perPage: number; sort: ReviewRuleUpgradeSort; @@ -88,6 +93,7 @@ interface CalculateUpgradeableRulesDiffArgs { async function calculateUpgradeableRulesDiff({ ruleAssetsClient, ruleObjectsClient, + mlAuthz, page, perPage, sort, @@ -107,15 +113,19 @@ async function calculateUpgradeableRulesDiff({ sortField: sort.field, sortOrder: sort.order, }); - const upgradeableRuleIds = currentRuleVersions - .filter((rule) => { - const targetVersion = latestVersionsMap.get(rule.rule_id); - return targetVersion != null && rule.version < targetVersion.version; - }) + + const upgradeableRules = await getPossibleUpgrades( + currentRuleVersions, + latestVersionsMap, + mlAuthz + ); + + const totalUpgradeableRules = upgradeableRules.length; + + const pagedRuleIds = upgradeableRules + .slice((page - 1) * perPage, page * perPage) .map((rule) => rule.rule_id); - const totalUpgradeableRules = upgradeableRuleIds.length; - const pagedRuleIds = upgradeableRuleIds.slice((page - 1) * perPage, page * perPage); const currentRules = await ruleObjectsClient.fetchInstalledRulesByIds({ ruleIds: pagedRuleIds, sortField: sort.field, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/basic_rule_info.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/basic_rule_info.ts new file mode 100644 index 0000000000000..b82736f117655 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/basic_rule_info.ts @@ -0,0 +1,13 @@ +/* + * 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 { Type } from '@kbn/securitysolution-io-ts-alerting-types'; +import type { RuleVersionSpecifier } from './rule_versions/rule_version_specifier'; + +export interface BasicRuleInfo extends RuleVersionSpecifier { + type: Type; +} diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client.ts index b5c1199a48dee..2e2e25ba72d13 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client.ts @@ -18,6 +18,7 @@ import type { PrebuiltRuleAsset } from '../../model/rule_assets/prebuilt_rule_as import { validatePrebuiltRuleAssets } from './prebuilt_rule_assets_validation'; import { PREBUILT_RULE_ASSETS_SO_TYPE } from './prebuilt_rule_assets_type'; import type { RuleVersionSpecifier } from '../rule_versions/rule_version_specifier'; +import type { BasicRuleInfo } from '../basic_rule_info'; const RULE_ASSET_ATTRIBUTES = `${PREBUILT_RULE_ASSETS_SO_TYPE}.attributes`; const MAX_PREBUILT_RULES_COUNT = 10_000; @@ -27,7 +28,7 @@ const ES_MAX_CONCURRENT_REQUESTS = 2; export interface IPrebuiltRuleAssetsClient { fetchLatestAssets: () => Promise; - fetchLatestVersions(ruleIds?: string[]): Promise; + fetchLatestVersions(ruleIds?: string[]): Promise; fetchAssetsByVersion(versions: RuleVersionSpecifier[]): Promise; } @@ -79,7 +80,7 @@ export const createPrebuiltRuleAssetsClient = ( }); }, - fetchLatestVersions: (ruleIds?: string[]): Promise => { + fetchLatestVersions: (ruleIds?: string[]): Promise => { return withSecuritySpan('IPrebuiltRuleAssetsClient.fetchLatestVersions', async () => { if (ruleIds && ruleIds.length === 0) { return []; @@ -114,6 +115,7 @@ export const createPrebuiltRuleAssetsClient = ( _source: [ `${PREBUILT_RULE_ASSETS_SO_TYPE}.rule_id`, `${PREBUILT_RULE_ASSETS_SO_TYPE}.version`, + `${PREBUILT_RULE_ASSETS_SO_TYPE}.type`, ], }, }, @@ -141,9 +143,10 @@ export const createPrebuiltRuleAssetsClient = ( const latestVersions = buckets.map((bucket) => { const hit = bucket.latest_version.hits.hits[0]; const soAttributes = hit._source[PREBUILT_RULE_ASSETS_SO_TYPE]; - const versionInfo: RuleVersionSpecifier = { + const versionInfo: BasicRuleInfo = { rule_id: soAttributes.rule_id, version: soAttributes.version, + type: soAttributes.type, }; return versionInfo; }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client.ts index 2bf7cf5c44364..5bce3d8ee59cb 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client.ts @@ -7,9 +7,11 @@ import type { RulesClient } from '@kbn/alerting-plugin/server'; import type { + RuleObjectId, RuleResponse, RuleSignatureId, RuleTagArray, + RuleVersion, } from '../../../../../../common/api/detection_engine/model/rule_schema'; import { withSecuritySpan } from '../../../../../utils/with_security_span'; import { findRules } from '../../../rule_management/logic/search/find_rules'; @@ -21,7 +23,6 @@ import type { SortOrder, } from '../../../../../../common/api/detection_engine'; import { MAX_PREBUILT_RULES_COUNT } from '../../../rule_management/logic/search/get_existing_prepackaged_rules'; -import type { RuleVersionSpecifier } from '../rule_versions/rule_version_specifier'; interface FetchAllInstalledRulesArgs { page?: number; @@ -49,15 +50,20 @@ interface FetchInstalledRulesByIdsArgs { sortOrder?: SortOrder; } +interface RuleSummary { + id: RuleObjectId; + rule_id: RuleSignatureId; + version: RuleVersion; + tags: RuleTagArray; +} + export interface IPrebuiltRuleObjectsClient { fetchInstalledRulesByIds(args: FetchInstalledRulesByIdsArgs): Promise; fetchInstalledRules(args?: FetchAllInstalledRulesArgs): Promise; fetchInstalledRuleVersionsByIds( args: FetchInstalledRuleVersionsByIdsArgs - ): Promise>; - fetchInstalledRuleVersions( - args?: FetchAllInstalledRuleVersionsArgs - ): Promise>; + ): Promise; + fetchInstalledRuleVersions(args?: FetchAllInstalledRuleVersionsArgs): Promise; } export const createPrebuiltRuleObjectsClient = ( @@ -125,6 +131,7 @@ export const createPrebuiltRuleObjectsClient = ( fields: ['params.ruleId', 'params.version', 'tags'], }); return rulesData.data.map((rule) => ({ + id: rule.id, rule_id: rule.params.ruleId, version: rule.params.version, tags: rule.tags, @@ -151,6 +158,7 @@ export const createPrebuiltRuleObjectsClient = ( fields: ['params.ruleId', 'params.version', 'tags'], }); return rulesData.data.map((rule) => ({ + id: rule.id, rule_id: rule.params.ruleId, version: rule.params.version, tags: rule.tags, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/utils.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/utils.ts index 9c1b3fdff54b3..78706f6060bb9 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/utils.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/utils.ts @@ -5,7 +5,11 @@ * 2.0. */ +import type { Type } from '@kbn/securitysolution-io-ts-alerting-types'; +import type { MlAuthz } from '../../../machine_learning/authz'; import type { RuleAlertType } from '../../rule_schema'; +import type { RuleVersionSpecifier } from './rule_versions/rule_version_specifier'; +import type { BasicRuleInfo } from './basic_rule_info'; /** * Converts an array of rules to a Map with rule IDs as keys @@ -15,3 +19,51 @@ import type { RuleAlertType } from '../../rule_schema'; */ export const rulesToMap = (rules: RuleAlertType[]) => new Map(rules.map((rule) => [rule.params.ruleId, rule])); + +/** + * Excludes rules that are not allowed under the current license. + * + * @param rules The array of rule objects to filter + * @param mlAuthz Machine Learning authorization object + * @returns A new array containing only the rules that are allowed under the current license + */ +export async function excludeLicenseRestrictedRules( + rules: T[], + mlAuthz: MlAuthz +): Promise { + const validationResults = await Promise.all( + rules.map((rule) => mlAuthz.validateRuleType(rule.type)) + ); + + return rules.filter((_rule, index) => validationResults[index].valid); +} + +function getUpgradeTargets( + currentRules: RuleVersionSpecifier[], + targetRulesMap: Map +): BasicRuleInfo[] { + return currentRules.reduce((allUpgradableRules, currentRule) => { + const targetRule = targetRulesMap.get(currentRule.rule_id); + if (targetRule && currentRule.version < targetRule.version) { + allUpgradableRules.push(targetRule); + } + return allUpgradableRules; + }, []); +} + +/** + * Given current and a target rules, returns a list of possible upgrade targets. + * + * @param currentRules The list of rules currently installed. + * @param targetRulesMap A map of the latest available rule versions, with rule_id as the key. + * @param mlAuthz Machine Learning authorization object + * @returns An array of target rule version specifiers. + */ +export function getPossibleUpgrades( + currentRules: RuleVersionSpecifier[], + targetRulesMap: Map, + mlAuthz: MlAuthz +): Promise { + const upgradeTargets = getUpgradeTargets(currentRules, targetRulesMap); + return excludeLicenseRestrictedRules(upgradeTargets, mlAuthz); +} diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts index b9d23fc1e6db9..022c9278db47b 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts @@ -170,6 +170,9 @@ const createSecuritySolutionRequestContextMock = ( getEntityStoreDataClient: jest.fn(() => clients.entityStoreDataClient), getSiemRuleMigrationsClient: jest.fn(() => clients.siemRuleMigrationsClient), getInferenceClient: jest.fn(() => clients.getInferenceClient()), + getMlAuthz: jest.fn(() => ({ + validateRuleType: jest.fn(async () => ({ valid: true, message: undefined })), + })), }; }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/request_context_factory.ts b/x-pack/solutions/security/plugins/security_solution/server/request_context_factory.ts index 40ef090798741..938c37392316e 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/request_context_factory.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/request_context_factory.ts @@ -274,6 +274,14 @@ export class RequestContextFactory implements IRequestContextFactory { request, }); }), + getMlAuthz: memoize(() => { + return buildMlAuthz({ + license: licensing.license, + ml: plugins.ml, + request, + savedObjectsClient: coreContext.savedObjects.client, + }); + }), }; } } diff --git a/x-pack/solutions/security/plugins/security_solution/server/types.ts b/x-pack/solutions/security/plugins/security_solution/server/types.ts index 0c2385564f010..bbc050afaea4e 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/types.ts @@ -39,6 +39,7 @@ import type { IDetectionRulesClient } from './lib/detection_engine/rule_manageme import type { EntityStoreDataClient } from './lib/entity_analytics/entity_store/entity_store_data_client'; import type { SiemRuleMigrationsClient } from './lib/siem_migrations/rules/siem_rule_migrations_service'; import type { ApiKeyManager } from './lib/entity_analytics/entity_store/auth/api_key'; +import type { MlAuthz } from './lib/machine_learning/authz'; export { AppClient }; export interface SecuritySolutionApiRequestHandlerContext { @@ -65,6 +66,7 @@ export interface SecuritySolutionApiRequestHandlerContext { getEntityStoreDataClient: () => EntityStoreDataClient; getSiemRuleMigrationsClient: () => SiemRuleMigrationsClient; getInferenceClient: () => InferenceClient; + getMlAuthz: () => MlAuthz; } export type SecuritySolutionRequestHandlerContext = CustomRequestHandlerContext<{ diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/configs/ess_basic_license.config.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/configs/ess_basic_license.config.ts new file mode 100644 index 0000000000000..9862899071b15 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/configs/ess_basic_license.config.ts @@ -0,0 +1,25 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile( + require.resolve('../../../../../../config/ess/config.base.basic') + ); + + const testConfig = { + ...functionalConfig.getAll(), + testFiles: [require.resolve('..')], + junit: { + reportName: + 'Rules Management - Prebuilt Rules (ML Disabled) - Integration Tests - ESS Env Basic License', + }, + }; + + return testConfig; +} diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/configs/serverless_essentials_tier.config.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/configs/serverless_essentials_tier.config.ts new file mode 100644 index 0000000000000..5f28201a6523e --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/configs/serverless_essentials_tier.config.ts @@ -0,0 +1,16 @@ +/* + * 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 { createTestConfig } from '../../../../../../config/serverless/config.base.essentials'; + +export default createTestConfig({ + testFiles: [require.resolve('..')], + junit: { + reportName: + 'Rules Management - Prebuilt Rules (ML Disabled) - Integration Tests - Serverless Env Essentials Tier', + }, +}); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/index.ts new file mode 100644 index 0000000000000..3c3f4580947c7 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/index.ts @@ -0,0 +1,18 @@ +/* + * 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 { FtrProviderContext } from '../../../../../ftr_provider_context'; + +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('Rules Management - Prebuilt Rules (ML Disabled)', function () { + loadTestFile(require.resolve('./review_installation')); + loadTestFile(require.resolve('./perform_installation')); + loadTestFile(require.resolve('./review_upgrade')); + loadTestFile(require.resolve('./perform_upgrade')); + loadTestFile(require.resolve('./status')); + }); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/perform_installation/index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/perform_installation/index.ts new file mode 100644 index 0000000000000..92cbf2e78b6a8 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/perform_installation/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../../../ftr_provider_context'; + +export default ({ loadTestFile }: FtrProviderContext): void => { + loadTestFile(require.resolve('./perform_installation')); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/perform_installation/perform_installation.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/perform_installation/perform_installation.ts new file mode 100644 index 0000000000000..1c71323c2521d --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/perform_installation/perform_installation.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from 'expect'; +import { FtrProviderContext } from '../../../../../../ftr_provider_context'; +import { + createPrebuiltRuleAssetSavedObjects, + createRuleAssetSavedObjectOfType, + deleteAllPrebuiltRuleAssets, + installPrebuiltRules, +} from '../../../../utils'; +import { deleteAllRules } from '../../../../../../../common/utils/security_solution'; + +export default ({ getService }: FtrProviderContext): void => { + const es = getService('es'); + const supertest = getService('supertest'); + const log = getService('log'); + + describe('@ess @serverless @skipInServerlessMKI Prebuilt rules installation perform', () => { + beforeEach(async () => { + await deleteAllRules(supertest, log); + await deleteAllPrebuiltRuleAssets(es, log); + }); + + it('ML rules are skipped during installation if mode is ALL_RULES', async () => { + const mlRuleAsset = createRuleAssetSavedObjectOfType('machine_learning'); + const nonMlRuleAsset = createRuleAssetSavedObjectOfType('query'); + await createPrebuiltRuleAssetSavedObjects(es, [mlRuleAsset, nonMlRuleAsset]); + + const performInstallationResponse = await installPrebuiltRules(es, supertest); + expect(performInstallationResponse).toMatchObject({ + summary: { + total: 1, + succeeded: 1, + skipped: 0, // ML rules are skipped silently as if they were not present, so they don't count towards "skipped" + failed: 0, + }, + results: { + created: [{ type: 'query' }], + }, + }); + }); + + it('ML rules produce an error during installation if mode is SPECIFIC_RULES', async () => { + const mlRuleFields = { rule_id: 'ml-rule', version: 1 }; + const mlRuleAsset = createRuleAssetSavedObjectOfType('machine_learning', mlRuleFields); + + const nonMlRuleFields = { rule_id: 'non-ml-rule', version: 1 }; + const nonMlRuleAsset = createRuleAssetSavedObjectOfType('query', nonMlRuleFields); + + await createPrebuiltRuleAssetSavedObjects(es, [mlRuleAsset, nonMlRuleAsset]); + + const performInstallationResponse = await installPrebuiltRules(es, supertest, [ + mlRuleFields, + nonMlRuleFields, + ]); + + expect(performInstallationResponse).toMatchObject({ + summary: { total: 2, succeeded: 1, skipped: 0, failed: 1 }, + results: { + created: [{ rule_id: nonMlRuleFields.rule_id }], + }, + errors: [ + { + message: 'Your license does not support machine learning. Please upgrade your license.', + status_code: 403, + rules: [{ rule_id: mlRuleFields.rule_id }], + }, + ], + }); + }); + }); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/perform_upgrade/index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/perform_upgrade/index.ts new file mode 100644 index 0000000000000..ce7681a102e60 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/perform_upgrade/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../../../ftr_provider_context'; + +export default ({ loadTestFile }: FtrProviderContext): void => { + loadTestFile(require.resolve('./perform_upgrade')); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/perform_upgrade/perform_upgrade.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/perform_upgrade/perform_upgrade.ts new file mode 100644 index 0000000000000..07e4c6f5a4590 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/perform_upgrade/perform_upgrade.ts @@ -0,0 +1,186 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from 'expect'; +import { ModeEnum } from '@kbn/security-solution-plugin/common/api/detection_engine'; +import { FtrProviderContext } from '../../../../../../ftr_provider_context'; +import { + createPrebuiltRuleAssetSavedObjects, + createRuleAssetSavedObjectOfType, + deleteAllPrebuiltRuleAssets, + performUpgradePrebuiltRules, +} from '../../../../utils'; +import { deleteAllRules } from '../../../../../../../common/utils/security_solution'; +import { setUpRuleUpgrade } from '../../../../utils/rules/prebuilt_rules/set_up_rule_upgrade'; +import { createMlRuleThroughAlertingEndpoint } from '../utils'; + +export default ({ getService }: FtrProviderContext): void => { + const es = getService('es'); + const supertest = getService('supertest'); + const log = getService('log'); + const deps = { + es, + supertest, + log, + }; + + describe('@ess @serverless @skipInServerlessMKI Prebuilt rules upgrade perform', () => { + beforeEach(async () => { + await deleteAllRules(supertest, log); + await deleteAllPrebuiltRuleAssets(es, log); + }); + + const ruleId = 'ml-rule'; + + describe('ALL_RULES mode', () => { + it('silently skips ML rules in ALL_RULES mode', async () => { + await setUpRuleUpgrade({ + assets: { + installed: { + type: 'machine_learning', + version: 1, + }, + upgrade: { + type: 'machine_learning', + version: 2, + }, + patch: {}, + }, + deps, + }); + + const upgradePerformResult = await performUpgradePrebuiltRules(es, supertest, { + mode: ModeEnum.ALL_RULES, + }); + + expect(upgradePerformResult).toMatchObject({ + summary: { + total: 0, + succeeded: 0, + failed: 0, + }, + errors: [], + }); + }); + }); + + describe('SPECIFIC_RULES mode', () => { + it(`doesn't upgrade if target is an ML rule`, async () => { + await createMlRuleThroughAlertingEndpoint(supertest, { + ruleId, + version: 1, + }); + + const targetMlRuleAsset = createRuleAssetSavedObjectOfType('machine_learning', { + rule_id: ruleId, + version: 2, + }); + await createPrebuiltRuleAssetSavedObjects(es, [targetMlRuleAsset]); + + const upgradePerformResult = await performUpgradePrebuiltRules(es, supertest, { + mode: ModeEnum.SPECIFIC_RULES, + rules: [ + { + rule_id: ruleId, + revision: 0, + version: 2, + }, + ], + }); + + expect(upgradePerformResult).toMatchObject({ + summary: { + total: 1, + failed: 1, + }, + errors: [ + { + message: + 'Your license does not support machine learning. Please upgrade your license.', + rules: [{ rule_id: ruleId }], + }, + ], + }); + }); + + it('upgrades successfully if current rule is an ML rule, but target is a non-ML rule', async () => { + await createMlRuleThroughAlertingEndpoint(supertest, { + ruleId, + version: 1, + }); + + const targetMlRuleAsset = createRuleAssetSavedObjectOfType('query', { + rule_id: ruleId, + version: 2, + }); + await createPrebuiltRuleAssetSavedObjects(es, [targetMlRuleAsset]); + + const upgradePerformResult = await performUpgradePrebuiltRules(es, supertest, { + mode: ModeEnum.SPECIFIC_RULES, + rules: [ + { + rule_id: ruleId, + revision: 0, + version: 2, + pick_version: 'TARGET', + }, + ], + }); + + expect(upgradePerformResult).toMatchObject({ + summary: { + total: 1, + succeeded: 1, + }, + results: { + updated: [{ rule_id: ruleId }], + }, + }); + }); + + it('upgrades successfully if both current and target are non-ML rules', async () => { + await setUpRuleUpgrade({ + assets: { + installed: { + type: 'query', + version: 1, + rule_id: ruleId, + }, + upgrade: { + type: 'query', + version: 2, + rule_id: ruleId, + }, + patch: {}, + }, + deps, + }); + + const upgradePerformResult = await performUpgradePrebuiltRules(es, supertest, { + mode: ModeEnum.SPECIFIC_RULES, + rules: [ + { + rule_id: ruleId, + revision: 0, + version: 2, + }, + ], + }); + + expect(upgradePerformResult).toMatchObject({ + summary: { + total: 1, + succeeded: 1, + }, + results: { + updated: [{ rule_id: ruleId }], + }, + }); + }); + }); + }); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/review_installation/index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/review_installation/index.ts new file mode 100644 index 0000000000000..f16796e2162eb --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/review_installation/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../../../ftr_provider_context'; + +export default ({ loadTestFile }: FtrProviderContext): void => { + loadTestFile(require.resolve('./review_installation')); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/review_installation/review_installation.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/review_installation/review_installation.ts new file mode 100644 index 0000000000000..ea5cc275ea94c --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/review_installation/review_installation.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from 'expect'; +import { FtrProviderContext } from '../../../../../../ftr_provider_context'; +import { + createPrebuiltRuleAssetSavedObjects, + createRuleAssetSavedObjectOfType, + deleteAllPrebuiltRuleAssets, + reviewPrebuiltRulesToInstall, +} from '../../../../utils'; +import { deleteAllRules } from '../../../../../../../common/utils/security_solution'; + +export default ({ getService }: FtrProviderContext): void => { + const es = getService('es'); + const supertest = getService('supertest'); + const log = getService('log'); + + describe('@ess @serverless @skipInServerlessMKI Prebuilt rules installation review', () => { + beforeEach(async () => { + await deleteAllRules(supertest, log); + await deleteAllPrebuiltRuleAssets(es, log); + }); + + it('ML rules are excluded from response', async () => { + const mlRuleAsset = createRuleAssetSavedObjectOfType('machine_learning', { + tags: ['Type: ML'], + }); + const nonMlRuleAsset = createRuleAssetSavedObjectOfType('query', { + tags: ['Type: Custom Query'], + }); + + await createPrebuiltRuleAssetSavedObjects(es, [mlRuleAsset, nonMlRuleAsset]); + + const prebuiltRulesToInstallReview = await reviewPrebuiltRulesToInstall(supertest); + + expect(prebuiltRulesToInstallReview.stats.num_rules_to_install).toBe(1); + expect(prebuiltRulesToInstallReview.rules.length).toBe(1); + expect(prebuiltRulesToInstallReview.rules[0]?.type).toBe('query'); + + // Ensure tags from ML rules are not included + expect(prebuiltRulesToInstallReview.stats.tags).toEqual(['Type: Custom Query']); + }); + }); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/review_upgrade/index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/review_upgrade/index.ts new file mode 100644 index 0000000000000..b7722e9f25463 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/review_upgrade/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../../../ftr_provider_context'; + +export default ({ loadTestFile }: FtrProviderContext): void => { + loadTestFile(require.resolve('./review_upgrade')); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/review_upgrade/review_upgrade.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/review_upgrade/review_upgrade.ts new file mode 100644 index 0000000000000..69d313483f253 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/review_upgrade/review_upgrade.ts @@ -0,0 +1,128 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from 'expect'; +import { FtrProviderContext } from '../../../../../../ftr_provider_context'; +import { + createPrebuiltRuleAssetSavedObjects, + createRuleAssetSavedObjectOfType, + deleteAllPrebuiltRuleAssets, + reviewPrebuiltRulesToUpgrade, +} from '../../../../utils'; +import { deleteAllRules } from '../../../../../../../common/utils/security_solution'; +import { createMlRuleThroughAlertingEndpoint } from '../utils'; +import { setUpRuleUpgrade } from '../../../../utils/rules/prebuilt_rules/set_up_rule_upgrade'; + +export default ({ getService }: FtrProviderContext): void => { + const es = getService('es'); + const supertest = getService('supertest'); + const log = getService('log'); + const deps = { + es, + supertest, + log, + }; + + describe('@ess @serverless @skipInServerlessMKI Prebuilt rules upgrade review', () => { + beforeEach(async () => { + await deleteAllRules(supertest, log); + await deleteAllPrebuiltRuleAssets(es, log); + }); + + const ruleId = 'ml-rule'; + + it('rule is excluded from response if target is an ML rule', async () => { + await createMlRuleThroughAlertingEndpoint(supertest, { + ruleId, + version: 1, + }); + + const targetMlRuleAsset = createRuleAssetSavedObjectOfType('machine_learning', { + rule_id: ruleId, + version: 2, + }); + await createPrebuiltRuleAssetSavedObjects(es, [targetMlRuleAsset]); + + const upgradeReviewResult = await reviewPrebuiltRulesToUpgrade(supertest); + expect(upgradeReviewResult).toMatchObject({ + total: 0, + rules: [], + }); + }); + + it('rule is included in response if current rule is an ML rule, but target is a non-ML rule', async () => { + await createMlRuleThroughAlertingEndpoint(supertest, { + ruleId, + version: 1, + }); + + const targetRuleAsset = createRuleAssetSavedObjectOfType('query', { + rule_id: ruleId, + version: 2, + }); + await createPrebuiltRuleAssetSavedObjects(es, [targetRuleAsset]); + + const upgradeReviewResult = await reviewPrebuiltRulesToUpgrade(supertest); + expect(upgradeReviewResult).toMatchObject({ + total: 1, + rules: [ + { + current_rule: { + rule_id: ruleId, + type: 'machine_learning', + version: 1, + }, + target_rule: { + rule_id: ruleId, + type: 'query', + version: 2, + }, + }, + ], + }); + }); + + it('rule is included in response if both current and target are non-ML rules', async () => { + await setUpRuleUpgrade({ + assets: { + installed: { + type: 'query', + rule_id: ruleId, + version: 1, + }, + upgrade: { + type: 'query', + rule_id: ruleId, + version: 2, + }, + patch: {}, + }, + deps, + }); + + const upgradeReviewResult = await reviewPrebuiltRulesToUpgrade(supertest); + + expect(upgradeReviewResult).toMatchObject({ + total: 1, + rules: [ + { + current_rule: { + rule_id: ruleId, + type: 'query', + version: 1, + }, + target_rule: { + rule_id: ruleId, + type: 'query', + version: 2, + }, + }, + ], + }); + }); + }); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/status/index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/status/index.ts new file mode 100644 index 0000000000000..75f8b0fa5eed1 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/status/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../../../ftr_provider_context'; + +export default ({ loadTestFile }: FtrProviderContext): void => { + loadTestFile(require.resolve('./status')); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/status/status.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/status/status.ts new file mode 100644 index 0000000000000..638315dc85bdd --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/status/status.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from 'expect'; +import { FtrProviderContext } from '../../../../../../ftr_provider_context'; +import { + createPrebuiltRuleAssetSavedObjects, + createRuleAssetSavedObjectOfType, + deleteAllPrebuiltRuleAssets, + getPrebuiltRulesStatus, +} from '../../../../utils'; +import { deleteAllRules } from '../../../../../../../common/utils/security_solution'; +import { createMlRuleThroughAlertingEndpoint } from '../utils'; + +export default ({ getService }: FtrProviderContext): void => { + const es = getService('es'); + const supertest = getService('supertest'); + const log = getService('log'); + + describe('@ess @serverless @skipInServerlessMKI Prebuilt rules status', () => { + beforeEach(async () => { + await deleteAllRules(supertest, log); + await deleteAllPrebuiltRuleAssets(es, log); + }); + + it('ML rules are not counted towards installable rules', async () => { + const mlRuleAsset = createRuleAssetSavedObjectOfType('machine_learning', { + version: 1, + }); + await createPrebuiltRuleAssetSavedObjects(es, [mlRuleAsset]); + + const statusResponse = await getPrebuiltRulesStatus(es, supertest); + expect(statusResponse).toMatchObject({ + stats: { + num_prebuilt_rules_to_install: 0, + num_prebuilt_rules_total_in_package: 1, + }, + }); + }); + + it('ML rules are not counted towards upgradable rules', async () => { + await createMlRuleThroughAlertingEndpoint(supertest, { + ruleId: 'ml-rule', + version: 1, + }); + + const targetMlRuleAsset = createRuleAssetSavedObjectOfType('machine_learning', { + rule_id: 'ml-rule', + version: 2, + }); + await createPrebuiltRuleAssetSavedObjects(es, [targetMlRuleAsset]); + + const statusResponse = await getPrebuiltRulesStatus(es, supertest); + expect(statusResponse).toMatchObject({ + stats: { + num_prebuilt_rules_to_upgrade: 0, + num_prebuilt_rules_installed: 1, + num_prebuilt_rules_total_in_package: 1, + }, + }); + }); + }); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/utils.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/utils.ts new file mode 100644 index 0000000000000..c2f86183a0d1f --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/ml_disabled/utils.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type SuperTest from 'supertest'; +import { ML_RULE_TYPE_ID } from '@kbn/securitysolution-rules'; +import { RuleParamsWithDefaultValue } from '@kbn/response-ops-rule-params'; +import { CreateRuleRequestBody } from '@kbn/alerting-plugin/common/routes/rule/apis/create'; +import { createRuleThroughAlertingEndpoint } from '../../../utils'; + +/** + * Creates a machine learning rule through the alerting endpoint. + * Useful during testing under Basic and Essential licenses, as the DE rule creation API does not permit the creation of ML rules under those licenses. + */ +export async function createMlRuleThroughAlertingEndpoint( + supertest: SuperTest.Agent, + paramsOverride: Partial = {} +): Promise { + const params: RuleParamsWithDefaultValue = { + alertSuppression: undefined, + anomalyThreshold: 50, + author: [], + description: 'rule', + exceptionsList: [], + falsePositives: [], + from: 'now-5m', + immutable: true, + machineLearningJobId: ['ml-rule-job'], + outputIndex: '', + references: [], + responseActions: undefined, + riskScore: 1, + riskScoreMapping: [], + ruleId: 'ml-rule', + severity: 'low', + severityMapping: [], + threat: [], + to: 'now', + type: 'machine_learning', + version: 1, + ...paramsOverride, + }; + + const body: CreateRuleRequestBody = { + actions: [], + consumer: 'siem', + enabled: false, + name: 'Test ML rule', + params, + rule_type_id: ML_RULE_TYPE_ID, + schedule: { + interval: '5m', + }, + tags: [], + throttle: null, + }; + + await createRuleThroughAlertingEndpoint(supertest, body); +} diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/create_rule_saved_object.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/create_rule_saved_object.ts index f4e3f22a0c9a6..39cabfa706fa6 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/create_rule_saved_object.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/create_rule_saved_object.ts @@ -8,10 +8,9 @@ import type SuperTest from 'supertest'; import { Rule } from '@kbn/alerting-plugin/common'; -import { - BaseRuleParams, - InternalRuleCreate, -} from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_schema'; +import { BaseRuleParams } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_schema'; +import { RuleParamsWithDefaultValue } from '@kbn/response-ops-rule-params'; +import { CreateRuleRequestBody } from '@kbn/alerting-plugin/common/routes/rule/apis/create'; /** * Creates a rule using the alerting APIs directly. @@ -22,7 +21,7 @@ import { */ export const createRuleThroughAlertingEndpoint = async ( supertest: SuperTest.Agent, - rule: InternalRuleCreate + rule: CreateRuleRequestBody ): Promise> => { const { body } = await supertest .post('/api/alerting/rule') diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/get_rule_with_legacy_investigation_fields.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/get_rule_with_legacy_investigation_fields.ts index 9b315dbd9dbb5..723da5e5aafd9 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/get_rule_with_legacy_investigation_fields.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/get_rule_with_legacy_investigation_fields.ts @@ -5,128 +5,86 @@ * 2.0. */ -import { InternalRuleCreate } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_schema'; +import { RuleParamsWithDefaultValue } from '@kbn/response-ops-rule-params'; +import { CreateRuleRequestBody } from '@kbn/alerting-plugin/common/routes/rule/apis/create'; + +const baseBody = { + name: 'Test investigation fields', + tags: ['migration'], + rule_type_id: 'siem.queryRule', + consumer: 'siem', + schedule: { + interval: '5m', + }, + enabled: false, + actions: [], + throttle: null, +}; + +const baseParams: RuleParamsWithDefaultValue = { + alertSuppression: undefined, + author: [], + buildingBlockType: undefined, + dataViewId: undefined, + description: 'a', + exceptionsList: [], + falsePositives: [], + filters: [], + from: '1900-01-01T00:00:00.000Z', + immutable: false, + index: ['auditbeat-*'], + investigationFields: ['client.address', 'agent.name'], + language: 'kuery', + license: '', + maxSignals: 100, + meta: undefined, + namespace: undefined, + note: undefined, + outputIndex: '', + query: '_id:BhbXBmkBR346wHgn4PeZ or _id:GBbXBmkBR346wHgn5_eR or _id:x10zJ2oE9v5HJNSHhyxi', + references: [], + relatedIntegrations: [], + requiredFields: undefined, + responseActions: undefined, + riskScore: 21, + riskScoreMapping: [], + ruleId: '2297be91-894c-4831-830f-b424a0ec84f0', + ruleNameOverride: undefined, + savedId: undefined, + setup: '', + severity: 'low', + severityMapping: [], + threat: [], + timelineId: undefined, + timelineTitle: undefined, + timestampOverride: undefined, + timestampOverrideFallbackDisabled: undefined, + to: 'now', + type: 'query', + version: 1, +}; export const getRuleSavedObjectWithLegacyInvestigationFields = ( - rewrites?: Partial -): InternalRuleCreate => - ({ - name: 'Test investigation fields', - tags: ['migration'], - rule_type_id: 'siem.queryRule', - consumer: 'siem', - params: { - author: [], - buildingBlockType: undefined, - falsePositives: [], - description: 'a', - ruleId: '2297be91-894c-4831-830f-b424a0ec84f0', - from: '1900-01-01T00:00:00.000Z', - immutable: false, - license: '', - outputIndex: '', - investigationFields: ['client.address', 'agent.name'], - maxSignals: 100, - meta: undefined, - riskScore: 21, - riskScoreMapping: [], - severity: 'low', - severityMapping: [], - threat: [], - to: 'now', - references: [], - timelineId: undefined, - timelineTitle: undefined, - ruleNameOverride: undefined, - timestampOverride: undefined, - timestampOverrideFallbackDisabled: undefined, - namespace: undefined, - note: undefined, - requiredFields: undefined, - version: 1, - exceptionsList: [], - relatedIntegrations: [], - setup: '', - type: 'query', - language: 'kuery', - index: ['auditbeat-*'], - query: '_id:BhbXBmkBR346wHgn4PeZ or _id:GBbXBmkBR346wHgn5_eR or _id:x10zJ2oE9v5HJNSHhyxi', - filters: [], - savedId: undefined, - responseActions: undefined, - alertSuppression: undefined, - dataViewId: undefined, - ...rewrites?.params, - }, - schedule: { - interval: '5m', - }, - enabled: false, - actions: [], - throttle: null, - ...rewrites, - // cast is due to alerting API expecting rule_type_id - // and our internal schema expecting alertTypeId - } as unknown as InternalRuleCreate); + rewrites?: Partial> +): CreateRuleRequestBody => ({ + ...baseBody, + params: { + ...baseParams, + ...rewrites?.params, + }, + ...rewrites, +}); export const getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray = ( - rewrites?: Partial -): InternalRuleCreate => - ({ - name: 'Test investigation fields empty array', - tags: ['migration'], - rule_type_id: 'siem.queryRule', - consumer: 'siem', - params: { - author: [], - description: 'a', - ruleId: '2297be91-894c-4831-830f-b424a0ec5678', - falsePositives: [], - from: '1900-01-01T00:00:00.000Z', - immutable: false, - license: '', - outputIndex: '', - investigationFields: [], - maxSignals: 100, - riskScore: 21, - riskScoreMapping: [], - severity: 'low', - severityMapping: [], - threat: [], - to: 'now', - references: [], - version: 1, - exceptionsList: [], - type: 'query', - language: 'kuery', - index: ['auditbeat-*'], - query: '_id:BhbXBmkBR346wHgn4PeZ or _id:GBbXBmkBR346wHgn5_eR or _id:x10zJ2oE9v5HJNSHhyxi', - filters: [], - relatedIntegrations: [], - setup: '', - buildingBlockType: undefined, - meta: undefined, - timelineId: undefined, - timelineTitle: undefined, - ruleNameOverride: undefined, - timestampOverride: undefined, - timestampOverrideFallbackDisabled: undefined, - namespace: undefined, - note: undefined, - requiredFields: undefined, - savedId: undefined, - responseActions: undefined, - alertSuppression: undefined, - dataViewId: undefined, - ...rewrites?.params, - }, - schedule: { - interval: '5m', - }, - enabled: false, - actions: [], - throttle: null, - ...rewrites, - // cast is due to alerting API expecting rule_type_id - // and our internal schema expecting alertTypeId - } as unknown as InternalRuleCreate); + rewrites?: Partial> +): CreateRuleRequestBody => ({ + ...baseBody, + name: 'Test investigation fields empty array', + params: { + ...baseParams, + ruleId: 'rule-with-legacy-investigation-fields-empty-array', + investigationFields: [], + ...rewrites?.params, + }, + ...rewrites, +}); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/review_install_prebuilt_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/review_install_prebuilt_rules.ts index 487d2dbe53044..228ca6ff73ed5 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/review_install_prebuilt_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/review_install_prebuilt_rules.ts @@ -10,7 +10,7 @@ import { ReviewRuleInstallationResponseBody } from '@kbn/security-solution-plugi import type SuperTest from 'supertest'; /** - * Returns prebuilt rules that are avaialble to install + * Returns prebuilt rules that are available to install * * @param supertest SuperTest instance * @returns Review Install prebuilt rules response diff --git a/x-pack/test/security_solution_api_integration/tsconfig.json b/x-pack/test/security_solution_api_integration/tsconfig.json index 5594bfd9b113f..b3a8ec25ff97c 100644 --- a/x-pack/test/security_solution_api_integration/tsconfig.json +++ b/x-pack/test/security_solution_api_integration/tsconfig.json @@ -2,7 +2,11 @@ "extends": "../../../tsconfig.base.json", "compilerOptions": { "outDir": "target/types", - "types": ["node", "jest","@kbn/ambient-ftr-types"] + "types": [ + "node", + "jest", + "@kbn/ambient-ftr-types" + ] }, "include": [ "**/*", @@ -13,11 +17,19 @@ "target/**/*" ], "kbn_references": [ - { "path": "../security_solution_endpoint/tsconfig.json" }, + { + "path": "../security_solution_endpoint/tsconfig.json" + }, "@kbn/test-suites-serverless", - { "path": "../../test_serverless/api_integration/**/*" }, - { "path": "../../test_serverless/shared/**/*" }, - { "path": "../../api_integration/services/**/*" }, + { + "path": "../../test_serverless/api_integration/**/*" + }, + { + "path": "../../test_serverless/shared/**/*" + }, + { + "path": "../../api_integration/services/**/*" + }, "@kbn/dev-utils", "@kbn/test", "@kbn/expect", @@ -54,5 +66,6 @@ "@kbn/elastic-assistant-plugin", "@kbn/test-suites-src", "@kbn/openapi-common", + "@kbn/response-ops-rule-params", ] } diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_update_error_handling.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_update_error_handling.cy.ts index a8902a3513392..63012448f1aa7 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_update_error_handling.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_update_error_handling.cy.ts @@ -37,6 +37,10 @@ import { assertRulesPresentInAddPrebuiltRulesTable, assertRuleUpgradeFailureToastShown, assertRulesPresentInRuleUpdatesTable, + interceptInstallationRequestToFailPartially, + assertRuleInstallationSuccessToastShown, + assertRuleUpgradeSuccessToastShown, + interceptUpgradeRequestToFailPartially, } from '../../../../tasks/prebuilt_rules'; import { visitRulesManagementTable } from '../../../../tasks/rules_management'; @@ -113,6 +117,19 @@ describe( assertRuleInstallationFailureToastShown([RULE_1, RULE_2]); assertRulesPresentInAddPrebuiltRulesTable([RULE_1, RULE_2]); }); + + it('installing all available rules at once with some rules succeeding', () => { + clickAddElasticRulesButton(); + interceptInstallationRequestToFailPartially({ + rulesToSucceed: [RULE_1], + rulesToFail: [RULE_2], + }); + cy.get(INSTALL_ALL_RULES_BUTTON).click(); + assertInstallationRequestIsComplete([RULE_1, RULE_2]); + assertRuleInstallationSuccessToastShown([RULE_1]); + assertRuleInstallationFailureToastShown([RULE_2]); + assertRulesPresentInAddPrebuiltRulesTable([RULE_1, RULE_2]); + }); }); describe('Update of prebuilt rules - Should fail gracefully with toast error message when', () => { @@ -203,6 +220,21 @@ describe( assertRuleUpgradeFailureToastShown([OUTDATED_RULE_1, OUTDATED_RULE_2]); assertRulesPresentInRuleUpdatesTable([OUTDATED_RULE_1, OUTDATED_RULE_2]); }); + + it('upgrading all rules with available upgrades at once with some rules succeeding', () => { + interceptUpgradeRequestToFailPartially({ + rulesToSucceed: [OUTDATED_RULE_1], + rulesToFail: [OUTDATED_RULE_2], + }); + + // Navigate to Rule Upgrade table + clickRuleUpdatesTab(); + + cy.get(UPGRADE_ALL_RULES_BUTTON).click(); + assertRuleUpgradeSuccessToastShown([OUTDATED_RULE_1]); + assertRuleUpgradeFailureToastShown([OUTDATED_RULE_2]); + assertRulesPresentInRuleUpdatesTable([OUTDATED_RULE_1, OUTDATED_RULE_2]); + }); }); } ); diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/prebuilt_rules.ts b/x-pack/test/security_solution_cypress/cypress/tasks/prebuilt_rules.ts index 175e1694bcabe..510be189788b2 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/prebuilt_rules.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/prebuilt_rules.ts @@ -64,10 +64,55 @@ export const interceptInstallationRequestToFail = (rules: Array ({ + rule_id: rule['security-rule'].rule_id, + name: rule['security-rule'].name, + })), + }, + ], + }, + delay: 500, // Add delay to give Cypress time to find the loading spinner + }).as('installPrebuiltRules'); +}; + +export const interceptInstallationRequestToFailPartially = ({ + rulesToSucceed, + rulesToFail, +}: { + rulesToSucceed: Array; + rulesToFail: Array; +}) => { + cy.intercept('POST', '/internal/detection_engine/prebuilt_rules/installation/_perform', { + body: { + summary: { + total: rulesToSucceed.length, + succeeded: rulesToSucceed.length, + skipped: 0, + failed: rulesToFail.length, + }, + results: { + created: rulesToSucceed.map((rule) => ({ + rule_id: rule['security-rule'].rule_id, + name: rule['security-rule'].name, + })), + skipped: [], + }, + errors: [ + { + message: 'Something went wrong during installation 🤷‍♀️', + rules: rulesToFail.map((rule) => ({ + rule_id: rule['security-rule'].rule_id, + name: rule['security-rule'].name, + })), + }, + ], }, delay: 500, // Add delay to give Cypress time to find the loading spinner }).as('installPrebuiltRules'); @@ -77,19 +122,59 @@ export const interceptUpgradeRequestToFail = (rules: Array ({ + rule_id: rule['security-rule'].rule_id, + name: rule['security-rule'].name, + })), + }, + ], + }, + delay: 500, // Add delay to give Cypress time to find the loading spinner + }).as('updatePrebuiltRules'); +}; + +export const interceptUpgradeRequestToFailPartially = ({ + rulesToSucceed, + rulesToFail, +}: { + rulesToSucceed: Array; + rulesToFail: Array; +}) => { + cy.intercept('POST', '/internal/detection_engine/prebuilt_rules/upgrade/_perform', { + body: { + summary: { + total: rulesToSucceed.length + rulesToFail.length, + succeeded: rulesToSucceed.length, + skipped: 0, + failed: rulesToFail.length, + }, + results: { + updated: rulesToSucceed.map((rule) => ({ + rule_id: rule['security-rule'].rule_id, + name: rule['security-rule'].name, + })), + skipped: [], }, + errors: [ + { + message: 'Something went wrong during upgrade 🤷‍♀️', + rules: rulesToFail.map((rule) => ({ + rule_id: rule['security-rule'].rule_id, + name: rule['security-rule'].name, + })), + }, + ], }, delay: 500, // Add delay to give Cypress time to find the loading spinner }).as('updatePrebuiltRules'); @@ -101,7 +186,7 @@ export const assertRuleInstallationSuccessToastShown = ( const rulesString = rules.length > 1 ? 'rules' : 'rule'; cy.get(SUCCESS_TOASTER) .should('be.visible') - .should('have.text', `${rules.length} ${rulesString} installed successfully.`); + .should('have.text', `${rules.length} ${rulesString} installed successfully`); }; export const assertRuleInstallationFailureToastShown = ( @@ -110,21 +195,21 @@ export const assertRuleInstallationFailureToastShown = ( const rulesString = rules.length > 1 ? 'rules' : 'rule'; cy.get(TOASTER) .should('be.visible') - .should('have.text', `${rules.length} ${rulesString} failed to install.`); + .should('have.text', `${rules.length} ${rulesString} failed to install`); }; export const assertRuleUpgradeSuccessToastShown = (rules: Array) => { const rulesString = rules.length > 1 ? 'rules' : 'rule'; cy.get(SUCCESS_TOASTER) .should('be.visible') - .should('contain', `${rules.length} ${rulesString} updated successfully.`); + .should('contain', `${rules.length} ${rulesString} updated successfully`); }; export const assertRuleUpgradeFailureToastShown = (rules: Array) => { const rulesString = rules.length > 1 ? 'rules' : 'rule'; cy.get(TOASTER) .should('be.visible') - .should('have.text', `${rules.length} ${rulesString} failed to update.`); + .should('have.text', `${rules.length} ${rulesString} failed to update`); }; export const assertRulesPresentInInstalledRulesTable = (