From 5cf9522ad9d3b55c23aa0440b8e24b9dc7c73830 Mon Sep 17 00:00:00 2001 From: Dmitrii Shevchenko Date: Thu, 29 Aug 2024 09:56:16 +0200 Subject: [PATCH] [Security Solution] Recalculate isCustomized when bulk editing rules (#190041) **Resolves: https://github.com/elastic/kibana/issues/187706** ## Summary Added the `isCustomized` field recalculation after a bulk edit operation on rules as part of the [rules customization epic](https://github.com/elastic/security-team/issues/1974). **Background** The `isCustomized` field is a rule parameter indicating if a prebuilt Elastic rule has been modified by a user. This field is extensively used in the prebuilt rule upgrade workflow. It's essential to ensure any rule modification operation recalculates this field to keep its value in sync with the rule content. Most of the rule CRUD operations were already covered in a previous PR: [Calculate and save ruleSource.isCustomized in API endpoint handlers](https://github.com/elastic/kibana/issues/180145). This PR addresses the remaining bulk rule modification operations performed using the `rulesClient.bulkEdit` method. **`rulesClient.bulkEdit` changes** The `isCustomized` calculation is based on the entire rule object (i.e., rule params and attributes) and should be performed after all bulk operations have been applied to the rule - after `operations` and `paramsModifier`. To support this, I changed the `paramsModifier` to accept entire rule object: ```diff export type ParamsModifier = ( - params: Params + rule: Rule ) => Promise>; ``` **Security Solution Bulk Endpoint changes** The `/api/detection_engine/rules/_bulk_action` endpoint now handles bulk edit actions a bit differently. Previously, most of the bulk action was delegated to the rules client. Now, we need to do some preparatory work: 1. Fetch the affected rules in memory first, regardless of whether we received a query or rule IDs as input (previously delegated to Alerting). 2. Identify all prebuilt rules among the fetched rules. 3. Fetch base versions of the prebuilt rules. 4. Provide base versions to `ruleModifier` for the `isCustomized` calculation. These changes add a few extra roundtrips to Elasticsearch and make the bulk edit endpoint less efficient than before. However, this seems justified given the added functionality of the customization epic. In the future, we might consider optimizing to reduce the number of database requests. Ideally, for Security Solution use cases, we would need a more generic method than `bulkEdit`, such as `bulkUpdate`, allowing us to implement any required rule update logic fully on the solution side. --- .../ftr_security_serverless_configs.yml | 1 + .buildkite/ftr_security_stateful_configs.yml | 1 + .../methods/bulk_edit/bulk_edit_rules.test.ts | 6 +- .../rule/methods/bulk_edit/bulk_edit_rules.ts | 5 +- .../types/bulk_edit_rules_options.ts | 5 +- .../fetch_rules_by_query_or_ids.ts | 9 +- .../api/rules/bulk_actions/route.ts | 85 ++++---- .../logic/bulk_actions/bulk_edit_rules.ts | 69 ++++-- .../logic/bulk_actions/validations.ts | 18 +- .../configs/ess.config.ts | 34 +++ .../configs/serverless.config.ts | 21 ++ .../trial_license_complete_tier/index.ts | 14 ++ .../is_customized_calculation.ts | 200 ++++++++++++++++++ .../perform_bulk_action.ts | 13 +- 14 files changed, 404 insertions(+), 77 deletions(-) create mode 100644 x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/trial_license_complete_tier/configs/ess.config.ts create mode 100644 x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/trial_license_complete_tier/configs/serverless.config.ts create mode 100644 x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/trial_license_complete_tier/index.ts create mode 100644 x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/trial_license_complete_tier/is_customized_calculation.ts diff --git a/.buildkite/ftr_security_serverless_configs.yml b/.buildkite/ftr_security_serverless_configs.yml index d6a92fbb5f446..cf1b374e68c60 100644 --- a/.buildkite/ftr_security_serverless_configs.yml +++ b/.buildkite/ftr_security_serverless_configs.yml @@ -65,6 +65,7 @@ enabled: - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/large_prebuilt_rules_package/trial_license_complete_tier/configs/serverless.config.ts - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/configs/serverless.config.ts - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/update_prebuilt_rules_package/trial_license_complete_tier/configs/serverless.config.ts + - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/trial_license_complete_tier/configs/serverless.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 148d78583a613..a72f5287d189b 100644 --- a/.buildkite/ftr_security_stateful_configs.yml +++ b/.buildkite/ftr_security_stateful_configs.yml @@ -47,6 +47,7 @@ enabled: - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/large_prebuilt_rules_package/trial_license_complete_tier/configs/ess.config.ts - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/configs/ess.config.ts - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/update_prebuilt_rules_package/trial_license_complete_tier/configs/ess.config.ts + - x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/trial_license_complete_tier/configs/ess.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 diff --git a/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.test.ts b/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.test.ts index f43ffc6096dcf..c7c795359aaee 100644 --- a/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.test.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.test.ts @@ -3214,7 +3214,8 @@ describe('bulkEdit()', () => { value: ['test-1'], }, ], - paramsModifier: async (params) => { + paramsModifier: async (rule) => { + const params = rule.params; params.index = ['test-index-*']; return { modifiedParams: params, isParamsUpdateSkipped: false, skipReasons: [] }; @@ -3431,7 +3432,8 @@ describe('bulkEdit()', () => { value: ['test-1'], }, ], - paramsModifier: async (params) => { + paramsModifier: async (rule) => { + const params = rule.params; params.index = ['test-index-*']; return { modifiedParams: params, isParamsUpdateSkipped: false, skipReasons: [] }; diff --git a/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.ts b/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.ts index 8bd10fe722c1d..81dd189116949 100644 --- a/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.ts @@ -16,7 +16,7 @@ import { SavedObjectsUpdateResponse, } from '@kbn/core/server'; import { validateAndAuthorizeSystemActions } from '../../../../lib/validate_authorize_system_actions'; -import { RuleAction, RuleSystemAction } from '../../../../../common'; +import { Rule, RuleAction, RuleSystemAction } from '../../../../../common'; import { RULE_SAVED_OBJECT_TYPE } from '../../../../saved_objects'; import { BulkActionSkipResult } from '../../../../../common/bulk_edit'; import { RuleTypeRegistry } from '../../../../types'; @@ -505,7 +505,8 @@ async function updateRuleAttributesAndParamsInMemory( validateScheduleInterval(context, updatedRule.schedule.interval, ruleType.id, rule.id); const { modifiedParams: ruleParams, isParamsUpdateSkipped } = paramsModifier - ? await paramsModifier(updatedRule.params) + ? // TODO (http-versioning): Remove the cast when all rule types are fixed + await paramsModifier(updatedRule as Rule) : { modifiedParams: updatedRule.params, isParamsUpdateSkipped: true, diff --git a/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/types/bulk_edit_rules_options.ts b/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/types/bulk_edit_rules_options.ts index c099cde044363..7c30f6583865e 100644 --- a/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/types/bulk_edit_rules_options.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/types/bulk_edit_rules_options.ts @@ -12,14 +12,15 @@ import { bulkEditOperationsSchema, bulkEditOperationSchema, } from '../schemas'; -import { RuleParams, RuleDomain, Rule } from '../../../types'; +import { RuleParams, RuleDomain } from '../../../types'; +import { Rule } from '../../../../../../common'; export type BulkEditRuleSnoozeSchedule = TypeOf; export type BulkEditOperation = TypeOf; export type BulkEditOperations = TypeOf; export type ParamsModifier = ( - params: Params + rule: Rule ) => Promise>; interface ParamsModifierResult { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/fetch_rules_by_query_or_ids.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/fetch_rules_by_query_or_ids.ts index d59ba5676b607..d74bcc5e7f450 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/fetch_rules_by_query_or_ids.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/fetch_rules_by_query_or_ids.ts @@ -13,18 +13,19 @@ import { initPromisePool } from '../../../../../../utils/promise_pool'; import type { RuleAlertType } from '../../../../rule_schema'; import { readRules } from '../../../logic/detection_rules_client/read_rules'; import { findRules } from '../../../logic/search/find_rules'; -import { MAX_RULES_TO_PROCESS_TOTAL } from './route'; export const fetchRulesByQueryOrIds = async ({ query, ids, rulesClient, abortSignal, + maxRules, }: { query: string | undefined; ids: string[] | undefined; rulesClient: RulesClient; abortSignal: AbortSignal; + maxRules: number; }): Promise> => { if (ids) { return initPromisePool({ @@ -43,7 +44,7 @@ export const fetchRulesByQueryOrIds = async ({ const { data, total } = await findRules({ rulesClient, - perPage: MAX_RULES_TO_PROCESS_TOTAL, + perPage: maxRules, filter: query, page: undefined, sortField: undefined, @@ -51,9 +52,9 @@ export const fetchRulesByQueryOrIds = async ({ fields: undefined, }); - if (total > MAX_RULES_TO_PROCESS_TOTAL) { + if (total > maxRules) { throw new BadRequestError( - `More than ${MAX_RULES_TO_PROCESS_TOTAL} rules matched the filter query. Try to narrow it down.` + `More than ${maxRules} rules matched the filter query. Try to narrow it down.` ); } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.ts index 7ac96312b793d..4d31bd457a3e9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.ts @@ -9,6 +9,7 @@ import type { IKibanaResponse, Logger } from '@kbn/core/server'; import { AbortError } from '@kbn/kibana-utils-plugin/common'; import { transformError } from '@kbn/securitysolution-es-utils'; import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; +import type { BulkActionSkipResult } from '@kbn/alerting-plugin/common'; import type { ConfigType } from '../../../../../../config'; import type { PerformRulesBulkActionResponse } from '../../../../../../../common/api/detection_engine/rule_management'; import { @@ -42,8 +43,12 @@ import { buildBulkResponse } from './bulk_actions_response'; import { bulkEnableDisableRules } from './bulk_enable_disable_rules'; import { fetchRulesByQueryOrIds } from './fetch_rules_by_query_or_ids'; import { bulkScheduleBackfill } from './bulk_schedule_rule_run'; +import { createPrebuiltRuleAssetsClient } from '../../../../prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client'; -export const MAX_RULES_TO_PROCESS_TOTAL = 10000; +const MAX_RULES_TO_PROCESS_TOTAL = 10000; +// Set a lower limit for bulk edit as the rules client might fail with a "Query +// contains too many nested clauses" error +const MAX_RULES_TO_BULK_EDIT = 2000; const MAX_ROUTE_CONCURRENCY = 5; export const performBulkActionRoute = ( @@ -126,6 +131,7 @@ export const performBulkActionRoute = ( const savedObjectsClient = ctx.core.savedObjects.client; const actionsClient = ctx.actions.getActionsClient(); const detectionRulesClient = ctx.securitySolution.getDetectionRulesClient(); + const prebuiltRuleAssetClient = createPrebuiltRuleAssetsClient(savedObjectsClient); const { getExporter, getClient } = ctx.core.savedObjects; const client = getClient({ includedHiddenTypes: ['action'] }); @@ -141,31 +147,15 @@ export const performBulkActionRoute = ( const query = body.query !== '' ? body.query : undefined; - // handling this action before switch statement as bulkEditRules fetch rules within - // rulesClient method, hence there is no need to use fetchRulesByQueryOrIds utility - if (body.action === BulkActionTypeEnum.edit && !isDryRun) { - const { rules, errors, skipped } = await bulkEditRules({ - actionsClient, - rulesClient, - filter: query, - ids: body.ids, - actions: body.edit, - mlAuthz, - experimentalFeatures: config.experimentalFeatures, - }); - - return buildBulkResponse(response, { - updated: rules, - skipped, - errors, - }); - } - const fetchRulesOutcome = await fetchRulesByQueryOrIds({ rulesClient, query, ids: body.ids, abortSignal: abortController.signal, + maxRules: + body.action === BulkActionTypeEnum.edit + ? MAX_RULES_TO_BULK_EDIT + : MAX_RULES_TO_PROCESS_TOTAL, }); const rules = fetchRulesOutcome.results.map(({ result }) => result); @@ -173,6 +163,7 @@ export const performBulkActionRoute = ( let updated: RuleAlertType[] = []; let created: RuleAlertType[] = []; let deleted: RuleAlertType[] = []; + let skipped: BulkActionSkipResult[] = []; switch (body.action) { case BulkActionTypeEnum.enable: { @@ -307,25 +298,40 @@ export const performBulkActionRoute = ( // will be processed only when isDryRun === true // during dry run only validation is getting performed and rule is not saved in ES case BulkActionTypeEnum.edit: { - const bulkActionOutcome = await initPromisePool({ - concurrency: MAX_RULES_TO_UPDATE_IN_PARALLEL, - items: rules, - executor: async (rule) => { - await dryRunValidateBulkEditRule({ - mlAuthz, - rule, - edit: body.edit, - experimentalFeatures: config.experimentalFeatures, - }); + if (isDryRun) { + const bulkActionOutcome = await initPromisePool({ + concurrency: MAX_RULES_TO_UPDATE_IN_PARALLEL, + items: rules, + executor: async (rule) => { + await dryRunValidateBulkEditRule({ + mlAuthz, + rule, + edit: body.edit, + experimentalFeatures: config.experimentalFeatures, + }); - return rule; - }, - abortSignal: abortController.signal, - }); - errors.push(...bulkActionOutcome.errors); - updated = bulkActionOutcome.results - .map(({ result }) => result) - .filter((rule): rule is RuleAlertType => rule !== null); + return rule; + }, + abortSignal: abortController.signal, + }); + errors.push(...bulkActionOutcome.errors); + updated = bulkActionOutcome.results + .map(({ result }) => result) + .filter((rule): rule is RuleAlertType => rule !== null); + } else { + const bulkEditResult = await bulkEditRules({ + actionsClient, + rulesClient, + prebuiltRuleAssetClient, + rules, + actions: body.edit, + mlAuthz, + experimentalFeatures: config.experimentalFeatures, + }); + updated = bulkEditResult.rules; + skipped = bulkEditResult.skipped; + errors.push(...bulkEditResult.errors); + } break; } @@ -352,6 +358,7 @@ export const performBulkActionRoute = ( updated, deleted, created, + skipped, errors, isDryRun, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/bulk_edit_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/bulk_edit_rules.ts index ab3a9f486d81b..bee44a3600f97 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/bulk_edit_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/bulk_edit_rules.ts @@ -5,28 +5,30 @@ * 2.0. */ -import type { RulesClient } from '@kbn/alerting-plugin/server'; import type { ActionsClient } from '@kbn/actions-plugin/server'; +import type { RulesClient } from '@kbn/alerting-plugin/server'; import type { ExperimentalFeatures } from '../../../../../../common'; import type { BulkActionEditPayload } from '../../../../../../common/api/detection_engine/rule_management'; import type { MlAuthz } from '../../../../machine_learning/authz'; -import { enrichFilterWithRuleTypeMapping } from '../search/enrich_filter_with_rule_type_mappings'; -import type { RuleAlertType } from '../../../rule_schema'; +import type { RuleAlertType, RuleParams } from '../../../rule_schema'; +import type { IPrebuiltRuleAssetsClient } from '../../../prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client'; +import { convertAlertingRuleToRuleResponse } from '../detection_rules_client/converters/convert_alerting_rule_to_rule_response'; +import { calculateIsCustomized } from '../detection_rules_client/mergers/rule_source/calculate_is_customized'; +import { bulkEditActionToRulesClientOperation } from './action_to_rules_client_operation'; import { ruleParamsModifier } from './rule_params_modifier'; import { splitBulkEditActions } from './split_bulk_edit_actions'; import { validateBulkEditRule } from './validations'; -import { bulkEditActionToRulesClientOperation } from './action_to_rules_client_operation'; export interface BulkEditRulesArguments { actionsClient: ActionsClient; rulesClient: RulesClient; + prebuiltRuleAssetClient: IPrebuiltRuleAssetsClient; actions: BulkActionEditPayload[]; - filter?: string; - ids?: string[]; + rules: RuleAlertType[]; mlAuthz: MlAuthz; experimentalFeatures: ExperimentalFeatures; } @@ -41,27 +43,70 @@ export interface BulkEditRulesArguments { export const bulkEditRules = async ({ actionsClient, rulesClient, - ids, + prebuiltRuleAssetClient, + rules, actions, - filter, mlAuthz, experimentalFeatures, }: BulkEditRulesArguments) => { + // Split operations const { attributesActions, paramsActions } = splitBulkEditActions(actions); const operations = attributesActions .map((attribute) => bulkEditActionToRulesClientOperation(actionsClient, attribute)) .flat(); - const result = await rulesClient.bulkEdit({ - ...(ids ? { ids } : { filter: enrichFilterWithRuleTypeMapping(filter) }), + + // Check if there are any prebuilt rules and fetch their base versions + const prebuiltRules = rules.filter((rule) => rule.params.immutable); + const baseVersions = await prebuiltRuleAssetClient.fetchAssetsByVersion( + prebuiltRules.map((rule) => ({ + rule_id: rule.params.ruleId, + version: rule.params.version, + })) + ); + const baseVersionsMap = new Map( + baseVersions.map((baseVersion) => [baseVersion.rule_id, baseVersion]) + ); + + const result = await rulesClient.bulkEdit({ + ids: rules.map((rule) => rule.id), operations, - paramsModifier: async (ruleParams: RuleAlertType['params']) => { + paramsModifier: async (rule) => { + const ruleParams = rule.params; + await validateBulkEditRule({ mlAuthz, ruleType: ruleParams.type, edit: actions, immutable: ruleParams.immutable, + experimentalFeatures, }); - return ruleParamsModifier(ruleParams, paramsActions, experimentalFeatures); + const { modifiedParams, isParamsUpdateSkipped } = ruleParamsModifier( + ruleParams, + paramsActions, + experimentalFeatures + ); + + // Update rule source + const updatedRule = { + ...rule, + params: modifiedParams, + }; + const ruleResponse = convertAlertingRuleToRuleResponse(updatedRule); + const ruleSource = + ruleResponse.immutable === true + ? { + type: 'external' as const, + isCustomized: calculateIsCustomized( + baseVersionsMap.get(ruleResponse.rule_id), + ruleResponse + ), + } + : { + type: 'internal' as const, + }; + modifiedParams.ruleSource = ruleSource; + + return { modifiedParams, isParamsUpdateSkipped }; }, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/validations.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/validations.ts index 591bcc0a18ec5..d89c78be6b846 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/validations.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/validations.ts @@ -32,6 +32,7 @@ interface BulkEditBulkActionsValidationArgs { mlAuthz: MlAuthz; edit: BulkActionEditPayload[]; immutable: boolean; + experimentalFeatures: ExperimentalFeatures; } interface DryRunBulkEditBulkActionsValidationArgs { @@ -110,15 +111,18 @@ export const validateBulkEditRule = async ({ mlAuthz, edit, immutable, + experimentalFeatures, }: BulkEditBulkActionsValidationArgs) => { await throwMlAuthError(mlAuthz, ruleType); - // if rule can't be edited error will be thrown - const canRuleBeEdited = !immutable || istEditApplicableToImmutableRule(edit); - await throwDryRunError( - () => invariant(canRuleBeEdited, "Elastic rule can't be edited"), - BulkActionsDryRunErrCode.IMMUTABLE - ); + if (!experimentalFeatures.prebuiltRulesCustomizationEnabled) { + // if rule can't be edited error will be thrown + const canRuleBeEdited = !immutable || istEditApplicableToImmutableRule(edit); + await throwDryRunError( + () => invariant(canRuleBeEdited, "Elastic rule can't be edited"), + BulkActionsDryRunErrCode.IMMUTABLE + ); + } }; /** @@ -140,12 +144,14 @@ export const dryRunValidateBulkEditRule = async ({ rule, edit, mlAuthz, + experimentalFeatures, }: DryRunBulkEditBulkActionsValidationArgs) => { await validateBulkEditRule({ ruleType: rule.params.type, mlAuthz, edit, immutable: rule.params.immutable, + experimentalFeatures, }); // if rule is machine_learning, index pattern action can't be applied to it diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/trial_license_complete_tier/configs/ess.config.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/trial_license_complete_tier/configs/ess.config.ts new file mode 100644 index 0000000000000..eee14323c9b98 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/trial_license_complete_tier/configs/ess.config.ts @@ -0,0 +1,34 @@ +/* + * 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.trial') + ); + + const testConfig = { + ...functionalConfig.getAll(), + testFiles: [require.resolve('..')], + junit: { + reportName: + 'Rules Management - Prebuilt Rule Customization Integration Tests - ESS Env - Trial License', + }, + }; + testConfig.kbnTestServer.serverArgs = testConfig.kbnTestServer.serverArgs.map((arg: string) => { + // Override the default value of `--xpack.securitySolution.enableExperimental` to enable the prebuilt rules customization feature + if (arg.includes('--xpack.securitySolution.enableExperimental')) { + return `--xpack.securitySolution.enableExperimental=${JSON.stringify([ + 'prebuiltRulesCustomizationEnabled', + ])}`; + } + return arg; + }); + + return testConfig; +} diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/trial_license_complete_tier/configs/serverless.config.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/trial_license_complete_tier/configs/serverless.config.ts new file mode 100644 index 0000000000000..79a23c85d2279 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/trial_license_complete_tier/configs/serverless.config.ts @@ -0,0 +1,21 @@ +/* + * 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'; + +export default createTestConfig({ + testFiles: [require.resolve('..')], + junit: { + reportName: + 'Rules Management - Prebuilt Rule Customization Integration Tests - Serverless Env - Complete Tier', + }, + kbnTestServerArgs: [ + `--xpack.securitySolution.enableExperimental=${JSON.stringify([ + 'prebuiltRulesCustomizationEnabled', + ])}`, + ], +}); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/trial_license_complete_tier/index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/trial_license_complete_tier/index.ts new file mode 100644 index 0000000000000..76a461d438463 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/trial_license_complete_tier/index.ts @@ -0,0 +1,14 @@ +/* + * 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 - Update Prebuilt Rules Package', function () { + loadTestFile(require.resolve('./is_customized_calculation')); + }); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/trial_license_complete_tier/is_customized_calculation.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/trial_license_complete_tier/is_customized_calculation.ts new file mode 100644 index 0000000000000..72f9062f66ca1 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/trial_license_complete_tier/is_customized_calculation.ts @@ -0,0 +1,200 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { + BulkActionEditTypeEnum, + BulkActionTypeEnum, +} from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.gen'; +import expect from 'expect'; +import { deleteAllRules } from '../../../../../../../common/utils/security_solution'; +import { FtrProviderContext } from '../../../../../../ftr_provider_context'; +import { + createPrebuiltRuleAssetSavedObjects, + createRuleAssetSavedObject, + deleteAllPrebuiltRuleAssets, + installPrebuiltRules, +} from '../../../../utils'; + +export default ({ getService }: FtrProviderContext): void => { + const es = getService('es'); + const supertest = getService('supertest'); + const securitySolutionApi = getService('securitySolutionApi'); + const log = getService('log'); + + const ruleAsset = createRuleAssetSavedObject({ + rule_id: '000047bb-b27a-47ec-8b62-ef1a5d2c9e19', + tags: ['test-tag'], + }); + + describe('@ess @serverless @skipInServerlessMKI is_customized calculation', () => { + beforeEach(async () => { + await deleteAllRules(supertest, log); + await deleteAllPrebuiltRuleAssets(es, log); + }); + + describe('prebuilt rules', () => { + it('should set is_customized to true on bulk rule modification', async () => { + await createPrebuiltRuleAssetSavedObjects(es, [ruleAsset]); + await installPrebuiltRules(es, supertest); + + const { body: findResult } = await securitySolutionApi + .findRules({ + query: { + per_page: 1, + filter: `alert.attributes.params.immutable: true`, + }, + }) + .expect(200); + const prebuiltRule = findResult.data[0]; + expect(prebuiltRule).not.toBeNull(); + expect(prebuiltRule.rule_source.is_customized).toEqual(false); + + const { body: bulkResult } = await securitySolutionApi + .performRulesBulkAction({ + query: {}, + body: { + ids: [prebuiltRule.id], + action: BulkActionTypeEnum.edit, + [BulkActionTypeEnum.edit]: [ + { + type: BulkActionEditTypeEnum.add_tags, + value: ['new-tag'], + }, + ], + }, + }) + .expect(200); + + expect(bulkResult.attributes.summary).toEqual({ + failed: 0, + skipped: 0, + succeeded: 1, + total: 1, + }); + expect(bulkResult.attributes.results.updated[0].rule_source.is_customized).toEqual(true); + }); + + it('should leave is_customized intact if the change has been skipped', async () => { + await createPrebuiltRuleAssetSavedObjects(es, [ruleAsset]); + await installPrebuiltRules(es, supertest); + + const { body: findResult } = await securitySolutionApi + .findRules({ + query: { + per_page: 1, + filter: `alert.attributes.params.immutable: true`, + }, + }) + .expect(200); + const prebuiltRule = findResult.data[0]; + expect(prebuiltRule).not.toBeNull(); + expect(prebuiltRule.rule_source.is_customized).toEqual(false); + + const { body: bulkResult } = await securitySolutionApi + .performRulesBulkAction({ + query: {}, + body: { + ids: [prebuiltRule.id], + action: BulkActionTypeEnum.edit, + [BulkActionTypeEnum.edit]: [ + { + type: BulkActionEditTypeEnum.add_tags, + // This tag is already present on the rule, so the change will be skipped + value: [prebuiltRule.tags[0]], + }, + ], + }, + }) + .expect(200); + + expect(bulkResult.attributes.summary).toEqual({ + failed: 0, + skipped: 1, + succeeded: 0, + total: 1, + }); + + // Check that the rule has not been customized + const { body: findResultAfter } = await securitySolutionApi + .findRules({ + query: { + per_page: 1, + filter: `alert.attributes.params.immutable: true`, + }, + }) + .expect(200); + expect(findResultAfter.data[0].rule_source.is_customized).toEqual(false); + }); + + it('should set is_customized to false if the change has been reverted', async () => { + await createPrebuiltRuleAssetSavedObjects(es, [ruleAsset]); + await installPrebuiltRules(es, supertest); + + const { body: findResult } = await securitySolutionApi + .findRules({ + query: { + per_page: 1, + filter: `alert.attributes.params.immutable: true`, + }, + }) + .expect(200); + const prebuiltRule = findResult.data[0]; + expect(prebuiltRule).not.toBeNull(); + expect(prebuiltRule.rule_source.is_customized).toEqual(false); + + // Add a tag to the rule + const { body: bulkResult } = await securitySolutionApi + .performRulesBulkAction({ + query: {}, + body: { + ids: [prebuiltRule.id], + action: BulkActionTypeEnum.edit, + [BulkActionTypeEnum.edit]: [ + { + type: BulkActionEditTypeEnum.add_tags, + value: ['new-tag'], + }, + ], + }, + }) + .expect(200); + + expect(bulkResult.attributes.summary).toEqual({ + failed: 0, + skipped: 0, + succeeded: 1, + total: 1, + }); + + // Remove the added tag + const { body: revertResult } = await securitySolutionApi + .performRulesBulkAction({ + query: {}, + body: { + ids: [prebuiltRule.id], + action: BulkActionTypeEnum.edit, + [BulkActionTypeEnum.edit]: [ + { + type: BulkActionEditTypeEnum.delete_tags, + value: ['new-tag'], + }, + ], + }, + }) + .expect(200); + + expect(revertResult.attributes.summary).toEqual({ + failed: 0, + skipped: 0, + succeeded: 1, + total: 1, + }); + + expect(revertResult.attributes.results.updated[0].rule_source.is_customized).toEqual(false); + }); + }); + }); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action.ts index 43afebc55decd..c6c85751593a4 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action.ts @@ -34,12 +34,7 @@ import { removeServerGeneratedProperties, updateUsername, } from '../../../utils'; -import { - createRule, - createAlertsIndex, - deleteAllRules, - deleteAllAlerts, -} from '../../../../../../common/utils/security_solution'; +import { createRule, deleteAllRules } from '../../../../../../common/utils/security_solution'; import { deleteAllExceptions } from '../../../../lists_and_exception_lists/utils'; import { FtrProviderContext } from '../../../../../ftr_provider_context'; @@ -90,14 +85,12 @@ export default ({ getService }: FtrProviderContext): void => { describe('@ess @serverless @skipInServerless perform_bulk_action', () => { beforeEach(async () => { - await createAlertsIndex(supertest, log); + await deleteAllRules(supertest, log); await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts'); }); afterEach(async () => { - await deleteAllAlerts(supertest, log, es); - await deleteAllRules(supertest, log); - await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts'); + await esArchiver.unload('x-pack/test/functional/es_archives/auditbeat/hosts'); }); it('should export rules', async () => {