From 045aafcfea10fb2d1099eb2c7b7600bf25480ede Mon Sep 17 00:00:00 2001 From: Dmitrii Shevchenko Date: Tue, 9 Jul 2024 13:40:50 +0200 Subject: [PATCH] [Security Solution] Implement isCustomized calculation (#186988) **Resolves: https://github.com/elastic/kibana/issues/180145** **Resolves: https://github.com/elastic/kibana/issues/184364** > [!NOTE] > This PR doesn't include `isCustomized` recalculation when bulk editing rules. This should be addressed separately as it might require first changing the `RulesClient.bulkEdit` method signature. ## Summary This PR implements the calculation of `ruleSource.isCustomized` inside the `DetectionRulesClient`. The recalculation of the `isCustomized` field is performed on every rule patch and update operation, including rule upgrades. See the ticket for more information: https://github.com/elastic/kibana/issues/180145 and `detection_rules_client/mergers/rule_source/calculate_is_customized.ts` for implementation details. The `isCustomized` calculation is based on the `calculateRuleFieldsDiff` method used inside the prebuilt rules domain for calculating changed fields during rule upgrades. This ensures that the diff calculation logic is unified and reused to avoid any discrepancies in different paths of rule management. The recalculation and saving of the field is done in the following endpoints: - **Update Rule** - `PUT /rules` - **Patch Rule** - `PATCH /rules` - **Bulk Update Rules** - `PUT /rules/_bulk_update` - **Bulk Patch Rules** - `PATCH /rules/_bulk_update` - **Import Rules** - `POST /rules/_import` - **Perform Rule Upgrade** - `POST /prebuilt_rules/upgrade/_perform` This PR also partially addresses refactoring mentioned here: https://github.com/elastic/kibana/issues/184364. Namely: - Splits the rule converters into smaller single-responsibility functions. - Separate methods to convert RuleResponse to AlertingRule and back - Separate methods to apply rule patches, updates, or set defaults - Separate case converters - Migrates methods to work with RuleResponse instead of alerting type wherever possible. - Adds new methods for fetching rules by id or rule id and deprecates the `readRules`. Although new methods are not exposed yet in the public client interface, this is something that needs to be addressed separately. --- .../rule_schema/rule_response_schema.mock.ts | 28 +- .../review_rule_installation_route.ts | 2 +- .../review_rule_upgrade_route.ts | 2 +- .../__mocks__/prebuilt_rule_assets_client.ts | 14 + .../prebuilt_rule_objects_client.ts | 2 +- .../api/create_rule_exceptions/route.ts | 2 +- .../bulk_actions/bulk_actions_response.ts | 2 +- .../api/rules/bulk_patch_rules/route.ts | 2 +- .../api/rules/patch_rule/route.ts | 2 +- .../detection_engine/rule_management/index.ts | 9 +- .../logic/actions/duplicate_rule.ts | 5 +- .../common_params_camel_to_snake.test.ts | 29 + .../common_params_camel_to_snake.ts | 47 + .../convert_alerting_rule_to_rule_response.ts | 26 + ...rt_prebuilt_rule_asset_to_rule_response.ts | 39 + .../convert_rule_response_to_alerting_rule.ts | 210 +++++ .../internal_rule_to_api_response.ts | 61 ++ .../type_specific_camel_to_snake.test.ts | 67 ++ .../type_specific_camel_to_snake.ts | 127 +++ ...on_rules_client.create_custom_rule.test.ts | 4 +- ..._rules_client.create_prebuilt_rule.test.ts | 4 +- ...detection_rules_client.delete_rule.test.ts | 4 +- ...detection_rules_client.import_rule.test.ts | 38 +- .../detection_rules_client.patch_rule.test.ts | 185 ++-- .../detection_rules_client.ts | 143 +-- ...detection_rules_client.update_rule.test.ts | 140 +-- ...rules_client.upgrade_prebuilt_rule.test.ts | 69 +- .../detection_rules_client_interface.ts | 2 +- .../mergers/apply_rule_defaults.ts | 185 ++++ .../mergers/apply_rule_patch.test.ts | 456 +++++++++ .../mergers/apply_rule_patch.ts | 334 +++++++ .../mergers/apply_rule_update.ts | 52 ++ .../rule_source/calculate_is_customized.ts | 33 + .../rule_source/calculate_rule_source.test.ts | 112 +++ .../rule_source/calculate_rule_source.ts | 47 + .../methods/__mocks__/get_rule_by_rule_id.ts | 13 + .../methods/create_custom_rule.ts | 44 - .../methods/create_prebuilt_rule.ts | 49 - .../methods/create_rule.ts | 55 ++ .../methods/delete_rule.ts | 10 +- .../methods/get_rule_by_id.ts | 34 + .../methods/get_rule_by_id_or_rule_id.ts | 36 + .../methods/get_rule_by_rule_id.ts | 38 + .../methods/import_rule.ts | 83 +- .../methods/patch_rule.ts | 77 +- .../methods/update_rule.ts | 73 +- .../methods/upgrade_prebuilt_rule.ts | 102 +- .../detection_rules_client/read_rules.ts | 2 + .../logic/detection_rules_client/utils.ts | 10 +- .../export/get_export_by_object_ids.test.ts | 2 +- .../logic/export/get_export_by_object_ids.ts | 2 +- .../normalization/rule_converters.test.ts | 491 ---------- .../normalization/rule_converters.ts | 869 ------------------ .../rule_management/utils/utils.test.ts | 2 +- .../rule_management/utils/utils.ts | 26 +- .../rule_management/utils/validate.ts | 2 +- .../rule_preview/api/preview_rules/route.ts | 8 +- .../rule_schema/model/rule_schemas.ts | 39 +- .../server/request_context_factory.ts | 9 +- .../create_rule_exceptions_ess.ts | 11 - .../patch_rules_bulk.ts | 10 +- .../patch_rules_ess.ts | 11 +- 62 files changed, 2552 insertions(+), 2040 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_assets/__mocks__/prebuilt_rule_assets_client.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/common_params_camel_to_snake.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/common_params_camel_to_snake.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/convert_alerting_rule_to_rule_response.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/convert_prebuilt_rule_asset_to_rule_response.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/convert_rule_response_to_alerting_rule.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/internal_rule_to_api_response.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/type_specific_camel_to_snake.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/type_specific_camel_to_snake.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_defaults.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_patch.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_patch.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_update.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_is_customized.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_rule_source.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_rule_source.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/__mocks__/get_rule_by_rule_id.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/create_custom_rule.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/create_prebuilt_rule.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/create_rule.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/get_rule_by_id.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/get_rule_by_id_or_rule_id.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/get_rule_by_rule_id.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_converters.test.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_converters.ts diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_response_schema.mock.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_response_schema.mock.ts index 1a39d65c1c22..c60543657699 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_response_schema.mock.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_response_schema.mock.ts @@ -6,16 +6,18 @@ */ import { DEFAULT_INDICATOR_SOURCE_PATH } from '../../../../constants'; +import { getListArrayMock } from '../../../../detection_engine/schemas/types/lists.mock'; import type { EqlRule, EsqlRule, MachineLearningRule, + NewTermsRule, QueryRule, SavedQueryRule, SharedResponseProps, ThreatMatchRule, + ThresholdRule, } from './rule_schemas.gen'; -import { getListArrayMock } from '../../../../detection_engine/schemas/types/lists.mock'; export const ANCHOR_DATE = '2020-02-20T03:57:54.037Z'; @@ -238,3 +240,27 @@ export const getRulesEqlSchemaMock = (anchorDate: string = ANCHOR_DATE): EqlRule tiebreaker_field: undefined, }; }; + +export const getRulesNewTermsSchemaMock = (anchorDate: string = ANCHOR_DATE): NewTermsRule => { + return { + ...getResponseBaseParams(anchorDate), + type: 'new_terms', + query: '*', + language: 'kuery', + new_terms_fields: ['user.name'], + history_window_start: 'now-7d', + }; +}; + +export const getRulesThresholdSchemaMock = (anchorDate: string = ANCHOR_DATE): ThresholdRule => { + return { + ...getResponseBaseParams(anchorDate), + type: 'threshold', + language: 'kuery', + query: 'user.name: root or user.name: admin', + threshold: { + field: 'some.field', + value: 4, + }, + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_installation/review_rule_installation_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_installation/review_rule_installation_route.ts index 0b30c9bab478..ec3ca342bf8c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_installation/review_rule_installation_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_installation/review_rule_installation_route.ts @@ -18,7 +18,7 @@ import { createPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebui import { fetchRuleVersionsTriad } from '../../logic/rule_versions/fetch_rule_versions_triad'; import type { PrebuiltRuleAsset } from '../../model/rule_assets/prebuilt_rule_asset'; import { getVersionBuckets } from '../../model/rule_versions/get_version_buckets'; -import { convertPrebuiltRuleAssetToRuleResponse } from '../../../rule_management/normalization/rule_converters'; +import { convertPrebuiltRuleAssetToRuleResponse } from '../../../rule_management/logic/detection_rules_client/converters/convert_prebuilt_rule_asset_to_rule_response'; import { PREBUILT_RULES_OPERATION_SOCKET_TIMEOUT_MS } from '../../constants'; export const reviewRuleInstallationRoute = (router: SecuritySolutionPluginRouter) => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_route.ts index de7db929790d..f38fcc795364 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_route.ts @@ -27,7 +27,7 @@ import { createPrebuiltRuleAssetsClient } from '../../logic/rule_assets/prebuilt import { createPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebuilt_rule_objects_client'; import { fetchRuleVersionsTriad } from '../../logic/rule_versions/fetch_rule_versions_triad'; import { getVersionBuckets } from '../../model/rule_versions/get_version_buckets'; -import { convertPrebuiltRuleAssetToRuleResponse } from '../../../rule_management/normalization/rule_converters'; +import { convertPrebuiltRuleAssetToRuleResponse } from '../../../rule_management/logic/detection_rules_client/converters/convert_prebuilt_rule_asset_to_rule_response'; import { PREBUILT_RULES_OPERATION_SOCKET_TIMEOUT_MS } from '../../constants'; export const reviewRuleUpgradeRoute = (router: SecuritySolutionPluginRouter) => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_assets/__mocks__/prebuilt_rule_assets_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_assets/__mocks__/prebuilt_rule_assets_client.ts new file mode 100644 index 000000000000..0776fefb9865 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_assets/__mocks__/prebuilt_rule_assets_client.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. + */ + +export const createPrebuiltRuleAssetsClient = () => { + return { + fetchLatestAssets: jest.fn(), + fetchLatestVersions: jest.fn(), + fetchAssetsByVersion: jest.fn(), + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client.ts index 7ad4df3cddab..1138a48cc39d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client.ts @@ -13,7 +13,7 @@ import type { import { withSecuritySpan } from '../../../../../utils/with_security_span'; import { findRules } from '../../../rule_management/logic/search/find_rules'; import { getExistingPrepackagedRules } from '../../../rule_management/logic/search/get_existing_prepackaged_rules'; -import { internalRuleToAPIResponse } from '../../../rule_management/normalization/rule_converters'; +import { internalRuleToAPIResponse } from '../../../rule_management/logic/detection_rules_client/converters/internal_rule_to_api_response'; export interface IPrebuiltRuleObjectsClient { fetchAllInstalledRules(): Promise; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_exceptions/api/create_rule_exceptions/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_exceptions/api/create_rule_exceptions/route.ts index c8c257770e26..060043c14819 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_exceptions/api/create_rule_exceptions/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_exceptions/api/create_rule_exceptions/route.ts @@ -295,7 +295,7 @@ export const createAndAssociateDefaultExceptionList = async ({ : existingRuleExceptionLists; await detectionRulesClient.patchRule({ - nextParams: { + rulePatch: { rule_id: rule.params.ruleId, ...rule.params, exceptions_list: [ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/bulk_actions_response.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/bulk_actions_response.ts index 2bf14ccbf085..1b78da705e34 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/bulk_actions_response.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/bulk_actions_response.ts @@ -25,7 +25,7 @@ import type { BulkActionsDryRunErrCode } from '../../../../../../../common/const import type { PromisePoolError } from '../../../../../../utils/promise_pool'; import type { RuleAlertType } from '../../../../rule_schema'; import type { DryRunError } from '../../../logic/bulk_actions/dry_run'; -import { internalRuleToAPIResponse } from '../../../normalization/rule_converters'; +import { internalRuleToAPIResponse } from '../../../logic/detection_rules_client/converters/internal_rule_to_api_response'; const MAX_ERROR_MESSAGE_LENGTH = 1000; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_patch_rules/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_patch_rules/route.ts index 3b16ba5fa474..da75e4e33362 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_patch_rules/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_patch_rules/route.ts @@ -86,7 +86,7 @@ export const bulkPatchRulesRoute = (router: SecuritySolutionPluginRouter, logger }); const patchedRule = await detectionRulesClient.patchRule({ - nextParams: payloadRule, + rulePatch: payloadRule, }); return patchedRule; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/patch_rule/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/patch_rule/route.ts index 0e508f43103d..3886f63c482b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/patch_rule/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/patch_rule/route.ts @@ -76,7 +76,7 @@ export const patchRuleRoute = (router: SecuritySolutionPluginRouter) => { }); const patchedRule = await detectionRulesClient.patchRule({ - nextParams: params, + rulePatch: params, }); return response.ok({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/index.ts index 7e379651b2fa..f2e147ef3154 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/index.ts @@ -7,12 +7,7 @@ export * from './api/register_routes'; -// TODO: https://github.com/elastic/kibana/pull/142950 -// TODO: Revisit and consider moving to the rule_schema subdomain -export { - commonParamsCamelToSnake, - typeSpecificCamelToSnake, - convertCreateAPIToInternalSchema, -} from './normalization/rule_converters'; +export { commonParamsCamelToSnake } from './logic/detection_rules_client/converters/common_params_camel_to_snake'; +export { typeSpecificCamelToSnake } from './logic/detection_rules_client/converters/type_specific_camel_to_snake'; export { transformFromAlertThrottle, transformToNotifyWhen } from './normalization/rule_actions'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.ts index dd22dac3adc7..1dfa3a9b0e9a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.ts @@ -12,7 +12,6 @@ import type { SanitizedRule } from '@kbn/alerting-plugin/common'; import { SERVER_APP_ID } from '../../../../../../common/constants'; import type { InternalRuleCreate, RuleParams } from '../../../rule_schema'; import { transformToActionFrequency } from '../../normalization/rule_actions'; -import { convertImmutableToRuleSource } from '../../normalization/rule_converters'; const DUPLICATE_TITLE = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.cloneRule.duplicateTitle', @@ -47,7 +46,9 @@ export const duplicateRule = async ({ rule }: DuplicateRuleParams): Promise { + test('should convert rule_source params to snake case', () => { + const transformedParams = commonParamsCamelToSnake({ + ...getBaseRuleParams(), + ruleSource: { + type: 'external', + isCustomized: false, + }, + }); + expect(transformedParams).toEqual( + expect.objectContaining({ + rule_source: { + type: 'external', + is_customized: false, + }, + }) + ); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/common_params_camel_to_snake.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/common_params_camel_to_snake.ts new file mode 100644 index 000000000000..6f98230043e7 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/common_params_camel_to_snake.ts @@ -0,0 +1,47 @@ +/* + * 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 { convertObjectKeysToSnakeCase } from '../../../../../../utils/object_case_converters'; +import type { BaseRuleParams } from '../../../../rule_schema'; +import { migrateLegacyInvestigationFields } from '../../../utils/utils'; + +export const commonParamsCamelToSnake = (params: BaseRuleParams) => { + return { + description: params.description, + risk_score: params.riskScore, + severity: params.severity, + building_block_type: params.buildingBlockType, + namespace: params.namespace, + note: params.note, + license: params.license, + output_index: params.outputIndex, + timeline_id: params.timelineId, + timeline_title: params.timelineTitle, + meta: params.meta, + rule_name_override: params.ruleNameOverride, + timestamp_override: params.timestampOverride, + timestamp_override_fallback_disabled: params.timestampOverrideFallbackDisabled, + investigation_fields: migrateLegacyInvestigationFields(params.investigationFields), + author: params.author, + false_positives: params.falsePositives, + from: params.from, + rule_id: params.ruleId, + max_signals: params.maxSignals, + risk_score_mapping: params.riskScoreMapping, + severity_mapping: params.severityMapping, + threat: params.threat, + to: params.to, + references: params.references, + version: params.version, + exceptions_list: params.exceptionsList, + immutable: params.immutable, + rule_source: convertObjectKeysToSnakeCase(params.ruleSource), + related_integrations: params.relatedIntegrations ?? [], + required_fields: params.requiredFields ?? [], + setup: params.setup ?? '', + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/convert_alerting_rule_to_rule_response.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/convert_alerting_rule_to_rule_response.ts new file mode 100644 index 000000000000..ab7fb237e64f --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/convert_alerting_rule_to_rule_response.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SanitizedRule } from '@kbn/alerting-plugin/common'; +import { stringifyZodError } from '@kbn/zod-helpers'; +import { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema'; +import type { RuleParams } from '../../../../rule_schema'; +import { internalRuleToAPIResponse } from './internal_rule_to_api_response'; +import { RuleResponseValidationError } from '../utils'; + +export function convertAlertingRuleToRuleResponse(rule: SanitizedRule): RuleResponse { + const parseResult = RuleResponse.safeParse(internalRuleToAPIResponse(rule)); + + if (!parseResult.success) { + throw new RuleResponseValidationError({ + message: stringifyZodError(parseResult.error), + ruleId: rule.params.ruleId, + }); + } + + return parseResult.data; +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/convert_prebuilt_rule_asset_to_rule_response.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/convert_prebuilt_rule_asset_to_rule_response.ts new file mode 100644 index 000000000000..0cb42100d451 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/convert_prebuilt_rule_asset_to_rule_response.ts @@ -0,0 +1,39 @@ +/* + * 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 { v4 as uuidv4 } from 'uuid'; +import { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema'; +import { addEcsToRequiredFields } from '../../../utils/utils'; +import type { PrebuiltRuleAsset } from '../../../../prebuilt_rules'; +import { RULE_DEFAULTS } from '../mergers/apply_rule_defaults'; + +export const convertPrebuiltRuleAssetToRuleResponse = ( + prebuiltRuleAsset: PrebuiltRuleAsset +): RuleResponse => { + const immutable = true; + + const ruleResponseSpecificFields = { + id: uuidv4(), + updated_at: new Date().toISOString(), + updated_by: '', + created_at: new Date().toISOString(), + created_by: '', + immutable, + rule_source: { + type: 'external', + is_customized: false, + }, + revision: 1, + }; + + return RuleResponse.parse({ + ...RULE_DEFAULTS, + ...prebuiltRuleAsset, + required_fields: addEcsToRequiredFields(prebuiltRuleAsset.required_fields), + ...ruleResponseSpecificFields, + }); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/convert_rule_response_to_alerting_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/convert_rule_response_to_alerting_rule.ts new file mode 100644 index 000000000000..60a41211a66c --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/convert_rule_response_to_alerting_rule.ts @@ -0,0 +1,210 @@ +/* + * 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 { UpdateRuleData } from '@kbn/alerting-plugin/server/application/rule/methods/update'; +import type { + RuleResponse, + TypeSpecificCreateProps, +} from '../../../../../../../common/api/detection_engine/model/rule_schema'; +import { + transformRuleToAlertAction, + transformRuleToAlertResponseAction, +} from '../../../../../../../common/detection_engine/transform_actions'; +import { + normalizeMachineLearningJobIds, + normalizeThresholdObject, +} from '../../../../../../../common/detection_engine/utils'; +import { assertUnreachable } from '../../../../../../../common/utility_types'; +import { convertObjectKeysToCamelCase } from '../../../../../../utils/object_case_converters'; +import type { RuleParams, TypeSpecificRuleParams } from '../../../../rule_schema'; +import { transformToActionFrequency } from '../../../normalization/rule_actions'; +import { addEcsToRequiredFields } from '../../../utils/utils'; + +/** + * These are the fields that are added to the rule response that are not part of the rule params + */ +type RuntimeFields = + | 'id' + | 'created_at' + | 'updated_at' + | 'created_by' + | 'updated_by' + | 'revision' + | 'execution_summary'; + +export const convertRuleResponseToAlertingRule = ( + rule: Omit +): UpdateRuleData => { + const alertActions = rule.actions.map((action) => transformRuleToAlertAction(action)); + const actions = transformToActionFrequency(alertActions, rule.throttle); + + // Because of Omit Typescript doesn't recognize + // that rule is assignable to TypeSpecificCreateProps despite omitted fields + // are not part of type specific props. So we need to cast here. + const typeSpecificParams = typeSpecificSnakeToCamel(rule as TypeSpecificCreateProps); + + return { + name: rule.name, + tags: rule.tags, + params: { + author: rule.author, + buildingBlockType: rule.building_block_type, + description: rule.description, + ruleId: rule.rule_id, + falsePositives: rule.false_positives, + from: rule.from, + investigationFields: rule.investigation_fields, + immutable: rule.immutable, + ruleSource: convertObjectKeysToCamelCase(rule.rule_source), + license: rule.license, + outputIndex: rule.output_index ?? '', + timelineId: rule.timeline_id, + timelineTitle: rule.timeline_title, + meta: rule.meta, + maxSignals: rule.max_signals, + relatedIntegrations: rule.related_integrations, + requiredFields: addEcsToRequiredFields(rule.required_fields), + riskScore: rule.risk_score, + riskScoreMapping: rule.risk_score_mapping, + ruleNameOverride: rule.rule_name_override, + setup: rule.setup, + severity: rule.severity, + severityMapping: rule.severity_mapping, + threat: rule.threat, + timestampOverride: rule.timestamp_override, + timestampOverrideFallbackDisabled: rule.timestamp_override_fallback_disabled, + to: rule.to, + references: rule.references, + namespace: rule.namespace, + note: rule.note, + version: rule.version, + exceptionsList: rule.exceptions_list, + ...typeSpecificParams, + }, + schedule: { interval: rule.interval }, + actions, + }; +}; + +// Converts params from the snake case API format to the internal camel case format AND applies default values where needed. +// Notice that params.language is possibly undefined for most rule types in the API but we default it to kuery to match +// the legacy API behavior +const typeSpecificSnakeToCamel = (params: TypeSpecificCreateProps): TypeSpecificRuleParams => { + switch (params.type) { + case 'eql': { + return { + type: params.type, + language: params.language, + index: params.index, + dataViewId: params.data_view_id, + query: params.query, + filters: params.filters, + timestampField: params.timestamp_field, + eventCategoryOverride: params.event_category_override, + tiebreakerField: params.tiebreaker_field, + alertSuppression: convertObjectKeysToCamelCase(params.alert_suppression), + }; + } + case 'esql': { + return { + type: params.type, + language: params.language, + query: params.query, + alertSuppression: convertObjectKeysToCamelCase(params.alert_suppression), + }; + } + case 'threat_match': { + return { + type: params.type, + language: params.language ?? 'kuery', + index: params.index, + dataViewId: params.data_view_id, + query: params.query, + filters: params.filters, + savedId: params.saved_id, + threatFilters: params.threat_filters, + threatQuery: params.threat_query, + threatMapping: params.threat_mapping, + threatLanguage: params.threat_language, + threatIndex: params.threat_index, + threatIndicatorPath: params.threat_indicator_path, + concurrentSearches: params.concurrent_searches, + itemsPerSearch: params.items_per_search, + alertSuppression: convertObjectKeysToCamelCase(params.alert_suppression), + }; + } + case 'query': { + return { + type: params.type, + language: params.language ?? 'kuery', + index: params.index, + dataViewId: params.data_view_id, + query: params.query ?? '', + filters: params.filters, + savedId: params.saved_id, + responseActions: params.response_actions?.map((rule) => + transformRuleToAlertResponseAction(rule) + ), + alertSuppression: convertObjectKeysToCamelCase(params.alert_suppression), + }; + } + case 'saved_query': { + return { + type: params.type, + language: params.language ?? 'kuery', + index: params.index, + query: params.query, + filters: params.filters, + savedId: params.saved_id, + dataViewId: params.data_view_id, + responseActions: params.response_actions?.map((rule) => + transformRuleToAlertResponseAction(rule) + ), + alertSuppression: convertObjectKeysToCamelCase(params.alert_suppression), + }; + } + case 'threshold': { + return { + type: params.type, + language: params.language ?? 'kuery', + index: params.index, + dataViewId: params.data_view_id, + query: params.query, + filters: params.filters, + savedId: params.saved_id, + threshold: normalizeThresholdObject(params.threshold), + alertSuppression: params.alert_suppression?.duration + ? { duration: params.alert_suppression.duration } + : undefined, + }; + } + case 'machine_learning': { + return { + type: params.type, + anomalyThreshold: params.anomaly_threshold, + machineLearningJobId: normalizeMachineLearningJobIds(params.machine_learning_job_id), + alertSuppression: convertObjectKeysToCamelCase(params.alert_suppression), + }; + } + case 'new_terms': { + return { + type: params.type, + query: params.query, + newTermsFields: params.new_terms_fields, + historyWindowStart: params.history_window_start, + index: params.index, + filters: params.filters, + language: params.language ?? 'kuery', + dataViewId: params.data_view_id, + alertSuppression: convertObjectKeysToCamelCase(params.alert_suppression), + }; + } + default: { + return assertUnreachable(params); + } + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/internal_rule_to_api_response.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/internal_rule_to_api_response.ts new file mode 100644 index 000000000000..452f59df8dcf --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/internal_rule_to_api_response.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ResolvedSanitizedRule, SanitizedRule } from '@kbn/alerting-plugin/common'; +import type { RequiredOptional } from '@kbn/zod-helpers'; +import type { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema'; +import { transformAlertToRuleAction } from '../../../../../../../common/detection_engine/transform_actions'; +import { createRuleExecutionSummary } from '../../../../rule_monitoring'; +import type { RuleParams } from '../../../../rule_schema'; +import { + transformFromAlertThrottle, + transformToActionFrequency, +} from '../../../normalization/rule_actions'; +import { typeSpecificCamelToSnake } from './type_specific_camel_to_snake'; +import { commonParamsCamelToSnake } from './common_params_camel_to_snake'; + +export const internalRuleToAPIResponse = ( + rule: SanitizedRule | ResolvedSanitizedRule +): RequiredOptional => { + const executionSummary = createRuleExecutionSummary(rule); + + const isResolvedRule = (obj: unknown): obj is ResolvedSanitizedRule => { + const outcome = (obj as ResolvedSanitizedRule).outcome; + return outcome != null && outcome !== 'exactMatch'; + }; + + const alertActions = rule.actions.map(transformAlertToRuleAction); + const throttle = transformFromAlertThrottle(rule); + const actions = transformToActionFrequency(alertActions, throttle); + + return { + // saved object properties + outcome: isResolvedRule(rule) ? rule.outcome : undefined, + alias_target_id: isResolvedRule(rule) ? rule.alias_target_id : undefined, + alias_purpose: isResolvedRule(rule) ? rule.alias_purpose : undefined, + // Alerting framework params + id: rule.id, + updated_at: rule.updatedAt.toISOString(), + updated_by: rule.updatedBy ?? 'elastic', + created_at: rule.createdAt.toISOString(), + created_by: rule.createdBy ?? 'elastic', + name: rule.name, + tags: rule.tags, + interval: rule.schedule.interval, + enabled: rule.enabled, + revision: rule.revision, + // Security solution shared rule params + ...commonParamsCamelToSnake(rule.params), + // Type specific security solution rule params + ...typeSpecificCamelToSnake(rule.params), + // Actions + throttle: undefined, + actions, + // Execution summary + execution_summary: executionSummary ?? undefined, + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/type_specific_camel_to_snake.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/type_specific_camel_to_snake.test.ts new file mode 100644 index 000000000000..08e6d3cc64b0 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/type_specific_camel_to_snake.test.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 type { + AlertSuppressionDuration, + AlertSuppressionMissingFieldsStrategy, +} from '../../../../../../../common/api/detection_engine'; +import { getEqlRuleParams } from '../../../../rule_schema/mocks'; +import { typeSpecificCamelToSnake } from './type_specific_camel_to_snake'; + +describe('typeSpecificCamelToSnake', () => { + describe('EQL', () => { + test('should accept EQL params when existing rule type is EQL', () => { + const params = { + timestampField: 'event.created', + eventCategoryOverride: 'event.not_category', + tiebreakerField: 'event.created', + }; + const eqlRule = { ...getEqlRuleParams(), ...params }; + const transformedParams = typeSpecificCamelToSnake(eqlRule); + expect(transformedParams).toEqual( + expect.objectContaining({ + timestamp_field: 'event.created', + event_category_override: 'event.not_category', + tiebreaker_field: 'event.created', + }) + ); + }); + + test('should accept EQL params with suppression in camel case and convert to snake case when rule type is EQL', () => { + const params = { + timestampField: 'event.created', + eventCategoryOverride: 'event.not_category', + tiebreakerField: 'event.created', + alertSuppression: { + groupBy: ['event.type'], + duration: { + value: 10, + unit: 'm', + } as AlertSuppressionDuration, + missingFieldsStrategy: 'suppress' as AlertSuppressionMissingFieldsStrategy, + }, + }; + const eqlRule = { ...getEqlRuleParams(), ...params }; + const transformedParams = typeSpecificCamelToSnake(eqlRule); + expect(transformedParams).toEqual( + expect.objectContaining({ + timestamp_field: 'event.created', + event_category_override: 'event.not_category', + tiebreaker_field: 'event.created', + alert_suppression: { + group_by: ['event.type'], + duration: { + value: 10, + unit: 'm', + } as AlertSuppressionDuration, + missing_fields_strategy: 'suppress', + }, + }) + ); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/type_specific_camel_to_snake.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/type_specific_camel_to_snake.ts new file mode 100644 index 000000000000..0808d1921e9b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/type_specific_camel_to_snake.ts @@ -0,0 +1,127 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { RequiredOptional } from '@kbn/zod-helpers'; +import type { TypeSpecificResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema'; +import { transformAlertToRuleResponseAction } from '../../../../../../../common/detection_engine/transform_actions'; +import { assertUnreachable } from '../../../../../../../common/utility_types'; +import { convertObjectKeysToSnakeCase } from '../../../../../../utils/object_case_converters'; +import type { TypeSpecificRuleParams } from '../../../../rule_schema'; + +export const typeSpecificCamelToSnake = ( + params: TypeSpecificRuleParams +): RequiredOptional => { + switch (params.type) { + case 'eql': { + return { + type: params.type, + language: params.language, + index: params.index, + data_view_id: params.dataViewId, + query: params.query, + filters: params.filters, + timestamp_field: params.timestampField, + event_category_override: params.eventCategoryOverride, + tiebreaker_field: params.tiebreakerField, + alert_suppression: convertObjectKeysToSnakeCase(params.alertSuppression), + }; + } + case 'esql': { + return { + type: params.type, + language: params.language, + query: params.query, + alert_suppression: convertObjectKeysToSnakeCase(params.alertSuppression), + }; + } + case 'threat_match': { + return { + type: params.type, + language: params.language, + index: params.index, + data_view_id: params.dataViewId, + query: params.query, + filters: params.filters, + saved_id: params.savedId, + threat_filters: params.threatFilters, + threat_query: params.threatQuery, + threat_mapping: params.threatMapping, + threat_language: params.threatLanguage, + threat_index: params.threatIndex, + threat_indicator_path: params.threatIndicatorPath, + concurrent_searches: params.concurrentSearches, + items_per_search: params.itemsPerSearch, + alert_suppression: convertObjectKeysToSnakeCase(params.alertSuppression), + }; + } + case 'query': { + return { + type: params.type, + language: params.language, + index: params.index, + data_view_id: params.dataViewId, + query: params.query, + filters: params.filters, + saved_id: params.savedId, + response_actions: params.responseActions?.map(transformAlertToRuleResponseAction), + alert_suppression: convertObjectKeysToSnakeCase(params.alertSuppression), + }; + } + case 'saved_query': { + return { + type: params.type, + language: params.language, + index: params.index, + query: params.query, + filters: params.filters, + saved_id: params.savedId, + data_view_id: params.dataViewId, + response_actions: params.responseActions?.map(transformAlertToRuleResponseAction), + alert_suppression: convertObjectKeysToSnakeCase(params.alertSuppression), + }; + } + case 'threshold': { + return { + type: params.type, + language: params.language, + index: params.index, + data_view_id: params.dataViewId, + query: params.query, + filters: params.filters, + saved_id: params.savedId, + threshold: params.threshold, + alert_suppression: params.alertSuppression?.duration + ? { duration: params.alertSuppression?.duration } + : undefined, + }; + } + case 'machine_learning': { + return { + type: params.type, + anomaly_threshold: params.anomalyThreshold, + machine_learning_job_id: params.machineLearningJobId, + alert_suppression: convertObjectKeysToSnakeCase(params.alertSuppression), + }; + } + case 'new_terms': { + return { + type: params.type, + query: params.query, + new_terms_fields: params.newTermsFields, + history_window_start: params.historyWindowStart, + index: params.index, + filters: params.filters, + language: params.language, + data_view_id: params.dataViewId, + alert_suppression: convertObjectKeysToSnakeCase(params.alertSuppression), + }; + } + default: { + return assertUnreachable(params); + } + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.create_custom_rule.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.create_custom_rule.test.ts index 7aab6640a1b5..5578854ed95b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.create_custom_rule.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.create_custom_rule.test.ts @@ -6,6 +6,7 @@ */ import { rulesClientMock } from '@kbn/alerting-plugin/server/mocks'; +import { savedObjectsClientMock } from '@kbn/core/server/mocks'; import { getCreateRulesSchemaMock, @@ -35,7 +36,8 @@ describe('DetectionRulesClient.createCustomRule', () => { rulesClient = rulesClientMock.create(); rulesClient.create.mockResolvedValue(getRuleMock(getQueryRuleParams())); - detectionRulesClient = createDetectionRulesClient(rulesClient, mlAuthz); + const savedObjectsClient = savedObjectsClientMock.create(); + detectionRulesClient = createDetectionRulesClient({ rulesClient, mlAuthz, savedObjectsClient }); }); it('should create a rule with the correct parameters and options', async () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.create_prebuilt_rule.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.create_prebuilt_rule.test.ts index fd3ac991a968..f91c577f3b2a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.create_prebuilt_rule.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.create_prebuilt_rule.test.ts @@ -6,6 +6,7 @@ */ import { rulesClientMock } from '@kbn/alerting-plugin/server/mocks'; +import { savedObjectsClientMock } from '@kbn/core/server/mocks'; import { getCreateRulesSchemaMock, @@ -35,7 +36,8 @@ describe('DetectionRulesClient.createPrebuiltRule', () => { rulesClient = rulesClientMock.create(); rulesClient.create.mockResolvedValue(getRuleMock(getQueryRuleParams())); - detectionRulesClient = createDetectionRulesClient(rulesClient, mlAuthz); + const savedObjectsClient = savedObjectsClientMock.create(); + detectionRulesClient = createDetectionRulesClient({ rulesClient, mlAuthz, savedObjectsClient }); }); it('creates a rule with the correct parameters and options', async () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.delete_rule.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.delete_rule.test.ts index 37cb8e0aa709..166656701f30 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.delete_rule.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.delete_rule.test.ts @@ -6,6 +6,7 @@ */ import { rulesClientMock } from '@kbn/alerting-plugin/server/mocks'; +import { savedObjectsClientMock } from '@kbn/core/server/mocks'; import { buildMlAuthz } from '../../../../machine_learning/authz'; import { createDetectionRulesClient } from './detection_rules_client'; import type { IDetectionRulesClient } from './detection_rules_client_interface'; @@ -20,7 +21,8 @@ describe('DetectionRulesClient.deleteRule', () => { beforeEach(() => { rulesClient = rulesClientMock.create(); - detectionRulesClient = createDetectionRulesClient(rulesClient, mlAuthz); + const savedObjectsClient = savedObjectsClientMock.create(); + detectionRulesClient = createDetectionRulesClient({ rulesClient, mlAuthz, savedObjectsClient }); }); it('should call rulesClient.delete passing the expected ruleId', async () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.import_rule.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.import_rule.test.ts index 474fecc18651..fb9b4f7995c9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.import_rule.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.import_rule.test.ts @@ -6,19 +6,23 @@ */ import { rulesClientMock } from '@kbn/alerting-plugin/server/mocks'; -import { readRules } from './read_rules'; -import { getCreateRulesSchemaMock } from '../../../../../../common/api/detection_engine/model/rule_schema/mocks'; -import { getRuleMock } from '../../../routes/__mocks__/request_responses'; -import { getQueryRuleParams } from '../../../rule_schema/mocks'; +import { savedObjectsClientMock } from '@kbn/core/server/mocks'; +import { + getCreateRulesSchemaMock, + getRulesSchemaMock, +} from '../../../../../../common/api/detection_engine/model/rule_schema/mocks'; import { buildMlAuthz } from '../../../../machine_learning/authz'; import { throwAuthzError } from '../../../../machine_learning/validation'; +import { getRuleMock } from '../../../routes/__mocks__/request_responses'; +import { getQueryRuleParams } from '../../../rule_schema/mocks'; import { createDetectionRulesClient } from './detection_rules_client'; import type { IDetectionRulesClient } from './detection_rules_client_interface'; +import { getRuleByRuleId } from './methods/get_rule_by_rule_id'; jest.mock('../../../../machine_learning/authz'); jest.mock('../../../../machine_learning/validation'); -jest.mock('./read_rules'); +jest.mock('./methods/get_rule_by_rule_id'); describe('DetectionRulesClient.importRule', () => { let rulesClient: ReturnType; @@ -34,21 +38,19 @@ describe('DetectionRulesClient.importRule', () => { version: 1, immutable, }; - const existingRule = getRuleMock({ - ...getQueryRuleParams({ - ruleId: ruleToImport.rule_id, - }), - }); + const existingRule = getRulesSchemaMock(); + existingRule.rule_id = ruleToImport.rule_id; beforeEach(() => { rulesClient = rulesClientMock.create(); rulesClient.create.mockResolvedValue(getRuleMock(getQueryRuleParams())); rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); - detectionRulesClient = createDetectionRulesClient(rulesClient, mlAuthz); + const savedObjectsClient = savedObjectsClientMock.create(); + detectionRulesClient = createDetectionRulesClient({ rulesClient, mlAuthz, savedObjectsClient }); }); it('calls rulesClient.create with the correct parameters when rule_id does not match an installed rule', async () => { - (readRules as jest.Mock).mockResolvedValue(null); + (getRuleByRuleId as jest.Mock).mockResolvedValueOnce(null); await detectionRulesClient.importRule({ ruleToImport, overwriteRules: true, @@ -90,7 +92,8 @@ describe('DetectionRulesClient.importRule', () => { describe('when rule_id matches an installed rule', () => { it('calls rulesClient.update with the correct parameters when overwriteRules is true', async () => { - (readRules as jest.Mock).mockResolvedValue(existingRule); + (getRuleByRuleId as jest.Mock).mockResolvedValueOnce(existingRule); + await detectionRulesClient.importRule({ ruleToImport, overwriteRules: true, @@ -122,12 +125,9 @@ describe('DetectionRulesClient.importRule', () => { it('ensures overwritten rule DOES NOT preserve fields missed in the imported rule when "overwriteRules" is "true" and matching rule found', async () => { const existingRuleWithTimestampOverride = { ...existingRule, - params: { - ...existingRule.params, - timestamp_override: '2020-01-01T00:00:00Z', - }, + timestamp_override: '2020-01-01T00:00:00Z', }; - (readRules as jest.Mock).mockResolvedValue(existingRuleWithTimestampOverride); + (getRuleByRuleId as jest.Mock).mockResolvedValue(existingRuleWithTimestampOverride); await detectionRulesClient.importRule({ ruleToImport: { @@ -151,7 +151,7 @@ describe('DetectionRulesClient.importRule', () => { }); it('rejects when overwriteRules is false', async () => { - (readRules as jest.Mock).mockResolvedValue(existingRule); + (getRuleByRuleId as jest.Mock).mockResolvedValue(existingRule); await expect( detectionRulesClient.importRule({ ruleToImport, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.patch_rule.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.patch_rule.test.ts index 7f1c21988863..d17b12415642 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.patch_rule.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.patch_rule.test.ts @@ -12,17 +12,20 @@ import { getMlRuleParams, getQueryRuleParams } from '../../../rule_schema/mocks' import { getCreateMachineLearningRulesSchemaMock, getCreateRulesSchemaMock, + getRulesMlSchemaMock, + getRulesSchemaMock, } from '../../../../../../common/api/detection_engine/model/rule_schema/mocks'; -import { readRules } from './read_rules'; +import { getRuleByRuleId } from './methods/get_rule_by_rule_id'; import { buildMlAuthz } from '../../../../machine_learning/authz'; import { throwAuthzError } from '../../../../machine_learning/validation'; import { createDetectionRulesClient } from './detection_rules_client'; import type { IDetectionRulesClient } from './detection_rules_client_interface'; +import { savedObjectsClientMock } from '@kbn/core/server/mocks'; jest.mock('../../../../machine_learning/authz'); jest.mock('../../../../machine_learning/validation'); -jest.mock('./read_rules'); +jest.mock('./methods/get_rule_by_rule_id'); describe('DetectionRulesClient.patchRule', () => { let rulesClient: ReturnType; @@ -32,97 +35,78 @@ describe('DetectionRulesClient.patchRule', () => { beforeEach(() => { rulesClient = rulesClientMock.create(); - detectionRulesClient = createDetectionRulesClient(rulesClient, mlAuthz); + const savedObjectsClient = savedObjectsClientMock.create(); + detectionRulesClient = createDetectionRulesClient({ rulesClient, mlAuthz, savedObjectsClient }); }); it('calls the rulesClient with expected params', async () => { - const nextParams = getCreateRulesSchemaMock(); - const existingRule = getRuleMock(getQueryRuleParams()); - (readRules as jest.Mock).mockResolvedValueOnce(existingRule); + // Mock the existing rule + const existingRule = getRulesSchemaMock(); + (getRuleByRuleId as jest.Mock).mockResolvedValueOnce(existingRule); + + // Mock the rule update + const rulePatch = getCreateRulesSchemaMock('query-rule-id'); + rulePatch.name = 'new name'; + rulePatch.description = 'new description'; + + // Mock the rule returned after update; not used for this test directly but + // needed so that the patchRule method does not throw rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); - await detectionRulesClient.patchRule({ nextParams }); + await detectionRulesClient.patchRule({ rulePatch }); expect(rulesClient.update).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ - name: nextParams.name, + name: rulePatch.name, params: expect.objectContaining({ - ruleId: nextParams.rule_id, - description: nextParams.description, + ruleId: rulePatch.rule_id, + description: rulePatch.description, }), }), }) ); }); - it('returns rule enabled: true if the nexParams have enabled: true', async () => { - const nextParams = { ...getCreateRulesSchemaMock(), enabled: true }; - const existingRule = getRuleMock(getQueryRuleParams()); - (readRules as jest.Mock).mockResolvedValueOnce(existingRule); - rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); + it('enables the rule if the nexParams have enabled: true', async () => { + // Mock the existing rule + const existingRule = getRulesSchemaMock(); + existingRule.enabled = false; + (getRuleByRuleId as jest.Mock).mockResolvedValueOnce(existingRule); - const rule = await detectionRulesClient.patchRule({ nextParams }); + // Mock the rule update + const rulePatch = { ...getCreateRulesSchemaMock(), enabled: true }; - expect(rule.enabled).toBe(true); - }); + // Mock the rule returned after update; not used for this test directly but + // needed so that the patchRule method does not throw + rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); - it('calls the rulesClient with legacy ML params', async () => { - const nextParams = getCreateMachineLearningRulesSchemaMock(); - const existingRule = getRuleMock(getMlRuleParams()); - (readRules as jest.Mock).mockResolvedValueOnce(existingRule); - rulesClient.update.mockResolvedValue(getRuleMock(getMlRuleParams())); + const rule = await detectionRulesClient.patchRule({ rulePatch }); - await detectionRulesClient.patchRule({ nextParams }); - expect(rulesClient.update).toHaveBeenCalledWith( + expect(rule.enabled).toBe(true); + expect(rulesClient.enable).toHaveBeenCalledWith( expect.objectContaining({ - data: expect.objectContaining({ - params: expect.objectContaining({ - anomalyThreshold: 58, - machineLearningJobId: ['typical-ml-job-id'], - }), - }), + id: existingRule.id, }) ); }); - it('calls the rulesClient with new ML params', async () => { - const nextParams = { - ...getCreateMachineLearningRulesSchemaMock(), - machine_learning_job_id: ['new_job_1', 'new_job_2'], - }; - const existingRule = getRuleMock(getMlRuleParams()); - (readRules as jest.Mock).mockResolvedValueOnce(existingRule); - rulesClient.update.mockResolvedValue(getRuleMock(getMlRuleParams())); - - await detectionRulesClient.patchRule({ nextParams }); + it('disables the rule if the nexParams have enabled: false', async () => { + // Mock the existing rule + const existingRule = getRulesSchemaMock(); + existingRule.enabled = true; + (getRuleByRuleId as jest.Mock).mockResolvedValueOnce(existingRule); - expect(rulesClient.update).toHaveBeenCalledWith( - expect.objectContaining({ - data: expect.objectContaining({ - params: expect.objectContaining({ - anomalyThreshold: 58, - machineLearningJobId: ['new_job_1', 'new_job_2'], - }), - }), - }) - ); - }); + // Mock the rule update + const rulePatch = { ...getCreateRulesSchemaMock(), enabled: false }; - it('should call rulesClient.disable if the rule was enabled and enabled is false', async () => { - const nextParams = { - ...getCreateRulesSchemaMock(), - enabled: false, - }; - const existingRule = { - ...getRuleMock(getQueryRuleParams()), - enabled: true, - }; - (readRules as jest.Mock).mockResolvedValueOnce(existingRule); + // Mock the rule returned after update; not used for this test directly but + // needed so that the patchRule method does not throw rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); - await detectionRulesClient.patchRule({ nextParams }); + const rule = await detectionRulesClient.patchRule({ rulePatch }); + expect(rule.enabled).toBe(false); expect(rulesClient.disable).toHaveBeenCalledWith( expect.objectContaining({ id: existingRule.id, @@ -130,23 +114,29 @@ describe('DetectionRulesClient.patchRule', () => { ); }); - it('should call rulesClient.enable if the rule was disabled and enabled is true', async () => { - const nextParams = { - ...getCreateRulesSchemaMock(), - enabled: true, - }; - const existingRule = { - ...getRuleMock(getQueryRuleParams()), - enabled: false, - }; - (readRules as jest.Mock).mockResolvedValueOnce(existingRule); - rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); + it('calls the rulesClient with new ML params', async () => { + // Mock the existing rule + const existingRule = getRulesMlSchemaMock(); + (getRuleByRuleId as jest.Mock).mockResolvedValueOnce(existingRule); - await detectionRulesClient.patchRule({ nextParams }); + // Mock the rule update + const rulePatch = getCreateMachineLearningRulesSchemaMock(); + rulePatch.anomaly_threshold = 42; + rulePatch.machine_learning_job_id = ['new-job-id']; - expect(rulesClient.enable).toHaveBeenCalledWith( + // Mock the rule returned after update; not used for this test directly but + // needed so that the patchRule method does not throw + rulesClient.update.mockResolvedValue(getRuleMock(getMlRuleParams())); + + await detectionRulesClient.patchRule({ rulePatch }); + expect(rulesClient.update).toHaveBeenCalledWith( expect.objectContaining({ - id: existingRule.id, + data: expect.objectContaining({ + params: expect.objectContaining({ + anomalyThreshold: rulePatch.anomaly_threshold, + machineLearningJobId: rulePatch.machine_learning_job_id, + }), + }), }) ); }); @@ -156,21 +146,23 @@ describe('DetectionRulesClient.patchRule', () => { throw new Error('mocked MLAuth error'); }); - const nextParams = { - ...getCreateRulesSchemaMock(), - enabled: true, - }; + const rulePatch = getCreateRulesSchemaMock(); - await expect(detectionRulesClient.patchRule({ nextParams })).rejects.toThrow( + await expect(detectionRulesClient.patchRule({ rulePatch })).rejects.toThrow( 'mocked MLAuth error' ); expect(rulesClient.create).not.toHaveBeenCalled(); }); - describe('regression tests', () => { + describe('actions', () => { it("updates the rule's actions if provided", async () => { - const nextParams = { + // Mock the existing rule + const existingRule = getRulesSchemaMock(); + (getRuleByRuleId as jest.Mock).mockResolvedValueOnce(existingRule); + + // Mock the rule update + const rulePatch = { ...getCreateRulesSchemaMock(), actions: [ { @@ -183,11 +175,12 @@ describe('DetectionRulesClient.patchRule', () => { }, ], }; - const existingRule = getRuleMock(getQueryRuleParams()); - (readRules as jest.Mock).mockResolvedValueOnce(existingRule); + + // Mock the rule returned after update; not used for this test directly but + // needed so that the patchRule method does not throw rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); - await detectionRulesClient.patchRule({ nextParams }); + await detectionRulesClient.patchRule({ rulePatch }); expect(rulesClient.update).toHaveBeenCalledWith( expect.objectContaining({ @@ -209,12 +202,12 @@ describe('DetectionRulesClient.patchRule', () => { }); it('does not update actions if none are specified', async () => { - const nextParams = getCreateRulesSchemaMock(); - delete nextParams.actions; - const existingRule = getRuleMock(getQueryRuleParams()); + // Mock the existing rule + const existingRule = getRulesSchemaMock(); + (getRuleByRuleId as jest.Mock).mockResolvedValueOnce(existingRule); existingRule.actions = [ { - actionTypeId: '.slack', + action_type_id: '.slack', id: '2933e581-d81c-4fe3-88fe-c57c6b8a5bfd', params: { message: 'Rule {{context.rule.name}} generated {{state.signals_count}} signals', @@ -222,10 +215,16 @@ describe('DetectionRulesClient.patchRule', () => { group: 'default', }, ]; - (readRules as jest.Mock).mockResolvedValueOnce(existingRule); + + // Mock the rule update + const rulePatch = getCreateRulesSchemaMock(); + delete rulePatch.actions; + + // Mock the rule returned after update; not used for this test directly but + // needed so that the patchRule method does not throw rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); - await detectionRulesClient.patchRule({ nextParams }); + await detectionRulesClient.patchRule({ rulePatch }); expect(rulesClient.update).toHaveBeenCalledWith( expect.objectContaining({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.ts index ce6043a42090..dfcfc8f7fa39 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.ts @@ -6,73 +6,110 @@ */ import type { RulesClient } from '@kbn/alerting-plugin/server'; -import type { MlAuthz } from '../../../../machine_learning/authz'; - +import type { SavedObjectsClientContract } from '@kbn/core/server'; import type { RuleResponse } from '../../../../../../common/api/detection_engine/model/rule_schema'; +import { withSecuritySpan } from '../../../../../utils/with_security_span'; +import type { MlAuthz } from '../../../../machine_learning/authz'; +import { createPrebuiltRuleAssetsClient } from '../../../prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client'; import type { - IDetectionRulesClient, CreateCustomRuleArgs, CreatePrebuiltRuleArgs, - UpdateRuleArgs, - PatchRuleArgs, DeleteRuleArgs, - UpgradePrebuiltRuleArgs, + IDetectionRulesClient, ImportRuleArgs, + PatchRuleArgs, + UpdateRuleArgs, + UpgradePrebuiltRuleArgs, } from './detection_rules_client_interface'; - -import { createCustomRule } from './methods/create_custom_rule'; -import { createPrebuiltRule } from './methods/create_prebuilt_rule'; -import { updateRule } from './methods/update_rule'; -import { patchRule } from './methods/patch_rule'; +import { createRule } from './methods/create_rule'; import { deleteRule } from './methods/delete_rule'; -import { upgradePrebuiltRule } from './methods/upgrade_prebuilt_rule'; import { importRule } from './methods/import_rule'; +import { patchRule } from './methods/patch_rule'; +import { updateRule } from './methods/update_rule'; +import { upgradePrebuiltRule } from './methods/upgrade_prebuilt_rule'; -import { withSecuritySpan } from '../../../../../utils/with_security_span'; +interface DetectionRulesClientParams { + rulesClient: RulesClient; + savedObjectsClient: SavedObjectsClientContract; + mlAuthz: MlAuthz; +} + +export const createDetectionRulesClient = ({ + rulesClient, + mlAuthz, + savedObjectsClient, +}: DetectionRulesClientParams): IDetectionRulesClient => { + const prebuiltRuleAssetClient = createPrebuiltRuleAssetsClient(savedObjectsClient); -export const createDetectionRulesClient = ( - rulesClient: RulesClient, - mlAuthz: MlAuthz -): IDetectionRulesClient => ({ - async createCustomRule(args: CreateCustomRuleArgs): Promise { - return withSecuritySpan('DetectionRulesClient.createCustomRule', async () => { - return createCustomRule(rulesClient, args, mlAuthz); - }); - }, + return { + async createCustomRule(args: CreateCustomRuleArgs): Promise { + return withSecuritySpan('DetectionRulesClient.createCustomRule', async () => { + return createRule({ + rulesClient, + rule: { + ...args.params, + // For backwards compatibility, we default to true if not provided. + // The default enabled value is false for prebuilt rules, and true + // for custom rules. + enabled: args.params.enabled ?? true, + immutable: false, + }, + mlAuthz, + }); + }); + }, - async createPrebuiltRule(args: CreatePrebuiltRuleArgs): Promise { - return withSecuritySpan('DetectionRulesClient.createPrebuiltRule', async () => { - return createPrebuiltRule(rulesClient, args, mlAuthz); - }); - }, + async createPrebuiltRule(args: CreatePrebuiltRuleArgs): Promise { + return withSecuritySpan('DetectionRulesClient.createPrebuiltRule', async () => { + return createRule({ + rulesClient, + rule: { + ...args.params, + immutable: true, + }, + mlAuthz, + }); + }); + }, - async updateRule(args: UpdateRuleArgs): Promise { - return withSecuritySpan('DetectionRulesClient.updateRule', async () => { - return updateRule(rulesClient, args, mlAuthz); - }); - }, + async updateRule({ ruleUpdate }: UpdateRuleArgs): Promise { + return withSecuritySpan('DetectionRulesClient.updateRule', async () => { + return updateRule({ rulesClient, prebuiltRuleAssetClient, mlAuthz, ruleUpdate }); + }); + }, - async patchRule(args: PatchRuleArgs): Promise { - return withSecuritySpan('DetectionRulesClient.patchRule', async () => { - return patchRule(rulesClient, args, mlAuthz); - }); - }, + async patchRule({ rulePatch }: PatchRuleArgs): Promise { + return withSecuritySpan('DetectionRulesClient.patchRule', async () => { + return patchRule({ rulesClient, prebuiltRuleAssetClient, mlAuthz, rulePatch }); + }); + }, - async deleteRule(args: DeleteRuleArgs): Promise { - return withSecuritySpan('DetectionRulesClient.deleteRule', async () => { - return deleteRule(rulesClient, args); - }); - }, + async deleteRule({ ruleId }: DeleteRuleArgs): Promise { + return withSecuritySpan('DetectionRulesClient.deleteRule', async () => { + return deleteRule({ rulesClient, ruleId }); + }); + }, - async upgradePrebuiltRule(args: UpgradePrebuiltRuleArgs): Promise { - return withSecuritySpan('DetectionRulesClient.upgradePrebuiltRule', async () => { - return upgradePrebuiltRule(rulesClient, args, mlAuthz); - }); - }, + async upgradePrebuiltRule({ ruleAsset }: UpgradePrebuiltRuleArgs): Promise { + return withSecuritySpan('DetectionRulesClient.upgradePrebuiltRule', async () => { + return upgradePrebuiltRule({ + rulesClient, + ruleAsset, + mlAuthz, + prebuiltRuleAssetClient, + }); + }); + }, - async importRule(args: ImportRuleArgs): Promise { - return withSecuritySpan('DetectionRulesClient.importRule', async () => { - return importRule(rulesClient, args, mlAuthz); - }); - }, -}); + async importRule(args: ImportRuleArgs): Promise { + return withSecuritySpan('DetectionRulesClient.importRule', async () => { + return importRule({ + rulesClient, + importRulePayload: args, + mlAuthz, + prebuiltRuleAssetClient, + }); + }); + }, + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.update_rule.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.update_rule.test.ts index 671460b046fe..db9d122e7d91 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.update_rule.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.update_rule.test.ts @@ -12,17 +12,20 @@ import { getMlRuleParams, getQueryRuleParams } from '../../../rule_schema/mocks' import { getCreateMachineLearningRulesSchemaMock, getCreateRulesSchemaMock, + getRulesMlSchemaMock, + getRulesSchemaMock, } from '../../../../../../common/api/detection_engine/model/rule_schema/mocks'; -import { readRules } from './read_rules'; +import { getRuleByRuleId } from './methods/get_rule_by_rule_id'; import { buildMlAuthz } from '../../../../machine_learning/authz'; import { throwAuthzError } from '../../../../machine_learning/validation'; import { createDetectionRulesClient } from './detection_rules_client'; import type { IDetectionRulesClient } from './detection_rules_client_interface'; +import { savedObjectsClientMock } from '@kbn/core/server/mocks'; jest.mock('../../../../machine_learning/authz'); jest.mock('../../../../machine_learning/validation'); -jest.mock('./read_rules'); +jest.mock('./methods/get_rule_by_rule_id'); describe('DetectionRulesClient.updateRule', () => { let rulesClient: ReturnType; @@ -32,13 +35,22 @@ describe('DetectionRulesClient.updateRule', () => { beforeEach(() => { rulesClient = rulesClientMock.create(); - detectionRulesClient = createDetectionRulesClient(rulesClient, mlAuthz); + const savedObjectsClient = savedObjectsClientMock.create(); + detectionRulesClient = createDetectionRulesClient({ rulesClient, mlAuthz, savedObjectsClient }); }); it('calls the rulesClient with expected params', async () => { - const ruleUpdate = getCreateRulesSchemaMock(); - const existingRule = getRuleMock(getQueryRuleParams()); - (readRules as jest.Mock).mockResolvedValueOnce(existingRule); + // Mock the existing rule + const existingRule = getRulesSchemaMock(); + (getRuleByRuleId as jest.Mock).mockResolvedValueOnce(existingRule); + + // Mock the rule update + const ruleUpdate = getCreateRulesSchemaMock('query-rule-id'); + ruleUpdate.name = 'new name'; + ruleUpdate.description = 'new description'; + + // Mock the rule returned after update; not used for this test directly but + // needed so that the patchRule method does not throw rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); await detectionRulesClient.updateRule({ ruleUpdate }); @@ -56,21 +68,18 @@ describe('DetectionRulesClient.updateRule', () => { ); }); - it('returns rule enabled: true if the nexParams have enabled: true', async () => { - const ruleUpdate = { ...getCreateRulesSchemaMock(), enabled: true }; - const existingRule = getRuleMock(getQueryRuleParams()); - (readRules as jest.Mock).mockResolvedValueOnce(existingRule); - rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); - - const rule = await detectionRulesClient.updateRule({ ruleUpdate }); - - expect(rule.enabled).toBe(true); - }); + it('calls the rulesClient with new ML params', async () => { + // Mock the existing rule + const existingRule = getRulesMlSchemaMock(); + (getRuleByRuleId as jest.Mock).mockResolvedValueOnce(existingRule); - it('calls the rulesClient with legacy ML params', async () => { + // Mock the rule update const ruleUpdate = getCreateMachineLearningRulesSchemaMock(); - const existingRule = getRuleMock(getMlRuleParams()); - (readRules as jest.Mock).mockResolvedValueOnce(existingRule); + ruleUpdate.anomaly_threshold = 42; + ruleUpdate.machine_learning_job_id = ['new-job-id']; + + // Mock the rule returned after update; not used for this test directly but + // needed so that the patchRule method does not throw rulesClient.update.mockResolvedValue(getRuleMock(getMlRuleParams())); await detectionRulesClient.updateRule({ ruleUpdate }); @@ -79,48 +88,26 @@ describe('DetectionRulesClient.updateRule', () => { expect.objectContaining({ data: expect.objectContaining({ params: expect.objectContaining({ - anomalyThreshold: 58, - machineLearningJobId: ['typical-ml-job-id'], + anomalyThreshold: ruleUpdate.anomaly_threshold, + machineLearningJobId: ruleUpdate.machine_learning_job_id, }), }), }) ); }); - it('calls the rulesClient with new ML params', async () => { - const ruleUpdate = { - ...getCreateMachineLearningRulesSchemaMock(), - machine_learning_job_id: ['new_job_1', 'new_job_2'], - }; - const existingRule = getRuleMock(getMlRuleParams()); - (readRules as jest.Mock).mockResolvedValueOnce(existingRule); - rulesClient.update.mockResolvedValue(getRuleMock(getMlRuleParams())); + it('disables rule if the rule was enabled and enabled is false', async () => { + // Mock the existing rule + const existingRule = getRulesSchemaMock(); + existingRule.enabled = true; + (getRuleByRuleId as jest.Mock).mockResolvedValueOnce(existingRule); - await detectionRulesClient.updateRule({ ruleUpdate }); + // Mock the rule update + const ruleUpdate = { ...getCreateRulesSchemaMock(), enabled: false }; - expect(rulesClient.update).toHaveBeenCalledWith( - expect.objectContaining({ - data: expect.objectContaining({ - params: expect.objectContaining({ - anomalyThreshold: 58, - machineLearningJobId: ['new_job_1', 'new_job_2'], - }), - }), - }) - ); - }); - - it('should call rulesClient.disable if the rule was enabled and enabled is false', async () => { - const ruleUpdate = { - ...getCreateRulesSchemaMock(), - enabled: false, - }; - const existingRule = { - ...getRuleMock(getQueryRuleParams()), - enabled: true, - }; + // Mock the rule returned after update; not used for this test directly but + // needed so that the patchRule method does not throw rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); - (readRules as jest.Mock).mockResolvedValueOnce(existingRule); await detectionRulesClient.updateRule({ ruleUpdate }); @@ -131,17 +118,18 @@ describe('DetectionRulesClient.updateRule', () => { ); }); - it('should call rulesClient.enable if the rule was disabled and enabled is true', async () => { - const ruleUpdate = { - ...getCreateRulesSchemaMock(), - enabled: true, - }; - const existingRule = { - ...getRuleMock(getQueryRuleParams()), - enabled: false, - }; + it('enables rule if the rule was disabled and enabled is true', async () => { + // Mock the existing rule + const existingRule = getRulesSchemaMock(); + existingRule.enabled = false; + (getRuleByRuleId as jest.Mock).mockResolvedValueOnce(existingRule); + + // Mock the rule update + const ruleUpdate = { ...getCreateRulesSchemaMock(), enabled: true }; + + // Mock the rule returned after update; not used for this test directly but + // needed so that the patchRule method does not throw rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); - (readRules as jest.Mock).mockResolvedValueOnce(existingRule); await detectionRulesClient.updateRule({ ruleUpdate }); @@ -169,8 +157,13 @@ describe('DetectionRulesClient.updateRule', () => { expect(rulesClient.create).not.toHaveBeenCalled(); }); - describe('regression tests', () => { + describe('actions', () => { it("updates the rule's actions if provided", async () => { + // Mock the existing rule + const existingRule = getRulesSchemaMock(); + (getRuleByRuleId as jest.Mock).mockResolvedValueOnce(existingRule); + + // Mock the rule update const ruleUpdate = { ...getCreateRulesSchemaMock(), actions: [ @@ -184,8 +177,9 @@ describe('DetectionRulesClient.updateRule', () => { }, ], }; - const existingRule = getRuleMock(getQueryRuleParams()); - (readRules as jest.Mock).mockResolvedValueOnce(existingRule); + + // Mock the rule returned after update; not used for this test directly but + // needed so that the patchRule method does not throw rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); await detectionRulesClient.updateRule({ ruleUpdate }); @@ -210,12 +204,12 @@ describe('DetectionRulesClient.updateRule', () => { }); it('updates actions to empty if none are specified', async () => { - const ruleUpdate = getCreateRulesSchemaMock(); - delete ruleUpdate.actions; - const existingRule = getRuleMock(getQueryRuleParams()); + // Mock the existing rule + const existingRule = getRulesSchemaMock(); + (getRuleByRuleId as jest.Mock).mockResolvedValueOnce(existingRule); existingRule.actions = [ { - actionTypeId: '.slack', + action_type_id: '.slack', id: '2933e581-d81c-4fe3-88fe-c57c6b8a5bfd', params: { message: 'Rule {{context.rule.name}} generated {{state.signals_count}} signals', @@ -223,8 +217,14 @@ describe('DetectionRulesClient.updateRule', () => { group: 'default', }, ]; + + // Mock the rule update + const ruleUpdate = getCreateRulesSchemaMock(); + delete ruleUpdate.actions; + + // Mock the rule returned after update; not used for this test directly but + // needed so that the patchRule method does not throw rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); - (readRules as jest.Mock).mockResolvedValueOnce(existingRule); await detectionRulesClient.updateRule({ ruleUpdate }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.upgrade_prebuilt_rule.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.upgrade_prebuilt_rule.test.ts index 38f3507d2f7a..7a5ae7637153 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.upgrade_prebuilt_rule.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client.upgrade_prebuilt_rule.test.ts @@ -10,21 +10,22 @@ import { rulesClientMock } from '@kbn/alerting-plugin/server/mocks'; import { getCreateEqlRuleSchemaMock, getCreateRulesSchemaMock, + getRulesEqlSchemaMock, + getRulesSchemaMock, } from '../../../../../../common/api/detection_engine/model/rule_schema/mocks'; import type { PrebuiltRuleAsset } from '../../../prebuilt_rules'; - -import { readRules } from './read_rules'; +import { getRuleByRuleId } from './methods/get_rule_by_rule_id'; import { getRuleMock } from '../../../routes/__mocks__/request_responses'; import { getEqlRuleParams, getQueryRuleParams } from '../../../rule_schema/mocks'; - import { buildMlAuthz } from '../../../../machine_learning/authz'; import { throwAuthzError } from '../../../../machine_learning/validation'; import { createDetectionRulesClient } from './detection_rules_client'; import type { IDetectionRulesClient } from './detection_rules_client_interface'; +import { savedObjectsClientMock } from '@kbn/core/server/mocks'; jest.mock('../../../../machine_learning/authz'); jest.mock('../../../../machine_learning/validation'); -jest.mock('./read_rules'); +jest.mock('./methods/get_rule_by_rule_id'); describe('DetectionRulesClient.upgradePrebuiltRule', () => { let rulesClient: ReturnType; @@ -34,7 +35,8 @@ describe('DetectionRulesClient.upgradePrebuiltRule', () => { beforeEach(() => { rulesClient = rulesClientMock.create(); - detectionRulesClient = createDetectionRulesClient(rulesClient, mlAuthz); + const savedObjectsClient = savedObjectsClientMock.create(); + detectionRulesClient = createDetectionRulesClient({ rulesClient, mlAuthz, savedObjectsClient }); }); it('throws if no matching rule_id is found', async () => { @@ -44,7 +46,7 @@ describe('DetectionRulesClient.upgradePrebuiltRule', () => { rule_id: 'rule-id', }; - (readRules as jest.Mock).mockResolvedValue(null); + (getRuleByRuleId as jest.Mock).mockResolvedValue(null); await expect(detectionRulesClient.upgradePrebuiltRule({ ruleAsset })).rejects.toThrow( `Failed to find rule ${ruleAsset.rule_id}` ); @@ -80,28 +82,24 @@ describe('DetectionRulesClient.upgradePrebuiltRule', () => { rule_id: 'rule-id', }; // Installed version is "query" - const installedRule = getRuleMock({ - ...getQueryRuleParams({ - exceptionsList: [ - { id: 'test_id', list_id: 'hi', type: 'detection', namespace_type: 'agnostic' }, - ], - }), - actions: [ - { - group: 'default', - id: 'test_id', - action_type_id: '.index', - config: { - index: ['index-1', 'index-2'], - }, - }, - ], - ruleId: 'rule-id', - }); + const installedRule = getRulesSchemaMock(); + installedRule.exceptions_list = [ + { id: 'test_id', list_id: 'hi', type: 'detection', namespace_type: 'agnostic' }, + ]; + installedRule.actions = [ + { + group: 'default', + id: 'test_id', + action_type_id: '.index', + params: {}, + }, + ]; + installedRule.rule_id = 'rule-id'; + beforeEach(() => { jest.resetAllMocks(); rulesClient.create.mockResolvedValue(getRuleMock(getQueryRuleParams())); - (readRules as jest.Mock).mockResolvedValue(installedRule); + (getRuleByRuleId as jest.Mock).mockResolvedValue(installedRule); }); it('deletes the old rule', async () => { @@ -117,16 +115,23 @@ describe('DetectionRulesClient.upgradePrebuiltRule', () => { name: ruleAsset.name, tags: ruleAsset.tags, // enabled and actions are kept from original rule - actions: installedRule.actions, + actions: [ + expect.objectContaining({ + actionTypeId: '.index', + group: 'default', + id: 'test_id', + params: {}, + }), + ], enabled: installedRule.enabled, params: expect.objectContaining({ index: ruleAsset.index, description: ruleAsset.description, immutable: true, // exceptions_lists, actions, timeline_id and timeline_title are maintained - timelineTitle: installedRule.params.timelineTitle, - timelineId: installedRule.params.timelineId, - exceptionsList: installedRule.params.exceptionsList, + timelineTitle: installedRule.timeline_title, + timelineId: installedRule.timeline_id, + exceptionsList: installedRule.exceptions_list, }), }), options: { @@ -147,11 +152,9 @@ describe('DetectionRulesClient.upgradePrebuiltRule', () => { rule_id: 'rule-id', }; // Installed version is "eql" - const installedRule = getRuleMock({ - ...getEqlRuleParams(), - }); + const installedRule = getRulesEqlSchemaMock(); beforeEach(() => { - (readRules as jest.Mock).mockResolvedValue(installedRule); + (getRuleByRuleId as jest.Mock).mockResolvedValue(installedRule); }); it('patches the existing rule with the new params from the rule asset', async () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client_interface.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client_interface.ts index 34c39153206b..d7b45f83e8bf 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client_interface.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client_interface.ts @@ -38,7 +38,7 @@ export interface UpdateRuleArgs { } export interface PatchRuleArgs { - nextParams: RulePatchProps; + rulePatch: RulePatchProps; } export interface DeleteRuleArgs { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_defaults.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_defaults.ts new file mode 100644 index 000000000000..837df0b3b2f1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_defaults.ts @@ -0,0 +1,185 @@ +/* + * 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 { v4 as uuidv4 } from 'uuid'; +import type { + RuleCreateProps, + RuleSource, + TypeSpecificCreateProps, +} from '../../../../../../../common/api/detection_engine/model/rule_schema'; +import { + DEFAULT_INDICATOR_SOURCE_PATH, + DEFAULT_MAX_SIGNALS, +} from '../../../../../../../common/constants'; +import { + normalizeMachineLearningJobIds, + normalizeThresholdObject, +} from '../../../../../../../common/detection_engine/utils'; +import { assertUnreachable } from '../../../../../../../common/utility_types'; +import { addEcsToRequiredFields } from '../../../utils/utils'; + +export const RULE_DEFAULTS = { + enabled: false, + risk_score_mapping: [], + severity_mapping: [], + interval: '5m' as const, + to: 'now' as const, + from: 'now-6m' as const, + exceptions_list: [], + false_positives: [], + max_signals: DEFAULT_MAX_SIGNALS, + actions: [], + related_integrations: [], + required_fields: [], + setup: '', + references: [], + threat: [], + tags: [], + author: [], + output_index: '', + version: 1, +}; + +export function applyRuleDefaults(rule: RuleCreateProps & { immutable?: boolean }) { + const typeSpecificParams = setTypeSpecificDefaults(rule); + const immutable = rule.immutable ?? false; + + return { + ...RULE_DEFAULTS, + ...rule, + ...typeSpecificParams, + rule_id: rule.rule_id ?? uuidv4(), + immutable, + rule_source: convertImmutableToRuleSource(immutable), + required_fields: addEcsToRequiredFields(rule.required_fields), + }; +} + +const convertImmutableToRuleSource = (immutable: boolean): RuleSource => { + if (immutable) { + return { + type: 'external', + is_customized: false, + }; + } + + return { + type: 'internal', + }; +}; + +export const setTypeSpecificDefaults = (props: TypeSpecificCreateProps) => { + switch (props.type) { + case 'eql': { + return { + type: props.type, + language: props.language, + index: props.index, + data_view_id: props.data_view_id, + query: props.query, + filters: props.filters, + timestamp_field: props.timestamp_field, + event_category_override: props.event_category_override, + tiebreaker_field: props.tiebreaker_field, + alert_suppression: props.alert_suppression, + }; + } + case 'esql': { + return { + type: props.type, + language: props.language, + query: props.query, + alert_suppression: props.alert_suppression, + }; + } + case 'threat_match': { + return { + type: props.type, + language: props.language ?? 'kuery', + index: props.index, + data_view_id: props.data_view_id, + query: props.query, + filters: props.filters, + saved_id: props.saved_id, + threat_filters: props.threat_filters, + threat_query: props.threat_query, + threat_mapping: props.threat_mapping, + threat_language: props.threat_language, + threat_index: props.threat_index, + threat_indicator_path: props.threat_indicator_path ?? DEFAULT_INDICATOR_SOURCE_PATH, + concurrent_searches: props.concurrent_searches, + items_per_search: props.items_per_search, + alert_suppression: props.alert_suppression, + }; + } + case 'query': { + return { + type: props.type, + language: props.language ?? 'kuery', + index: props.index, + data_view_id: props.data_view_id, + query: props.query ?? '', + filters: props.filters, + saved_id: props.saved_id, + response_actions: props.response_actions, + alert_suppression: props.alert_suppression, + }; + } + case 'saved_query': { + return { + type: props.type, + language: props.language ?? 'kuery', + index: props.index, + query: props.query, + filters: props.filters, + saved_id: props.saved_id, + data_view_id: props.data_view_id, + response_actions: props.response_actions, + alert_suppression: props.alert_suppression, + }; + } + case 'threshold': { + return { + type: props.type, + language: props.language ?? 'kuery', + index: props.index, + data_view_id: props.data_view_id, + query: props.query, + filters: props.filters, + saved_id: props.saved_id, + threshold: normalizeThresholdObject(props.threshold), + alert_suppression: props.alert_suppression?.duration + ? { duration: props.alert_suppression.duration } + : undefined, + }; + } + case 'machine_learning': { + return { + type: props.type, + anomaly_threshold: props.anomaly_threshold, + machine_learning_job_id: normalizeMachineLearningJobIds(props.machine_learning_job_id), + alert_suppression: props.alert_suppression, + }; + } + case 'new_terms': { + return { + type: props.type, + query: props.query, + new_terms_fields: props.new_terms_fields, + history_window_start: props.history_window_start, + index: props.index, + filters: props.filters, + language: props.language ?? 'kuery', + data_view_id: props.data_view_id, + alert_suppression: props.alert_suppression, + }; + } + default: { + return assertUnreachable(props); + } + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_patch.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_patch.test.ts new file mode 100644 index 000000000000..49592aff28f9 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_patch.test.ts @@ -0,0 +1,456 @@ +/* + * 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 { + AlertSuppressionDuration, + PatchRuleRequestBody, +} from '../../../../../../../common/api/detection_engine'; +import { + getEsqlRuleSchemaMock, + getRulesEqlSchemaMock, + getRulesMlSchemaMock, + getRulesNewTermsSchemaMock, + getRulesSchemaMock, + getRulesThresholdSchemaMock, + getSavedQuerySchemaMock, + getThreatMatchingSchemaMock, +} from '../../../../../../../common/api/detection_engine/model/rule_schema/mocks'; +import { createPrebuiltRuleAssetsClient } from '../../../../prebuilt_rules/logic/rule_assets/__mocks__/prebuilt_rule_assets_client'; +import { applyRulePatch } from './apply_rule_patch'; + +const prebuiltRuleAssetClient = createPrebuiltRuleAssetsClient(); + +describe('applyRulePatch', () => { + describe('EQL', () => { + test('should accept EQL params when existing rule type is EQL', async () => { + const rulePatch = { + timestamp_field: 'event.created', + event_category_override: 'event.not_category', + tiebreaker_field: 'event.created', + }; + const existingRule = getRulesEqlSchemaMock(); + const patchedRule = await applyRulePatch({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, + }); + expect(patchedRule).toEqual( + expect.objectContaining({ + timestamp_field: 'event.created', + event_category_override: 'event.not_category', + tiebreaker_field: 'event.created', + }) + ); + }); + test('should accept EQL params with suppression in snake case and convert to camel case when rule type is EQL', async () => { + const rulePatch = { + timestamp_field: 'event.created', + event_category_override: 'event.not_category', + tiebreaker_field: 'event.created', + alert_suppression: { + group_by: ['event.type'], + duration: { + value: 10, + unit: 'm', + } as AlertSuppressionDuration, + missing_fields_strategy: 'suppress', + }, + }; + const existingRule = getRulesEqlSchemaMock(); + const patchedRule = await applyRulePatch({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, + }); + expect(patchedRule).toEqual( + expect.objectContaining({ + timestamp_field: 'event.created', + event_category_override: 'event.not_category', + tiebreaker_field: 'event.created', + alert_suppression: { + group_by: ['event.type'], + duration: { + value: 10, + unit: 'm', + }, + missing_fields_strategy: 'suppress', + }, + }) + ); + }); + test('should reject invalid EQL params when existing rule type is EQL', async () => { + const rulePatch = { + timestamp_field: 1, + event_category_override: 1, + tiebreaker_field: 1, + } as PatchRuleRequestBody; + const existingRule = getRulesEqlSchemaMock(); + await expect( + applyRulePatch({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, + }) + ).rejects.toThrowError( + 'event_category_override: Expected string, received number, tiebreaker_field: Expected string, received number, timestamp_field: Expected string, received number' + ); + }); + test('should reject EQL params with invalid suppression group_by field', async () => { + const rulePatch = { + timestamp_field: 'event.created', + event_category_override: 'event.not_category', + tiebreaker_field: 'event.created', + alert_suppression: { + group_by: 'event.type', + duration: { + value: 10, + unit: 'm', + } as AlertSuppressionDuration, + missing_fields_strategy: 'suppress', + }, + }; + const existingRule = getRulesEqlSchemaMock(); + await expect( + applyRulePatch({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, + }) + ).rejects.toThrowError('alert_suppression.group_by: Expected array, received string'); + }); + }); + + test('should accept threat match params when existing rule type is threat match', async () => { + const rulePatch = { + threat_indicator_path: 'my.indicator', + threat_query: 'test-query', + }; + const existingRule = getThreatMatchingSchemaMock(); + const patchedRule = await applyRulePatch({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, + }); + expect(patchedRule).toEqual( + expect.objectContaining({ + threat_indicator_path: 'my.indicator', + threat_query: 'test-query', + }) + ); + }); + + test('should reject invalid threat match params when existing rule type is threat match', async () => { + const rulePatch = { + threat_indicator_path: 1, + threat_query: 1, + } as PatchRuleRequestBody; + const existingRule = getThreatMatchingSchemaMock(); + await expect( + applyRulePatch({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, + }) + ).rejects.toThrowError( + 'threat_query: Expected string, received number, threat_indicator_path: Expected string, received number' + ); + }); + + test('should accept query params when existing rule type is query', async () => { + const rulePatch = { + index: ['new-test-index'], + language: 'lucene', + } as PatchRuleRequestBody; + const existingRule = getRulesSchemaMock(); + const patchedRule = await applyRulePatch({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, + }); + expect(patchedRule).toEqual( + expect.objectContaining({ + index: ['new-test-index'], + language: 'lucene', + }) + ); + }); + + test('should reject invalid query params when existing rule type is query', async () => { + const rulePatch = { + index: [1], + language: 'non-language', + } as PatchRuleRequestBody; + const existingRule = getRulesSchemaMock(); + await expect( + applyRulePatch({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, + }) + ).rejects.toThrowError( + "index.0: Expected string, received number, language: Invalid enum value. Expected 'kuery' | 'lucene', received 'non-language'" + ); + }); + + test('should accept saved query params when existing rule type is saved query', async () => { + const rulePatch = { + index: ['new-test-index'], + language: 'lucene', + } as PatchRuleRequestBody; + const existingRule = getSavedQuerySchemaMock(); + const patchedRule = await applyRulePatch({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, + }); + expect(patchedRule).toEqual( + expect.objectContaining({ + index: ['new-test-index'], + language: 'lucene', + }) + ); + }); + + test('should reject invalid saved query params when existing rule type is saved query', async () => { + const rulePatch = { + index: [1], + language: 'non-language', + } as PatchRuleRequestBody; + const existingRule = getSavedQuerySchemaMock(); + await expect( + applyRulePatch({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, + }) + ).rejects.toThrowError( + "index.0: Expected string, received number, language: Invalid enum value. Expected 'kuery' | 'lucene', received 'non-language'" + ); + }); + + test('should accept threshold params when existing rule type is threshold', async () => { + const rulePatch = { + threshold: { + field: ['host.name'], + value: 107, + }, + }; + const existingRule = getRulesThresholdSchemaMock(); + const patchedRule = await applyRulePatch({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, + }); + expect(patchedRule).toEqual( + expect.objectContaining({ + threshold: { + field: ['host.name'], + value: 107, + }, + }) + ); + }); + + test('should reject invalid threshold params when existing rule type is threshold', async () => { + const rulePatch = { + threshold: { + field: ['host.name'], + value: 'invalid', + }, + } as PatchRuleRequestBody; + const existingRule = getRulesThresholdSchemaMock(); + await expect( + applyRulePatch({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, + }) + ).rejects.toThrowError('threshold.value: Expected number, received string'); + }); + + test('should accept ES|QL alerts suppression params', async () => { + const rulePatch = { + alert_suppression: { + group_by: ['agent.name'], + duration: { value: 4, unit: 'h' as const }, + missing_fields_strategy: 'doNotSuppress' as const, + }, + }; + const existingRule = getEsqlRuleSchemaMock(); + const patchedRule = await applyRulePatch({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, + }); + expect(patchedRule).toEqual( + expect.objectContaining({ + alert_suppression: { + group_by: ['agent.name'], + missing_fields_strategy: 'doNotSuppress', + duration: { value: 4, unit: 'h' }, + }, + }) + ); + }); + + test('should accept threshold alerts suppression params', async () => { + const rulePatch = { + alert_suppression: { + duration: { value: 4, unit: 'h' as const }, + }, + }; + const existingRule = getRulesThresholdSchemaMock(); + const patchedRule = await applyRulePatch({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, + }); + expect(patchedRule).toEqual( + expect.objectContaining({ + alert_suppression: { + duration: { value: 4, unit: 'h' }, + }, + }) + ); + }); + + test('should accept threat_match alerts suppression params', async () => { + const rulePatch = { + alert_suppression: { + group_by: ['agent.name'], + missing_fields_strategy: 'suppress' as const, + }, + }; + const existingRule = getThreatMatchingSchemaMock(); + const patchedRule = await applyRulePatch({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, + }); + expect(patchedRule).toEqual( + expect.objectContaining({ + alert_suppression: { + group_by: ['agent.name'], + missing_fields_strategy: 'suppress', + }, + }) + ); + }); + + test('should accept new_terms alerts suppression params', async () => { + const rulePatch = { + alert_suppression: { + group_by: ['agent.name'], + duration: { value: 4, unit: 'h' as const }, + missing_fields_strategy: 'suppress' as const, + }, + }; + const existingRule = getRulesNewTermsSchemaMock(); + const patchedRule = await applyRulePatch({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, + }); + expect(patchedRule).toEqual( + expect.objectContaining({ + alert_suppression: { + group_by: ['agent.name'], + missing_fields_strategy: 'suppress', + duration: { value: 4, unit: 'h' }, + }, + }) + ); + }); + + describe('machine learning rules', () => { + test('should accept machine learning params when existing rule type is machine learning', async () => { + const rulePatch = { + anomaly_threshold: 5, + }; + const existingRule = getRulesMlSchemaMock(); + const patchedRule = await applyRulePatch({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, + }); + expect(patchedRule).toEqual( + expect.objectContaining({ + anomaly_threshold: 5, + }) + ); + }); + + test('should reject invalid machine learning params when existing rule type is machine learning', async () => { + const rulePatch = { + anomaly_threshold: 'invalid', + } as PatchRuleRequestBody; + const existingRule = getRulesMlSchemaMock(); + await expect( + applyRulePatch({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, + }) + ).rejects.toThrowError('anomaly_threshold: Expected number, received string'); + }); + + it('accepts suppression params', async () => { + const rulePatch = { + alert_suppression: { + group_by: ['agent.name'], + missing_fields_strategy: 'suppress' as const, + }, + }; + const existingRule = getRulesMlSchemaMock(); + const patchedRule = await applyRulePatch({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, + }); + + expect(patchedRule).toEqual( + expect.objectContaining({ + alert_suppression: { + group_by: ['agent.name'], + missing_fields_strategy: 'suppress', + }, + }) + ); + }); + }); + + test('should accept new terms params when existing rule type is new terms', async () => { + const rulePatch = { + new_terms_fields: ['event.new_field'], + }; + const existingRule = getRulesNewTermsSchemaMock(); + const patchedRule = await applyRulePatch({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, + }); + expect(patchedRule).toEqual( + expect.objectContaining({ + new_terms_fields: ['event.new_field'], + }) + ); + }); + + test('should reject invalid new terms params when existing rule type is new terms', async () => { + const rulePatch = { + new_terms_fields: 'invalid', + } as PatchRuleRequestBody; + const existingRule = getRulesNewTermsSchemaMock(); + await expect( + applyRulePatch({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, + }) + ).rejects.toThrowError('new_terms_fields: Expected array, received string'); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_patch.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_patch.ts new file mode 100644 index 000000000000..9d02cd8dbb9d --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_patch.ts @@ -0,0 +1,334 @@ +/* + * 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 { BadRequestError } from '@kbn/securitysolution-es-utils'; +import { stringifyZodError } from '@kbn/zod-helpers'; +import type { + EqlRule, + EqlRuleResponseFields, + EsqlRule, + EsqlRuleResponseFields, + MachineLearningRule, + MachineLearningRuleResponseFields, + NewTermsRule, + NewTermsRuleResponseFields, + QueryRule, + QueryRuleResponseFields, + RuleResponse, + SavedQueryRule, + SavedQueryRuleResponseFields, + ThreatMatchRule, + ThreatMatchRuleResponseFields, + ThresholdRule, + ThresholdRuleResponseFields, + TypeSpecificResponse, +} from '../../../../../../../common/api/detection_engine/model/rule_schema'; +import { + EqlRulePatchFields, + EsqlRulePatchFields, + MachineLearningRulePatchFields, + NewTermsRulePatchFields, + QueryRulePatchFields, + SavedQueryRulePatchFields, + ThreatMatchRulePatchFields, + ThresholdRulePatchFields, +} from '../../../../../../../common/api/detection_engine/model/rule_schema'; +import type { PatchRuleRequestBody } from '../../../../../../../common/api/detection_engine/rule_management'; +import { + normalizeMachineLearningJobIds, + normalizeThresholdObject, +} from '../../../../../../../common/detection_engine/utils'; +import { assertUnreachable } from '../../../../../../../common/utility_types'; +import type { IPrebuiltRuleAssetsClient } from '../../../../prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client'; +import { addEcsToRequiredFields } from '../../../utils/utils'; +import { calculateRuleSource } from './rule_source/calculate_rule_source'; + +interface ApplyRulePatchProps { + prebuiltRuleAssetClient: IPrebuiltRuleAssetsClient; + existingRule: RuleResponse; + rulePatch: PatchRuleRequestBody; +} + +// eslint-disable-next-line complexity +export const applyRulePatch = async ({ + rulePatch, + existingRule, + prebuiltRuleAssetClient, +}: ApplyRulePatchProps): Promise => { + const typeSpecificParams = patchTypeSpecificParams(rulePatch, existingRule); + + const nextRule: RuleResponse = { + // Keep existing values for these fields + id: existingRule.id, + rule_id: existingRule.rule_id, + revision: existingRule.revision, + immutable: existingRule.immutable, + rule_source: existingRule.rule_source, + updated_at: existingRule.updated_at, + updated_by: existingRule.updated_by, + created_at: existingRule.created_at, + created_by: existingRule.created_by, + + // Update values for these fields + enabled: rulePatch.enabled ?? existingRule.enabled, + name: rulePatch.name ?? existingRule.name, + tags: rulePatch.tags ?? existingRule.tags, + author: rulePatch.author ?? existingRule.author, + building_block_type: rulePatch.building_block_type ?? existingRule.building_block_type, + description: rulePatch.description ?? existingRule.description, + false_positives: rulePatch.false_positives ?? existingRule.false_positives, + investigation_fields: rulePatch.investigation_fields ?? existingRule.investigation_fields, + from: rulePatch.from ?? existingRule.from, + license: rulePatch.license ?? existingRule.license, + output_index: rulePatch.output_index ?? existingRule.output_index, + timeline_id: rulePatch.timeline_id ?? existingRule.timeline_id, + timeline_title: rulePatch.timeline_title ?? existingRule.timeline_title, + meta: rulePatch.meta ?? existingRule.meta, + max_signals: rulePatch.max_signals ?? existingRule.max_signals, + related_integrations: rulePatch.related_integrations ?? existingRule.related_integrations, + required_fields: addEcsToRequiredFields(rulePatch.required_fields), + risk_score: rulePatch.risk_score ?? existingRule.risk_score, + risk_score_mapping: rulePatch.risk_score_mapping ?? existingRule.risk_score_mapping, + rule_name_override: rulePatch.rule_name_override ?? existingRule.rule_name_override, + setup: rulePatch.setup ?? existingRule.setup, + severity: rulePatch.severity ?? existingRule.severity, + severity_mapping: rulePatch.severity_mapping ?? existingRule.severity_mapping, + threat: rulePatch.threat ?? existingRule.threat, + timestamp_override: rulePatch.timestamp_override ?? existingRule.timestamp_override, + timestamp_override_fallback_disabled: + rulePatch.timestamp_override_fallback_disabled ?? + existingRule.timestamp_override_fallback_disabled, + to: rulePatch.to ?? existingRule.to, + references: rulePatch.references ?? existingRule.references, + namespace: rulePatch.namespace ?? existingRule.namespace, + note: rulePatch.note ?? existingRule.note, + version: rulePatch.version ?? existingRule.version, + exceptions_list: rulePatch.exceptions_list ?? existingRule.exceptions_list, + interval: rulePatch.interval ?? existingRule.interval, + throttle: rulePatch.throttle ?? existingRule.throttle, + actions: rulePatch.actions ?? existingRule.actions, + ...typeSpecificParams, + }; + + nextRule.rule_source = await calculateRuleSource({ + rule: nextRule, + prebuiltRuleAssetClient, + }); + + return nextRule; +}; + +const patchEqlParams = ( + rulePatch: EqlRulePatchFields, + existingRule: EqlRule +): EqlRuleResponseFields => { + return { + type: existingRule.type, + language: rulePatch.language ?? existingRule.language, + index: rulePatch.index ?? existingRule.index, + data_view_id: rulePatch.data_view_id ?? existingRule.data_view_id, + query: rulePatch.query ?? existingRule.query, + filters: rulePatch.filters ?? existingRule.filters, + timestamp_field: rulePatch.timestamp_field ?? existingRule.timestamp_field, + event_category_override: + rulePatch.event_category_override ?? existingRule.event_category_override, + tiebreaker_field: rulePatch.tiebreaker_field ?? existingRule.tiebreaker_field, + alert_suppression: rulePatch.alert_suppression ?? existingRule.alert_suppression, + }; +}; + +const patchEsqlParams = ( + rulePatch: EsqlRulePatchFields, + existingRule: EsqlRule +): EsqlRuleResponseFields => { + return { + type: existingRule.type, + language: rulePatch.language ?? existingRule.language, + query: rulePatch.query ?? existingRule.query, + alert_suppression: rulePatch.alert_suppression ?? existingRule.alert_suppression, + }; +}; + +const patchThreatMatchParams = ( + rulePatch: ThreatMatchRulePatchFields, + existingRule: ThreatMatchRule +): ThreatMatchRuleResponseFields => { + return { + type: existingRule.type, + language: rulePatch.language ?? existingRule.language, + index: rulePatch.index ?? existingRule.index, + data_view_id: rulePatch.data_view_id ?? existingRule.data_view_id, + query: rulePatch.query ?? existingRule.query, + filters: rulePatch.filters ?? existingRule.filters, + saved_id: rulePatch.saved_id ?? existingRule.saved_id, + threat_filters: rulePatch.threat_filters ?? existingRule.threat_filters, + threat_query: rulePatch.threat_query ?? existingRule.threat_query, + threat_mapping: rulePatch.threat_mapping ?? existingRule.threat_mapping, + threat_language: rulePatch.threat_language ?? existingRule.threat_language, + threat_index: rulePatch.threat_index ?? existingRule.threat_index, + threat_indicator_path: rulePatch.threat_indicator_path ?? existingRule.threat_indicator_path, + concurrent_searches: rulePatch.concurrent_searches ?? existingRule.concurrent_searches, + items_per_search: rulePatch.items_per_search ?? existingRule.items_per_search, + alert_suppression: rulePatch.alert_suppression ?? existingRule.alert_suppression, + }; +}; + +const patchQueryParams = ( + rulePatch: QueryRulePatchFields, + existingRule: QueryRule +): QueryRuleResponseFields => { + return { + type: existingRule.type, + language: rulePatch.language ?? existingRule.language, + index: rulePatch.index ?? existingRule.index, + data_view_id: rulePatch.data_view_id ?? existingRule.data_view_id, + query: rulePatch.query ?? existingRule.query, + filters: rulePatch.filters ?? existingRule.filters, + saved_id: rulePatch.saved_id ?? existingRule.saved_id, + response_actions: rulePatch.response_actions ?? existingRule.response_actions, + alert_suppression: rulePatch.alert_suppression ?? existingRule.alert_suppression, + }; +}; + +const patchSavedQueryParams = ( + rulePatch: SavedQueryRulePatchFields, + existingRule: SavedQueryRule +): SavedQueryRuleResponseFields => { + return { + type: existingRule.type, + language: rulePatch.language ?? existingRule.language, + index: rulePatch.index ?? existingRule.index, + data_view_id: rulePatch.data_view_id ?? existingRule.data_view_id, + query: rulePatch.query ?? existingRule.query, + filters: rulePatch.filters ?? existingRule.filters, + saved_id: rulePatch.saved_id ?? existingRule.saved_id, + response_actions: rulePatch.response_actions ?? existingRule.response_actions, + alert_suppression: rulePatch.alert_suppression ?? existingRule.alert_suppression, + }; +}; + +const patchThresholdParams = ( + rulePatch: ThresholdRulePatchFields, + existingRule: ThresholdRule +): ThresholdRuleResponseFields => { + return { + type: existingRule.type, + language: rulePatch.language ?? existingRule.language, + index: rulePatch.index ?? existingRule.index, + data_view_id: rulePatch.data_view_id ?? existingRule.data_view_id, + query: rulePatch.query ?? existingRule.query, + filters: rulePatch.filters ?? existingRule.filters, + saved_id: rulePatch.saved_id ?? existingRule.saved_id, + threshold: rulePatch.threshold + ? normalizeThresholdObject(rulePatch.threshold) + : existingRule.threshold, + alert_suppression: rulePatch.alert_suppression ?? existingRule.alert_suppression, + }; +}; + +const patchMachineLearningParams = ( + params: MachineLearningRulePatchFields, + existingRule: MachineLearningRule +): MachineLearningRuleResponseFields => { + return { + type: existingRule.type, + anomaly_threshold: params.anomaly_threshold ?? existingRule.anomaly_threshold, + machine_learning_job_id: params.machine_learning_job_id + ? normalizeMachineLearningJobIds(params.machine_learning_job_id) + : existingRule.machine_learning_job_id, + alert_suppression: params.alert_suppression ?? existingRule.alert_suppression, + }; +}; + +const patchNewTermsParams = ( + params: NewTermsRulePatchFields, + existingRule: NewTermsRule +): NewTermsRuleResponseFields => { + return { + type: existingRule.type, + language: params.language ?? existingRule.language, + index: params.index ?? existingRule.index, + data_view_id: params.data_view_id ?? existingRule.data_view_id, + query: params.query ?? existingRule.query, + filters: params.filters ?? existingRule.filters, + new_terms_fields: params.new_terms_fields ?? existingRule.new_terms_fields, + history_window_start: params.history_window_start ?? existingRule.history_window_start, + alert_suppression: params.alert_suppression ?? existingRule.alert_suppression, + }; +}; + +export const patchTypeSpecificParams = ( + params: PatchRuleRequestBody, + existingRule: RuleResponse +): TypeSpecificResponse => { + // Here we do the validation of patch params by rule type to ensure that the fields that are + // passed in to patch are of the correct type, e.g. `query` is a string. Since the combined patch schema + // is a union of types where everything is optional, it's hard to do the validation before we know the rule type - + // a patch request that defines `event_category_override` as a number would not be assignable to the EQL patch schema, + // but would be assignable to the other rule types since they don't specify `event_category_override`. + switch (existingRule.type) { + case 'eql': { + const result = EqlRulePatchFields.safeParse(params); + if (!result.success) { + throw new BadRequestError(stringifyZodError(result.error)); + } + return patchEqlParams(result.data, existingRule); + } + case 'esql': { + const result = EsqlRulePatchFields.safeParse(params); + if (!result.success) { + throw new BadRequestError(stringifyZodError(result.error)); + } + return patchEsqlParams(result.data, existingRule); + } + case 'threat_match': { + const result = ThreatMatchRulePatchFields.safeParse(params); + if (!result.success) { + throw new BadRequestError(stringifyZodError(result.error)); + } + return patchThreatMatchParams(result.data, existingRule); + } + case 'query': { + const result = QueryRulePatchFields.safeParse(params); + if (!result.success) { + throw new BadRequestError(stringifyZodError(result.error)); + } + return patchQueryParams(result.data, existingRule); + } + case 'saved_query': { + const result = SavedQueryRulePatchFields.safeParse(params); + if (!result.success) { + throw new BadRequestError(stringifyZodError(result.error)); + } + return patchSavedQueryParams(result.data, existingRule); + } + case 'threshold': { + const result = ThresholdRulePatchFields.safeParse(params); + if (!result.success) { + throw new BadRequestError(stringifyZodError(result.error)); + } + return patchThresholdParams(result.data, existingRule); + } + case 'machine_learning': { + const result = MachineLearningRulePatchFields.safeParse(params); + if (!result.success) { + throw new BadRequestError(stringifyZodError(result.error)); + } + return patchMachineLearningParams(result.data, existingRule); + } + case 'new_terms': { + const result = NewTermsRulePatchFields.safeParse(params); + if (!result.success) { + throw new BadRequestError(stringifyZodError(result.error)); + } + return patchNewTermsParams(result.data, existingRule); + } + default: { + return assertUnreachable(existingRule); + } + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_update.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_update.ts new file mode 100644 index 000000000000..b911e66a1fc4 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_update.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + RuleResponse, + RuleUpdateProps, +} from '../../../../../../../common/api/detection_engine/model/rule_schema'; +import type { IPrebuiltRuleAssetsClient } from '../../../../prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client'; +import { applyRuleDefaults } from './apply_rule_defaults'; +import { calculateRuleSource } from './rule_source/calculate_rule_source'; + +interface ApplyRuleUpdateProps { + prebuiltRuleAssetClient: IPrebuiltRuleAssetsClient; + existingRule: RuleResponse; + ruleUpdate: RuleUpdateProps; +} + +export const applyRuleUpdate = async ({ + prebuiltRuleAssetClient, + existingRule, + ruleUpdate, +}: ApplyRuleUpdateProps): Promise => { + const nextRule: RuleResponse = { + ...applyRuleDefaults(ruleUpdate), + + // Use existing values + enabled: ruleUpdate.enabled ?? existingRule.enabled, + version: ruleUpdate.version ?? existingRule.version, + + // Always keep existing values for these fields + id: existingRule.id, + rule_id: existingRule.rule_id, + revision: existingRule.revision, + immutable: existingRule.immutable, + rule_source: existingRule.rule_source, + updated_at: existingRule.updated_at, + updated_by: existingRule.updated_by, + created_at: existingRule.created_at, + created_by: existingRule.created_by, + }; + + nextRule.rule_source = await calculateRuleSource({ + rule: nextRule, + prebuiltRuleAssetClient, + }); + + return nextRule; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_is_customized.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_is_customized.ts new file mode 100644 index 000000000000..4f9bb4a060f6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_is_customized.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { RuleResponse } from '../../../../../../../../common/api/detection_engine'; +import { MissingVersion } from '../../../../../../../../common/api/detection_engine'; +import type { PrebuiltRuleAsset } from '../../../../../prebuilt_rules'; +import { calculateRuleFieldsDiff } from '../../../../../prebuilt_rules/logic/diff/calculation/calculate_rule_fields_diff'; +import { convertRuleToDiffable } from '../../../../../prebuilt_rules/logic/diff/normalization/convert_rule_to_diffable'; +import { convertPrebuiltRuleAssetToRuleResponse } from '../../converters/convert_prebuilt_rule_asset_to_rule_response'; + +export function calculateIsCustomized( + baseRule: PrebuiltRuleAsset | undefined, + nextRule: RuleResponse +) { + if (baseRule == null) { + // If the base version is missing, we consider the rule to be customized + return true; + } + + const baseRuleWithDefaults = convertPrebuiltRuleAssetToRuleResponse(baseRule); + + const fieldsDiff = calculateRuleFieldsDiff({ + base_version: MissingVersion, + current_version: convertRuleToDiffable(baseRuleWithDefaults), + target_version: convertRuleToDiffable(nextRule), + }); + + return Object.values(fieldsDiff).some((diff) => diff.has_update); +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_rule_source.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_rule_source.test.ts new file mode 100644 index 000000000000..e44c69d2705d --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_rule_source.test.ts @@ -0,0 +1,112 @@ +/* + * 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 { createPrebuiltRuleAssetsClient } from '../../../../../prebuilt_rules/logic/rule_assets/__mocks__/prebuilt_rule_assets_client'; +import { applyRuleDefaults } from '../apply_rule_defaults'; +import { calculateRuleSource } from './calculate_rule_source'; + +const prebuiltRuleAssetClient = createPrebuiltRuleAssetsClient(); + +const getSampleRuleAsset = () => { + return applyRuleDefaults({ + rule_id: 'test-rule-id', + name: 'Test rule', + description: 'Test description', + type: 'query', + query: 'user.name: root or user.name: admin', + severity: 'high', + risk_score: 55, + }); +}; + +const getSampleRule = () => { + return { + ...getSampleRuleAsset(), + id: 'test-rule-id', + updated_at: '2021-01-01T00:00:00Z', + updated_by: 'test-user', + created_at: '2021-01-01T00:00:00Z', + created_by: 'test-user', + revision: 1, + }; +}; + +describe('calculateRuleSource', () => { + it('returns an internal rule source when the rule is not prebuilt', async () => { + const rule = getSampleRule(); + rule.immutable = false; + + const result = await calculateRuleSource({ + prebuiltRuleAssetClient, + rule, + }); + expect(result).toEqual({ + type: 'internal', + }); + }); + + it('returns an external rule source with customized false when the rule is prebuilt', async () => { + const rule = getSampleRule(); + rule.immutable = true; + + const baseRule = getSampleRuleAsset(); + prebuiltRuleAssetClient.fetchAssetsByVersion.mockResolvedValueOnce([baseRule]); + + const result = await calculateRuleSource({ + prebuiltRuleAssetClient, + rule, + }); + expect(result).toEqual( + expect.objectContaining({ + type: 'external', + is_customized: false, + }) + ); + }); + + it('returns is_customized true when the rule is prebuilt and has been customized', async () => { + const rule = getSampleRule(); + rule.immutable = true; + rule.name = 'Updated name'; + + const baseRule = getSampleRuleAsset(); + prebuiltRuleAssetClient.fetchAssetsByVersion.mockResolvedValueOnce([baseRule]); + + const result = await calculateRuleSource({ + prebuiltRuleAssetClient, + rule, + }); + expect(result).toEqual( + expect.objectContaining({ + type: 'external', + is_customized: true, + }) + ); + }); + + it('returns is_customized false when the rule has only changes to revision, updated_at, updated_by', async () => { + const rule = getSampleRule(); + rule.immutable = true; + rule.revision = 5; + rule.updated_at = '2024-01-01T00:00:00Z'; + rule.updated_by = 'new-user'; + + const baseRule = getSampleRuleAsset(); + prebuiltRuleAssetClient.fetchAssetsByVersion.mockResolvedValueOnce([baseRule]); + + const result = await calculateRuleSource({ + prebuiltRuleAssetClient, + rule, + }); + expect(result).toEqual( + expect.objectContaining({ + type: 'external', + is_customized: false, + }) + ); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_rule_source.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_rule_source.ts new file mode 100644 index 000000000000..742cd20544a6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_rule_source.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + RuleResponse, + RuleSource, +} from '../../../../../../../../common/api/detection_engine/model/rule_schema'; +import type { PrebuiltRuleAsset } from '../../../../../prebuilt_rules'; +import type { IPrebuiltRuleAssetsClient } from '../../../../../prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client'; +import { calculateIsCustomized } from './calculate_is_customized'; + +interface CalculateRuleSourceProps { + prebuiltRuleAssetClient: IPrebuiltRuleAssetsClient; + rule: RuleResponse; +} + +export async function calculateRuleSource({ + prebuiltRuleAssetClient, + rule, +}: CalculateRuleSourceProps): Promise { + if (rule.immutable) { + // This is a prebuilt rule and, despite the name, they are not immutable. So + // we need to recalculate `ruleSource.isCustomized` based on the rule's contents. + const prebuiltRulesResponse = await prebuiltRuleAssetClient.fetchAssetsByVersion([ + { + rule_id: rule.rule_id, + version: rule.version, + }, + ]); + const baseRule: PrebuiltRuleAsset | undefined = prebuiltRulesResponse.at(0); + + const isCustomized = calculateIsCustomized(baseRule, rule); + + return { + type: 'external', + is_customized: isCustomized, + }; + } + + return { + type: 'internal', + }; +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/__mocks__/get_rule_by_rule_id.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/__mocks__/get_rule_by_rule_id.ts new file mode 100644 index 000000000000..251cd7f69919 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/__mocks__/get_rule_by_rule_id.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 { RuleResponse } from '../../../../../../../../common/api/detection_engine'; +import { getRulesSchemaMock } from '../../../../../../../../common/api/detection_engine/model/rule_schema/rule_response_schema.mock'; + +export const getRuleByRuleId = jest + .fn() + .mockImplementation(async (): Promise => getRulesSchemaMock()); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/create_custom_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/create_custom_rule.ts deleted file mode 100644 index 963cac7e10dd..000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/create_custom_rule.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { RulesClient } from '@kbn/alerting-plugin/server'; -import { stringifyZodError } from '@kbn/zod-helpers'; -import type { CreateCustomRuleArgs } from '../detection_rules_client_interface'; -import type { MlAuthz } from '../../../../../machine_learning/authz'; -import type { RuleParams } from '../../../../rule_schema'; -import { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema'; -import { - convertCreateAPIToInternalSchema, - internalRuleToAPIResponse, -} from '../../../normalization/rule_converters'; -import { validateMlAuth, RuleResponseValidationError } from '../utils'; - -export const createCustomRule = async ( - rulesClient: RulesClient, - args: CreateCustomRuleArgs, - mlAuthz: MlAuthz -): Promise => { - const { params } = args; - await validateMlAuth(mlAuthz, params.type); - - const internalRule = convertCreateAPIToInternalSchema(params, { immutable: false }); - const rule = await rulesClient.create({ - data: internalRule, - }); - - /* Trying to convert the rule to a RuleResponse object */ - const parseResult = RuleResponse.safeParse(internalRuleToAPIResponse(rule)); - - if (!parseResult.success) { - throw new RuleResponseValidationError({ - message: stringifyZodError(parseResult.error), - ruleId: rule.params.ruleId, - }); - } - - return parseResult.data; -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/create_prebuilt_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/create_prebuilt_rule.ts deleted file mode 100644 index 0f0a4aea12d7..000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/create_prebuilt_rule.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { RulesClient } from '@kbn/alerting-plugin/server'; -import { stringifyZodError } from '@kbn/zod-helpers'; -import type { CreatePrebuiltRuleArgs } from '../detection_rules_client_interface'; -import type { MlAuthz } from '../../../../../machine_learning/authz'; -import { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema'; -import type { RuleParams } from '../../../../rule_schema'; -import { - convertCreateAPIToInternalSchema, - internalRuleToAPIResponse, -} from '../../../normalization/rule_converters'; -import { validateMlAuth, RuleResponseValidationError } from '../utils'; - -export const createPrebuiltRule = async ( - rulesClient: RulesClient, - args: CreatePrebuiltRuleArgs, - mlAuthz: MlAuthz -): Promise => { - const { params } = args; - - await validateMlAuth(mlAuthz, params.type); - - const internalRule = convertCreateAPIToInternalSchema(params, { - immutable: true, - defaultEnabled: false, - }); - - const rule = await rulesClient.create({ - data: internalRule, - }); - - /* Trying to convert the rule to a RuleResponse object */ - const parseResult = RuleResponse.safeParse(internalRuleToAPIResponse(rule)); - - if (!parseResult.success) { - throw new RuleResponseValidationError({ - message: stringifyZodError(parseResult.error), - ruleId: rule.params.ruleId, - }); - } - - return parseResult.data; -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/create_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/create_rule.ts new file mode 100644 index 000000000000..772e0c775d8b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/create_rule.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { RulesClient } from '@kbn/alerting-plugin/server'; +import { ruleTypeMappings } from '@kbn/securitysolution-rules'; +import { SERVER_APP_ID } from '../../../../../../../common'; +import type { + RuleCreateProps, + RuleResponse, +} from '../../../../../../../common/api/detection_engine/model/rule_schema'; +import type { MlAuthz } from '../../../../../machine_learning/authz'; +import type { RuleParams } from '../../../../rule_schema'; +import { convertAlertingRuleToRuleResponse } from '../converters/convert_alerting_rule_to_rule_response'; +import { convertRuleResponseToAlertingRule } from '../converters/convert_rule_response_to_alerting_rule'; +import { applyRuleDefaults } from '../mergers/apply_rule_defaults'; +import { validateMlAuth } from '../utils'; + +interface CreateRuleOptions { + rulesClient: RulesClient; + mlAuthz: MlAuthz; + rule: RuleCreateProps & { immutable: boolean }; + id?: string; + allowMissingConnectorSecrets?: boolean; +} + +export const createRule = async ({ + rulesClient, + mlAuthz, + rule, + id, + allowMissingConnectorSecrets, +}: CreateRuleOptions): Promise => { + await validateMlAuth(mlAuthz, rule.type); + + const ruleWithDefaults = applyRuleDefaults(rule); + + const payload = { + ...convertRuleResponseToAlertingRule(ruleWithDefaults), + alertTypeId: ruleTypeMappings[rule.type], + consumer: SERVER_APP_ID, + enabled: rule.enabled ?? false, + }; + + const createdRule = await rulesClient.create({ + data: payload, + options: { id }, + allowMissingConnectorSecrets, + }); + + return convertAlertingRuleToRuleResponse(createdRule); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/delete_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/delete_rule.ts index ec1491e8159d..4a9ca8abcdeb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/delete_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/delete_rule.ts @@ -6,9 +6,13 @@ */ import type { RulesClient } from '@kbn/alerting-plugin/server'; -import type { DeleteRuleArgs } from '../detection_rules_client_interface'; +import type { RuleObjectId } from '../../../../../../../common/api/detection_engine'; -export const deleteRule = async (rulesClient: RulesClient, args: DeleteRuleArgs): Promise => { - const { ruleId } = args; +interface DeleteRuleParams { + rulesClient: RulesClient; + ruleId: RuleObjectId; +} + +export const deleteRule = async ({ rulesClient, ruleId }: DeleteRuleParams): Promise => { await rulesClient.delete({ id: ruleId }); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/get_rule_by_id.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/get_rule_by_id.ts new file mode 100644 index 000000000000..39ca15fda42f --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/get_rule_by_id.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 type { RulesClient } from '@kbn/alerting-plugin/server'; +import type { + RuleObjectId, + RuleResponse, +} from '../../../../../../../common/api/detection_engine/model/rule_schema'; +import type { RuleParams } from '../../../../rule_schema'; +import { convertAlertingRuleToRuleResponse } from '../converters/convert_alerting_rule_to_rule_response'; + +interface GethRuleByIdOptions { + rulesClient: RulesClient; + id: RuleObjectId; +} + +export const getRuleById = async ({ + rulesClient, + id, +}: GethRuleByIdOptions): Promise => { + try { + const rule = await rulesClient.resolve({ id }); + return convertAlertingRuleToRuleResponse(rule); + } catch (err) { + if (err?.output?.statusCode === 404) { + return null; + } + throw err; + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/get_rule_by_id_or_rule_id.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/get_rule_by_id_or_rule_id.ts new file mode 100644 index 000000000000..fce28d1a1c03 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/get_rule_by_id_or_rule_id.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { RulesClient } from '@kbn/alerting-plugin/server'; +import type { + RuleObjectId, + RuleResponse, + RuleSignatureId, +} from '../../../../../../../common/api/detection_engine/model/rule_schema'; +import { invariant } from '../../../../../../../common/utils/invariant'; +import { getRuleById } from './get_rule_by_id'; +import { getRuleByRuleId } from './get_rule_by_rule_id'; + +interface GetRuleByIdOptions { + rulesClient: RulesClient; + id: RuleObjectId | undefined; + ruleId: RuleSignatureId | undefined; +} + +export const getRuleByIdOrRuleId = async ({ + rulesClient, + id, + ruleId, +}: GetRuleByIdOptions): Promise => { + if (id != null) { + return getRuleById({ rulesClient, id }); + } + if (ruleId != null) { + return getRuleByRuleId({ rulesClient, ruleId }); + } + invariant(false, 'Either id or ruleId must be provided'); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/get_rule_by_rule_id.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/get_rule_by_rule_id.ts new file mode 100644 index 000000000000..fda00cd292b8 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/get_rule_by_rule_id.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { RulesClient } from '@kbn/alerting-plugin/server'; +import type { + RuleResponse, + RuleSignatureId, +} from '../../../../../../../common/api/detection_engine/model/rule_schema'; +import { findRules } from '../../search/find_rules'; +import { convertAlertingRuleToRuleResponse } from '../converters/convert_alerting_rule_to_rule_response'; + +interface GetRuleByRuleIdOptions { + rulesClient: RulesClient; + ruleId: RuleSignatureId; +} + +export const getRuleByRuleId = async ({ + rulesClient, + ruleId, +}: GetRuleByRuleIdOptions): Promise => { + const findRuleResponse = await findRules({ + rulesClient, + filter: `alert.attributes.params.ruleId: "${ruleId}"`, + page: 1, + fields: undefined, + perPage: undefined, + sortField: undefined, + sortOrder: undefined, + }); + if (findRuleResponse.data.length === 0) { + return null; + } + return convertAlertingRuleToRuleResponse(findRuleResponse.data[0]); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/import_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/import_rule.ts index 55a0399f1a52..adb28133b62f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/import_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/import_rule.ts @@ -6,78 +6,67 @@ */ import type { RulesClient } from '@kbn/alerting-plugin/server'; -import { stringifyZodError } from '@kbn/zod-helpers'; +import type { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema'; import type { MlAuthz } from '../../../../../machine_learning/authz'; -import type { ImportRuleArgs } from '../detection_rules_client_interface'; -import type { RuleAlertType, RuleParams } from '../../../../rule_schema'; +import type { IPrebuiltRuleAssetsClient } from '../../../../prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client'; import { createBulkErrorObject } from '../../../../routes/utils'; -import { - convertCreateAPIToInternalSchema, - convertUpdateAPIToInternalSchema, - internalRuleToAPIResponse, -} from '../../../normalization/rule_converters'; -import { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema'; - -import { validateMlAuth, RuleResponseValidationError } from '../utils'; +import { convertAlertingRuleToRuleResponse } from '../converters/convert_alerting_rule_to_rule_response'; +import { convertRuleResponseToAlertingRule } from '../converters/convert_rule_response_to_alerting_rule'; +import type { ImportRuleArgs } from '../detection_rules_client_interface'; +import { applyRuleUpdate } from '../mergers/apply_rule_update'; +import { validateMlAuth } from '../utils'; +import { createRule } from './create_rule'; +import { getRuleByRuleId } from './get_rule_by_rule_id'; -import { readRules } from '../read_rules'; +interface ImportRuleOptions { + rulesClient: RulesClient; + prebuiltRuleAssetClient: IPrebuiltRuleAssetsClient; + importRulePayload: ImportRuleArgs; + mlAuthz: MlAuthz; +} -export const importRule = async ( - rulesClient: RulesClient, - importRulePayload: ImportRuleArgs, - mlAuthz: MlAuthz -): Promise => { +export const importRule = async ({ + rulesClient, + importRulePayload, + prebuiltRuleAssetClient, + mlAuthz, +}: ImportRuleOptions): Promise => { const { ruleToImport, overwriteRules, allowMissingConnectorSecrets } = importRulePayload; await validateMlAuth(mlAuthz, ruleToImport.type); - const existingRule = await readRules({ + const existingRule = await getRuleByRuleId({ rulesClient, ruleId: ruleToImport.rule_id, - id: undefined, }); if (existingRule && !overwriteRules) { throw createBulkErrorObject({ - ruleId: existingRule.params.ruleId, + ruleId: existingRule.rule_id, statusCode: 409, - message: `rule_id: "${existingRule.params.ruleId}" already exists`, + message: `rule_id: "${existingRule.rule_id}" already exists`, }); } - let importedInternalRule: RuleAlertType; - if (existingRule && overwriteRules) { - const ruleUpdateParams = convertUpdateAPIToInternalSchema({ + const ruleWithUpdates = await applyRuleUpdate({ + prebuiltRuleAssetClient, existingRule, ruleUpdate: ruleToImport, }); - importedInternalRule = await rulesClient.update({ + const updatedRule = await rulesClient.update({ id: existingRule.id, - data: ruleUpdateParams, - }); - } else { - /* Rule does not exist, so we'll create it */ - const ruleCreateParams = convertCreateAPIToInternalSchema(ruleToImport, { - immutable: false, - }); - - importedInternalRule = await rulesClient.create({ - data: ruleCreateParams, - allowMissingConnectorSecrets, + data: convertRuleResponseToAlertingRule(ruleWithUpdates), }); + return convertAlertingRuleToRuleResponse(updatedRule); } - /* Trying to convert an internal rule to a RuleResponse object */ - const parseResult = RuleResponse.safeParse(internalRuleToAPIResponse(importedInternalRule)); - - if (!parseResult.success) { - throw new RuleResponseValidationError({ - message: stringifyZodError(parseResult.error), - ruleId: importedInternalRule.params.ruleId, - }); - } - - return parseResult.data; + /* Rule does not exist, so we'll create it */ + return createRule({ + rulesClient, + mlAuthz, + rule: ruleToImport, + allowMissingConnectorSecrets, + }); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/patch_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/patch_rule.ts index ce9956c5eec8..d615d5fc5a81 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/patch_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/patch_rule.ts @@ -6,34 +6,35 @@ */ import type { RulesClient } from '@kbn/alerting-plugin/server'; -import { stringifyZodError } from '@kbn/zod-helpers'; +import type { + RulePatchProps, + RuleResponse, +} from '../../../../../../../common/api/detection_engine/model/rule_schema'; import type { MlAuthz } from '../../../../../machine_learning/authz'; -import type { PatchRuleArgs } from '../detection_rules_client_interface'; +import type { IPrebuiltRuleAssetsClient } from '../../../../prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client'; +import { applyRulePatch } from '../mergers/apply_rule_patch'; import { getIdError } from '../../../utils/utils'; -import { - convertPatchAPIToInternalSchema, - internalRuleToAPIResponse, -} from '../../../normalization/rule_converters'; -import { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema'; +import { convertAlertingRuleToRuleResponse } from '../converters/convert_alerting_rule_to_rule_response'; +import { convertRuleResponseToAlertingRule } from '../converters/convert_rule_response_to_alerting_rule'; +import { ClientError, toggleRuleEnabledOnUpdate, validateMlAuth } from '../utils'; +import { getRuleByIdOrRuleId } from './get_rule_by_id_or_rule_id'; -import { - validateMlAuth, - ClientError, - toggleRuleEnabledOnUpdate, - RuleResponseValidationError, -} from '../utils'; +interface PatchRuleOptions { + rulesClient: RulesClient; + prebuiltRuleAssetClient: IPrebuiltRuleAssetsClient; + rulePatch: RulePatchProps; + mlAuthz: MlAuthz; +} -import { readRules } from '../read_rules'; +export const patchRule = async ({ + rulesClient, + prebuiltRuleAssetClient, + rulePatch, + mlAuthz, +}: PatchRuleOptions): Promise => { + const { rule_id: ruleId, id } = rulePatch; -export const patchRule = async ( - rulesClient: RulesClient, - args: PatchRuleArgs, - mlAuthz: MlAuthz -): Promise => { - const { nextParams } = args; - const { rule_id: ruleId, id } = nextParams; - - const existingRule = await readRules({ + const existingRule = await getRuleByIdOrRuleId({ rulesClient, ruleId, id, @@ -44,32 +45,20 @@ export const patchRule = async ( throw new ClientError(error.message, error.statusCode); } - await validateMlAuth(mlAuthz, nextParams.type ?? existingRule.params.type); + await validateMlAuth(mlAuthz, rulePatch.type ?? existingRule.type); - const patchedRule = convertPatchAPIToInternalSchema(nextParams, existingRule); + const patchedRule = await applyRulePatch({ + prebuiltRuleAssetClient, + existingRule, + rulePatch, + }); const patchedInternalRule = await rulesClient.update({ id: existingRule.id, - data: patchedRule, + data: convertRuleResponseToAlertingRule(patchedRule), }); - const { enabled } = await toggleRuleEnabledOnUpdate( - rulesClient, - existingRule, - nextParams.enabled - ); - - /* Trying to convert the internal rule to a RuleResponse object */ - const parseResult = RuleResponse.safeParse( - internalRuleToAPIResponse({ ...patchedInternalRule, enabled }) - ); - - if (!parseResult.success) { - throw new RuleResponseValidationError({ - message: stringifyZodError(parseResult.error), - ruleId: patchedInternalRule.params.ruleId, - }); - } + const { enabled } = await toggleRuleEnabledOnUpdate(rulesClient, existingRule, patchedRule); - return parseResult.data; + return convertAlertingRuleToRuleResponse({ ...patchedInternalRule, enabled }); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/update_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/update_rule.ts index 8684a7ccd2c6..cf42074c2a04 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/update_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/update_rule.ts @@ -6,36 +6,37 @@ */ import type { RulesClient } from '@kbn/alerting-plugin/server'; -import { stringifyZodError } from '@kbn/zod-helpers'; +import type { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema'; import type { MlAuthz } from '../../../../../machine_learning/authz'; -import type { UpdateRuleArgs } from '../detection_rules_client_interface'; +import { applyRuleUpdate } from '../mergers/apply_rule_update'; import { getIdError } from '../../../utils/utils'; -import { - convertUpdateAPIToInternalSchema, - internalRuleToAPIResponse, -} from '../../../normalization/rule_converters'; -import { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema'; +import { convertRuleResponseToAlertingRule } from '../converters/convert_rule_response_to_alerting_rule'; -import { - validateMlAuth, - ClientError, - toggleRuleEnabledOnUpdate, - RuleResponseValidationError, -} from '../utils'; +import { ClientError, toggleRuleEnabledOnUpdate, validateMlAuth } from '../utils'; -import { readRules } from '../read_rules'; +import type { RuleUpdateProps } from '../../../../../../../common/api/detection_engine'; +import type { IPrebuiltRuleAssetsClient } from '../../../../prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client'; +import { getRuleByIdOrRuleId } from './get_rule_by_id_or_rule_id'; +import { convertAlertingRuleToRuleResponse } from '../converters/convert_alerting_rule_to_rule_response'; -export const updateRule = async ( - rulesClient: RulesClient, - args: UpdateRuleArgs, - mlAuthz: MlAuthz -): Promise => { - const { ruleUpdate } = args; +interface UpdateRuleArguments { + rulesClient: RulesClient; + prebuiltRuleAssetClient: IPrebuiltRuleAssetsClient; + ruleUpdate: RuleUpdateProps; + mlAuthz: MlAuthz; +} + +export const updateRule = async ({ + rulesClient, + prebuiltRuleAssetClient, + ruleUpdate, + mlAuthz, +}: UpdateRuleArguments): Promise => { const { rule_id: ruleId, id } = ruleUpdate; await validateMlAuth(mlAuthz, ruleUpdate.type); - const existingRule = await readRules({ + const existingRule = await getRuleByIdOrRuleId({ rulesClient, ruleId, id, @@ -46,33 +47,21 @@ export const updateRule = async ( throw new ClientError(error.message, error.statusCode); } - const newInternalRule = convertUpdateAPIToInternalSchema({ + const ruleWithUpdates = await applyRuleUpdate({ + prebuiltRuleAssetClient, existingRule, ruleUpdate, }); - const updatedInternalRule = await rulesClient.update({ + const updatedRule = await rulesClient.update({ id: existingRule.id, - data: newInternalRule, + data: convertRuleResponseToAlertingRule(ruleWithUpdates), }); - const { enabled } = await toggleRuleEnabledOnUpdate( - rulesClient, - existingRule, - ruleUpdate.enabled - ); - - /* Trying to convert the internal rule to a RuleResponse object */ - const parseResult = RuleResponse.safeParse( - internalRuleToAPIResponse({ ...updatedInternalRule, enabled }) - ); + const { enabled } = await toggleRuleEnabledOnUpdate(rulesClient, existingRule, ruleWithUpdates); - if (!parseResult.success) { - throw new RuleResponseValidationError({ - message: stringifyZodError(parseResult.error), - ruleId: updatedInternalRule.params.ruleId, - }); - } - - return parseResult.data; + return convertAlertingRuleToRuleResponse({ + ...updatedRule, + enabled, + }); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/upgrade_prebuilt_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/upgrade_prebuilt_rule.ts index 8c1079f5716d..4eef323be2fd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/upgrade_prebuilt_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/methods/upgrade_prebuilt_rule.ts @@ -6,94 +6,74 @@ */ import type { RulesClient } from '@kbn/alerting-plugin/server'; -import { stringifyZodError } from '@kbn/zod-helpers'; +import type { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema'; import type { MlAuthz } from '../../../../../machine_learning/authz'; -import type { RuleParams } from '../../../../rule_schema'; -import type { UpgradePrebuiltRuleArgs } from '../detection_rules_client_interface'; -import { - convertPatchAPIToInternalSchema, - convertCreateAPIToInternalSchema, - internalRuleToAPIResponse, -} from '../../../normalization/rule_converters'; -import { transformAlertToRuleAction } from '../../../../../../../common/detection_engine/transform_actions'; -import { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema'; - -import { validateMlAuth, ClientError, RuleResponseValidationError } from '../utils'; - -import { readRules } from '../read_rules'; - -export const upgradePrebuiltRule = async ( - rulesClient: RulesClient, - upgradePrebuiltRulePayload: UpgradePrebuiltRuleArgs, - mlAuthz: MlAuthz -): Promise => { - const { ruleAsset } = upgradePrebuiltRulePayload; - +import type { PrebuiltRuleAsset } from '../../../../prebuilt_rules'; +import type { IPrebuiltRuleAssetsClient } from '../../../../prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client'; +import { convertAlertingRuleToRuleResponse } from '../converters/convert_alerting_rule_to_rule_response'; +import { convertRuleResponseToAlertingRule } from '../converters/convert_rule_response_to_alerting_rule'; +import { applyRulePatch } from '../mergers/apply_rule_patch'; +import { ClientError, validateMlAuth } from '../utils'; +import { createRule } from './create_rule'; +import { getRuleByRuleId } from './get_rule_by_rule_id'; + +export const upgradePrebuiltRule = async ({ + rulesClient, + ruleAsset, + mlAuthz, + prebuiltRuleAssetClient, +}: { + rulesClient: RulesClient; + ruleAsset: PrebuiltRuleAsset; + mlAuthz: MlAuthz; + prebuiltRuleAssetClient: IPrebuiltRuleAssetsClient; +}): Promise => { await validateMlAuth(mlAuthz, ruleAsset.type); - const existingRule = await readRules({ + const existingRule = await getRuleByRuleId({ rulesClient, ruleId: ruleAsset.rule_id, - id: undefined, }); if (!existingRule) { throw new ClientError(`Failed to find rule ${ruleAsset.rule_id}`, 500); } - if (ruleAsset.type !== existingRule.params.type) { + if (ruleAsset.type !== existingRule.type) { // If we're trying to change the type of a prepackaged rule, we need to delete the old one // and replace it with the new rule, keeping the enabled setting, actions, throttle, id, // and exception lists from the old rule await rulesClient.delete({ id: existingRule.id }); - const internalRule = convertCreateAPIToInternalSchema( - { + const createdRule = await createRule({ + rulesClient, + mlAuthz, + rule: { ...ruleAsset, + immutable: true, enabled: existingRule.enabled, - exceptions_list: existingRule.params.exceptionsList, - actions: existingRule.actions.map(transformAlertToRuleAction), - timeline_id: existingRule.params.timelineId, - timeline_title: existingRule.params.timelineTitle, + exceptions_list: existingRule.exceptions_list, + actions: existingRule.actions, + timeline_id: existingRule.timeline_id, + timeline_title: existingRule.timeline_title, }, - { immutable: true, defaultEnabled: existingRule.enabled } - ); - - const createdRule = await rulesClient.create({ - data: internalRule, - options: { id: existingRule.id }, + id: existingRule.id, }); - /* Trying to convert the rule to a RuleResponse object */ - const parseResult = RuleResponse.safeParse(internalRuleToAPIResponse(createdRule)); - - if (!parseResult.success) { - throw new RuleResponseValidationError({ - message: stringifyZodError(parseResult.error), - ruleId: createdRule.params.ruleId, - }); - } - - return parseResult.data; + return createdRule; } // Else, simply patch it. - const patchedRule = convertPatchAPIToInternalSchema(ruleAsset, existingRule); + const patchedRule = await applyRulePatch({ + prebuiltRuleAssetClient, + existingRule, + rulePatch: ruleAsset, + }); const patchedInternalRule = await rulesClient.update({ id: existingRule.id, - data: patchedRule, + data: convertRuleResponseToAlertingRule(patchedRule), }); - /* Trying to convert the internal rule to a RuleResponse object */ - const parseResult = RuleResponse.safeParse(internalRuleToAPIResponse(patchedInternalRule)); - - if (!parseResult.success) { - throw new RuleResponseValidationError({ - message: stringifyZodError(parseResult.error), - ruleId: patchedInternalRule.params.ruleId, - }); - } - - return parseResult.data; + return convertAlertingRuleToRuleResponse(patchedInternalRule); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/read_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/read_rules.ts index d699d5ee7dd5..67c7746bd0eb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/read_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/read_rules.ts @@ -30,6 +30,8 @@ export interface ReadRuleOptions { * be returned as a not-found or a thrown error that is not 404. * @param ruleId - This is a close second to being fast as long as it can find the rule_id from * a filter query against the ruleId property in params using `alert.attributes.params.ruleId: "${ruleId}"` + * + * @deprecated Should be replaced with DetectionRulesClient.getRuleById once it's implemented */ export const readRules = async ({ rulesClient, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/utils.ts index 4f25497b3056..db2af377eb5b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/utils.ts @@ -12,21 +12,21 @@ import type { RulesClient } from '@kbn/alerting-plugin/server'; 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 { RuleSignatureId } from '../../../../../../common/api/detection_engine/model/rule_schema/common_attributes.gen'; import { throwAuthzError } from '../../../../machine_learning/validation'; +import type { RuleResponse } from '../../../../../../common/api/detection_engine'; export const toggleRuleEnabledOnUpdate = async ( rulesClient: RulesClient, - existingRule: RuleAlertType, - updatedRuleEnabled?: boolean + existingRule: RuleResponse, + updatedRule: RuleResponse ): Promise<{ enabled: boolean }> => { - if (existingRule.enabled && updatedRuleEnabled === false) { + if (existingRule.enabled && !updatedRule.enabled) { await rulesClient.disable({ id: existingRule.id }); return { enabled: false }; } - if (!existingRule.enabled && updatedRuleEnabled === true) { + if (!existingRule.enabled && updatedRule.enabled) { await rulesClient.enable({ id: existingRule.id }); return { enabled: true }; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.test.ts index 08794143ce16..5093393d6d65 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.test.ts @@ -15,7 +15,7 @@ import { getRuleMock, } from '../../../routes/__mocks__/request_responses'; import { getThreatMock } from '../../../../../../common/detection_engine/schemas/types/threat.mock'; -import { internalRuleToAPIResponse } from '../../normalization/rule_converters'; +import { internalRuleToAPIResponse } from '../detection_rules_client/converters/internal_rule_to_api_response'; import { getEqlRuleParams, getQueryRuleParams } from '../../../rule_schema/mocks'; import { getExportByObjectIds } from './get_export_by_object_ids'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.ts index ce57b33227ca..7c3142aed85f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.ts @@ -13,7 +13,7 @@ import type { ExceptionListClient } from '@kbn/lists-plugin/server'; import type { RulesClient, PartialRule } from '@kbn/alerting-plugin/server'; import type { ActionsClient } from '@kbn/actions-plugin/server'; import { withSecuritySpan } from '../../../../../utils/with_security_span'; -import { internalRuleToAPIResponse } from '../../normalization/rule_converters'; +import { internalRuleToAPIResponse } from '../detection_rules_client/converters/internal_rule_to_api_response'; import type { RuleParams } from '../../../rule_schema'; import { hasValidRuleType } from '../../../rule_schema'; import { findRules } from '../search/find_rules'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_converters.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_converters.test.ts deleted file mode 100644 index 5df02371befa..000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_converters.test.ts +++ /dev/null @@ -1,491 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - commonParamsCamelToSnake, - patchTypeSpecificSnakeToCamel, - typeSpecificCamelToSnake, -} from './rule_converters'; -import { - getBaseRuleParams, - getEqlRuleParams, - getEsqlRuleParams, - getMlRuleParams, - getNewTermsRuleParams, - getQueryRuleParams, - getSavedQueryRuleParams, - getThreatRuleParams, - getThresholdRuleParams, -} from '../../rule_schema/mocks'; -import type { - AlertSuppressionDuration, - PatchRuleRequestBody, - AlertSuppressionMissingFieldsStrategy, -} from '../../../../../common/api/detection_engine'; - -describe('rule_converters', () => { - describe('patchTypeSpecificSnakeToCamel', () => { - describe('EQL', () => { - test('should accept EQL params when existing rule type is EQL', () => { - const patchParams = { - timestamp_field: 'event.created', - event_category_override: 'event.not_category', - tiebreaker_field: 'event.created', - }; - const rule = getEqlRuleParams(); - const patchedParams = patchTypeSpecificSnakeToCamel(patchParams, rule); - expect(patchedParams).toEqual( - expect.objectContaining({ - timestampField: 'event.created', - eventCategoryOverride: 'event.not_category', - tiebreakerField: 'event.created', - }) - ); - }); - test('should accept EQL params with suppression in snake case and convert to camel case when rule type is EQL', () => { - const patchParams = { - timestamp_field: 'event.created', - event_category_override: 'event.not_category', - tiebreaker_field: 'event.created', - alert_suppression: { - group_by: ['event.type'], - duration: { - value: 10, - unit: 'm', - } as AlertSuppressionDuration, - missing_fields_strategy: 'suppress', - }, - }; - const rule = getEqlRuleParams(); - const patchedParams = patchTypeSpecificSnakeToCamel(patchParams, rule); - expect(patchedParams).toEqual( - expect.objectContaining({ - timestampField: 'event.created', - eventCategoryOverride: 'event.not_category', - tiebreakerField: 'event.created', - alertSuppression: { - groupBy: ['event.type'], - duration: { - value: 10, - unit: 'm', - }, - missingFieldsStrategy: 'suppress', - }, - }) - ); - }); - test('should reject invalid EQL params when existing rule type is EQL', () => { - const patchParams = { - timestamp_field: 1, - event_category_override: 1, - tiebreaker_field: 1, - } as PatchRuleRequestBody; - const rule = getEqlRuleParams(); - expect(() => patchTypeSpecificSnakeToCamel(patchParams, rule)).toThrowError( - 'event_category_override: Expected string, received number, tiebreaker_field: Expected string, received number, timestamp_field: Expected string, received number' - ); - }); - test('should reject EQL params with invalid suppression group_by field', () => { - const patchParams = { - timestamp_field: 'event.created', - event_category_override: 'event.not_category', - tiebreaker_field: 'event.created', - alert_suppression: { - group_by: 'event.type', - duration: { - value: 10, - unit: 'm', - } as AlertSuppressionDuration, - missing_fields_strategy: 'suppress', - }, - }; - const rule = getEqlRuleParams(); - expect(() => patchTypeSpecificSnakeToCamel(patchParams, rule)).toThrowError( - 'alert_suppression.group_by: Expected array, received string' - ); - }); - }); - - describe('machine learning rules', () => { - test('should accept machine learning params when existing rule type is machine learning', () => { - const patchParams = { - anomaly_threshold: 5, - }; - const rule = getMlRuleParams(); - const patchedParams = patchTypeSpecificSnakeToCamel(patchParams, rule); - expect(patchedParams).toEqual( - expect.objectContaining({ - anomalyThreshold: 5, - }) - ); - }); - - test('should reject invalid machine learning params when existing rule type is machine learning', () => { - const patchParams = { - anomaly_threshold: 'invalid', - } as PatchRuleRequestBody; - const rule = getMlRuleParams(); - expect(() => patchTypeSpecificSnakeToCamel(patchParams, rule)).toThrowError( - 'anomaly_threshold: Expected number, received string' - ); - }); - - it('accepts suppression params', () => { - const patchParams = { - alert_suppression: { - group_by: ['agent.name'], - missing_fields_strategy: 'suppress' as const, - }, - }; - const rule = getMlRuleParams(); - const patchedParams = patchTypeSpecificSnakeToCamel(patchParams, rule); - - expect(patchedParams).toEqual( - expect.objectContaining({ - alertSuppression: { - groupBy: ['agent.name'], - missingFieldsStrategy: 'suppress', - }, - }) - ); - }); - }); - - test('should accept threat match params when existing rule type is threat match', () => { - const patchParams = { - threat_indicator_path: 'my.indicator', - threat_query: 'test-query', - }; - const rule = getThreatRuleParams(); - const patchedParams = patchTypeSpecificSnakeToCamel(patchParams, rule); - expect(patchedParams).toEqual( - expect.objectContaining({ - threatIndicatorPath: 'my.indicator', - threatQuery: 'test-query', - }) - ); - }); - - test('should reject invalid threat match params when existing rule type is threat match', () => { - const patchParams = { - threat_indicator_path: 1, - threat_query: 1, - } as PatchRuleRequestBody; - const rule = getThreatRuleParams(); - expect(() => patchTypeSpecificSnakeToCamel(patchParams, rule)).toThrowError( - 'threat_query: Expected string, received number, threat_indicator_path: Expected string, received number' - ); - }); - - test('should accept query params when existing rule type is query', () => { - const patchParams = { - index: ['new-test-index'], - language: 'lucene', - } as PatchRuleRequestBody; - const rule = getQueryRuleParams(); - const patchedParams = patchTypeSpecificSnakeToCamel(patchParams, rule); - expect(patchedParams).toEqual( - expect.objectContaining({ - index: ['new-test-index'], - language: 'lucene', - }) - ); - }); - - test('should reject invalid query params when existing rule type is query', () => { - const patchParams = { - index: [1], - language: 'non-language', - } as PatchRuleRequestBody; - const rule = getQueryRuleParams(); - expect(() => patchTypeSpecificSnakeToCamel(patchParams, rule)).toThrowError( - "index.0: Expected string, received number, language: Invalid enum value. Expected 'kuery' | 'lucene', received 'non-language'" - ); - }); - - test('should accept saved query params when existing rule type is saved query', () => { - const patchParams = { - index: ['new-test-index'], - language: 'lucene', - } as PatchRuleRequestBody; - const rule = getSavedQueryRuleParams(); - const patchedParams = patchTypeSpecificSnakeToCamel(patchParams, rule); - expect(patchedParams).toEqual( - expect.objectContaining({ - index: ['new-test-index'], - language: 'lucene', - }) - ); - }); - - test('should reject invalid saved query params when existing rule type is saved query', () => { - const patchParams = { - index: [1], - language: 'non-language', - } as PatchRuleRequestBody; - const rule = getSavedQueryRuleParams(); - expect(() => patchTypeSpecificSnakeToCamel(patchParams, rule)).toThrowError( - "index.0: Expected string, received number, language: Invalid enum value. Expected 'kuery' | 'lucene', received 'non-language'" - ); - }); - - test('should accept threshold params when existing rule type is threshold', () => { - const patchParams = { - threshold: { - field: ['host.name'], - value: 107, - }, - }; - const rule = getThresholdRuleParams(); - const patchedParams = patchTypeSpecificSnakeToCamel(patchParams, rule); - expect(patchedParams).toEqual( - expect.objectContaining({ - threshold: { - field: ['host.name'], - value: 107, - }, - }) - ); - }); - - test('should reject invalid threshold params when existing rule type is threshold', () => { - const patchParams = { - threshold: { - field: ['host.name'], - value: 'invalid', - }, - } as PatchRuleRequestBody; - const rule = getThresholdRuleParams(); - expect(() => patchTypeSpecificSnakeToCamel(patchParams, rule)).toThrowError( - 'threshold.value: Expected number, received string' - ); - }); - - test('should accept ES|QL alerts suppression params', () => { - const patchParams = { - alert_suppression: { - group_by: ['agent.name'], - duration: { value: 4, unit: 'h' as const }, - missing_fields_strategy: 'doNotSuppress' as const, - }, - }; - const rule = getEsqlRuleParams(); - const patchedParams = patchTypeSpecificSnakeToCamel(patchParams, rule); - expect(patchedParams).toEqual( - expect.objectContaining({ - alertSuppression: { - groupBy: ['agent.name'], - missingFieldsStrategy: 'doNotSuppress', - duration: { value: 4, unit: 'h' }, - }, - }) - ); - }); - - test('should accept threshold alerts suppression params', () => { - const patchParams = { - alert_suppression: { - duration: { value: 4, unit: 'h' as const }, - }, - }; - const rule = getThresholdRuleParams(); - const patchedParams = patchTypeSpecificSnakeToCamel(patchParams, rule); - expect(patchedParams).toEqual( - expect.objectContaining({ - alertSuppression: { - duration: { value: 4, unit: 'h' }, - }, - }) - ); - }); - - test('should accept threat_match alerts suppression params', () => { - const patchParams = { - alert_suppression: { - group_by: ['agent.name'], - missing_fields_strategy: 'suppress' as const, - }, - }; - const rule = getThreatRuleParams(); - const patchedParams = patchTypeSpecificSnakeToCamel(patchParams, rule); - expect(patchedParams).toEqual( - expect.objectContaining({ - alertSuppression: { - groupBy: ['agent.name'], - missingFieldsStrategy: 'suppress', - }, - }) - ); - }); - - test('should accept new_terms alerts suppression params', () => { - const patchParams = { - alert_suppression: { - group_by: ['agent.name'], - duration: { value: 4, unit: 'h' as const }, - missing_fields_strategy: 'suppress' as const, - }, - }; - const rule = getNewTermsRuleParams(); - const patchedParams = patchTypeSpecificSnakeToCamel(patchParams, rule); - expect(patchedParams).toEqual( - expect.objectContaining({ - alertSuppression: { - groupBy: ['agent.name'], - missingFieldsStrategy: 'suppress', - duration: { value: 4, unit: 'h' }, - }, - }) - ); - }); - - test('should accept new terms params when existing rule type is new terms', () => { - const patchParams = { - new_terms_fields: ['event.new_field'], - }; - const rule = getNewTermsRuleParams(); - const patchedParams = patchTypeSpecificSnakeToCamel(patchParams, rule); - expect(patchedParams).toEqual( - expect.objectContaining({ - newTermsFields: ['event.new_field'], - }) - ); - }); - - test('should reject invalid new terms params when existing rule type is new terms', () => { - const patchParams = { - new_terms_fields: 'invalid', - } as PatchRuleRequestBody; - const rule = getNewTermsRuleParams(); - expect(() => patchTypeSpecificSnakeToCamel(patchParams, rule)).toThrowError( - 'new_terms_fields: Expected array, received string' - ); - }); - }); - - describe('typeSpecificCamelToSnake', () => { - describe('EQL', () => { - test('should accept EQL params when existing rule type is EQL', () => { - const params = { - timestampField: 'event.created', - eventCategoryOverride: 'event.not_category', - tiebreakerField: 'event.created', - }; - const eqlRule = { ...getEqlRuleParams(), ...params }; - const transformedParams = typeSpecificCamelToSnake(eqlRule); - expect(transformedParams).toEqual( - expect.objectContaining({ - timestamp_field: 'event.created', - event_category_override: 'event.not_category', - tiebreaker_field: 'event.created', - }) - ); - }); - - test('should accept EQL params with suppression in camel case and convert to snake case when rule type is EQL', () => { - const params = { - timestampField: 'event.created', - eventCategoryOverride: 'event.not_category', - tiebreakerField: 'event.created', - alertSuppression: { - groupBy: ['event.type'], - duration: { - value: 10, - unit: 'm', - } as AlertSuppressionDuration, - missingFieldsStrategy: 'suppress' as AlertSuppressionMissingFieldsStrategy, - }, - }; - const eqlRule = { ...getEqlRuleParams(), ...params }; - const transformedParams = typeSpecificCamelToSnake(eqlRule); - expect(transformedParams).toEqual( - expect.objectContaining({ - timestamp_field: 'event.created', - event_category_override: 'event.not_category', - tiebreaker_field: 'event.created', - alert_suppression: { - group_by: ['event.type'], - duration: { - value: 10, - unit: 'm', - } as AlertSuppressionDuration, - missing_fields_strategy: 'suppress', - }, - }) - ); - }); - }); - - describe('machine learning rules', () => { - it('accepts normal params', () => { - const params = { - anomalyThreshold: 74, - machineLearningJobId: ['job-1'], - }; - const ruleParams = { ...getMlRuleParams(), ...params }; - const transformedParams = typeSpecificCamelToSnake(ruleParams); - expect(transformedParams).toEqual( - expect.objectContaining({ - anomaly_threshold: 74, - machine_learning_job_id: ['job-1'], - }) - ); - }); - - it('accepts suppression params', () => { - const params = { - anomalyThreshold: 74, - machineLearningJobId: ['job-1'], - alertSuppression: { - groupBy: ['event.type'], - duration: { - value: 10, - unit: 'm', - } as AlertSuppressionDuration, - missingFieldsStrategy: 'suppress' as AlertSuppressionMissingFieldsStrategy, - }, - }; - const ruleParams = { ...getMlRuleParams(), ...params }; - const transformedParams = typeSpecificCamelToSnake(ruleParams); - expect(transformedParams).toEqual( - expect.objectContaining({ - anomaly_threshold: 74, - machine_learning_job_id: ['job-1'], - alert_suppression: { - group_by: ['event.type'], - duration: { - value: 10, - unit: 'm', - }, - missing_fields_strategy: 'suppress', - }, - }) - ); - }); - }); - }); - - describe('commonParamsCamelToSnake', () => { - test('should convert rule_source params to snake case', () => { - const transformedParams = commonParamsCamelToSnake({ - ...getBaseRuleParams(), - ruleSource: { - type: 'external', - isCustomized: false, - }, - }); - expect(transformedParams).toEqual( - expect.objectContaining({ - rule_source: { - type: 'external', - is_customized: false, - }, - }) - ); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_converters.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_converters.ts deleted file mode 100644 index db815f32fb5e..000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_converters.ts +++ /dev/null @@ -1,869 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { v4 as uuidv4 } from 'uuid'; - -import { stringifyZodError } from '@kbn/zod-helpers'; -import { BadRequestError } from '@kbn/securitysolution-es-utils'; -import { ruleTypeMappings } from '@kbn/securitysolution-rules'; -import type { ResolvedSanitizedRule, SanitizedRule } from '@kbn/alerting-plugin/common'; - -import type { RequiredOptional } from '@kbn/zod-helpers'; -import { - DEFAULT_INDICATOR_SOURCE_PATH, - DEFAULT_MAX_SIGNALS, - SERVER_APP_ID, -} from '../../../../../common/constants'; - -import type { PatchRuleRequestBody } from '../../../../../common/api/detection_engine/rule_management'; -import type { - RuleCreateProps, - RuleUpdateProps, - TypeSpecificCreateProps, - TypeSpecificResponse, -} from '../../../../../common/api/detection_engine/model/rule_schema'; -import { - EqlRulePatchFields, - EsqlRulePatchFields, - MachineLearningRulePatchFields, - NewTermsRulePatchFields, - QueryRulePatchFields, - SavedQueryRulePatchFields, - ThreatMatchRulePatchFields, - ThresholdRulePatchFields, - RuleResponse, -} from '../../../../../common/api/detection_engine/model/rule_schema'; - -import { - transformAlertToRuleAction, - transformAlertToRuleResponseAction, - transformRuleToAlertAction, - transformRuleToAlertResponseAction, -} from '../../../../../common/detection_engine/transform_actions'; - -import { - normalizeMachineLearningJobIds, - normalizeThresholdObject, -} from '../../../../../common/detection_engine/utils'; - -import { assertUnreachable } from '../../../../../common/utility_types'; - -import type { - InternalRuleCreate, - RuleParams, - TypeSpecificRuleParams, - BaseRuleParams, - EqlRuleParams, - EqlSpecificRuleParams, - EsqlRuleParams, - EsqlSpecificRuleParams, - ThreatRuleParams, - ThreatSpecificRuleParams, - QueryRuleParams, - QuerySpecificRuleParams, - SavedQuerySpecificRuleParams, - SavedQueryRuleParams, - ThresholdRuleParams, - ThresholdSpecificRuleParams, - MachineLearningRuleParams, - MachineLearningSpecificRuleParams, - InternalRuleUpdate, - NewTermsRuleParams, - NewTermsSpecificRuleParams, - RuleSourceCamelCased, -} from '../../rule_schema'; -import { transformFromAlertThrottle, transformToActionFrequency } from './rule_actions'; -import { - addEcsToRequiredFields, - convertAlertSuppressionToCamel, - convertAlertSuppressionToSnake, - migrateLegacyInvestigationFields, -} from '../utils/utils'; -import { createRuleExecutionSummary } from '../../rule_monitoring'; -import type { PrebuiltRuleAsset } from '../../prebuilt_rules'; -import { convertObjectKeysToSnakeCase } from '../../../../utils/object_case_converters'; - -const DEFAULT_FROM = 'now-6m' as const; -const DEFAULT_TO = 'now' as const; -const DEFAULT_INTERVAL = '5m' as const; - -// These functions provide conversions from the request API schema to the internal rule schema and from the internal rule schema -// to the response API schema. This provides static type-check assurances that the internal schema is in sync with the API schema for -// required and default-able fields. However, it is still possible to add an optional field to the API schema -// without causing a type-check error here. - -// Converts params from the snake case API format to the internal camel case format AND applies default values where needed. -// Notice that params.language is possibly undefined for most rule types in the API but we default it to kuery to match -// the legacy API behavior -export const typeSpecificSnakeToCamel = ( - params: TypeSpecificCreateProps -): TypeSpecificRuleParams => { - switch (params.type) { - case 'eql': { - return { - type: params.type, - language: params.language, - index: params.index, - dataViewId: params.data_view_id, - query: params.query, - filters: params.filters, - timestampField: params.timestamp_field, - eventCategoryOverride: params.event_category_override, - tiebreakerField: params.tiebreaker_field, - alertSuppression: convertAlertSuppressionToCamel(params.alert_suppression), - }; - } - case 'esql': { - return { - type: params.type, - language: params.language, - query: params.query, - alertSuppression: convertAlertSuppressionToCamel(params.alert_suppression), - }; - } - case 'threat_match': { - return { - type: params.type, - language: params.language ?? 'kuery', - index: params.index, - dataViewId: params.data_view_id, - query: params.query, - filters: params.filters, - savedId: params.saved_id, - threatFilters: params.threat_filters, - threatQuery: params.threat_query, - threatMapping: params.threat_mapping, - threatLanguage: params.threat_language, - threatIndex: params.threat_index, - threatIndicatorPath: params.threat_indicator_path ?? DEFAULT_INDICATOR_SOURCE_PATH, - concurrentSearches: params.concurrent_searches, - itemsPerSearch: params.items_per_search, - alertSuppression: convertAlertSuppressionToCamel(params.alert_suppression), - }; - } - case 'query': { - return { - type: params.type, - language: params.language ?? 'kuery', - index: params.index, - dataViewId: params.data_view_id, - query: params.query ?? '', - filters: params.filters, - savedId: params.saved_id, - responseActions: params.response_actions?.map(transformRuleToAlertResponseAction), - alertSuppression: convertAlertSuppressionToCamel(params.alert_suppression), - }; - } - case 'saved_query': { - return { - type: params.type, - language: params.language ?? 'kuery', - index: params.index, - query: params.query, - filters: params.filters, - savedId: params.saved_id, - dataViewId: params.data_view_id, - responseActions: params.response_actions?.map(transformRuleToAlertResponseAction), - alertSuppression: convertAlertSuppressionToCamel(params.alert_suppression), - }; - } - case 'threshold': { - return { - type: params.type, - language: params.language ?? 'kuery', - index: params.index, - dataViewId: params.data_view_id, - query: params.query, - filters: params.filters, - savedId: params.saved_id, - threshold: normalizeThresholdObject(params.threshold), - alertSuppression: params.alert_suppression?.duration - ? { duration: params.alert_suppression.duration } - : undefined, - }; - } - case 'machine_learning': { - return { - type: params.type, - anomalyThreshold: params.anomaly_threshold, - machineLearningJobId: normalizeMachineLearningJobIds(params.machine_learning_job_id), - alertSuppression: convertAlertSuppressionToCamel(params.alert_suppression), - }; - } - case 'new_terms': { - return { - type: params.type, - query: params.query, - newTermsFields: params.new_terms_fields, - historyWindowStart: params.history_window_start, - index: params.index, - filters: params.filters, - language: params.language ?? 'kuery', - dataViewId: params.data_view_id, - alertSuppression: convertAlertSuppressionToCamel(params.alert_suppression), - }; - } - default: { - return assertUnreachable(params); - } - } -}; - -const patchEqlParams = ( - params: EqlRulePatchFields, - existingRule: EqlRuleParams -): EqlSpecificRuleParams => { - return { - type: existingRule.type, - language: params.language ?? existingRule.language, - index: params.index ?? existingRule.index, - dataViewId: params.data_view_id ?? existingRule.dataViewId, - query: params.query ?? existingRule.query, - filters: params.filters ?? existingRule.filters, - timestampField: params.timestamp_field ?? existingRule.timestampField, - eventCategoryOverride: params.event_category_override ?? existingRule.eventCategoryOverride, - tiebreakerField: params.tiebreaker_field ?? existingRule.tiebreakerField, - alertSuppression: - convertAlertSuppressionToCamel(params.alert_suppression) ?? existingRule.alertSuppression, - }; -}; - -const patchEsqlParams = ( - params: EsqlRulePatchFields, - existingRule: EsqlRuleParams -): EsqlSpecificRuleParams => { - return { - type: existingRule.type, - language: params.language ?? existingRule.language, - query: params.query ?? existingRule.query, - alertSuppression: - convertAlertSuppressionToCamel(params.alert_suppression) ?? existingRule.alertSuppression, - }; -}; - -const patchThreatMatchParams = ( - params: ThreatMatchRulePatchFields, - existingRule: ThreatRuleParams -): ThreatSpecificRuleParams => { - return { - type: existingRule.type, - language: params.language ?? existingRule.language, - index: params.index ?? existingRule.index, - dataViewId: params.data_view_id ?? existingRule.dataViewId, - query: params.query ?? existingRule.query, - filters: params.filters ?? existingRule.filters, - savedId: params.saved_id ?? existingRule.savedId, - threatFilters: params.threat_filters ?? existingRule.threatFilters, - threatQuery: params.threat_query ?? existingRule.threatQuery, - threatMapping: params.threat_mapping ?? existingRule.threatMapping, - threatLanguage: params.threat_language ?? existingRule.threatLanguage, - threatIndex: params.threat_index ?? existingRule.threatIndex, - threatIndicatorPath: params.threat_indicator_path ?? existingRule.threatIndicatorPath, - concurrentSearches: params.concurrent_searches ?? existingRule.concurrentSearches, - itemsPerSearch: params.items_per_search ?? existingRule.itemsPerSearch, - alertSuppression: - convertAlertSuppressionToCamel(params.alert_suppression) ?? existingRule.alertSuppression, - }; -}; - -const patchQueryParams = ( - params: QueryRulePatchFields, - existingRule: QueryRuleParams -): QuerySpecificRuleParams => { - return { - type: existingRule.type, - language: params.language ?? existingRule.language, - index: params.index ?? existingRule.index, - dataViewId: params.data_view_id ?? existingRule.dataViewId, - query: params.query ?? existingRule.query, - filters: params.filters ?? existingRule.filters, - savedId: params.saved_id ?? existingRule.savedId, - responseActions: - params.response_actions?.map(transformRuleToAlertResponseAction) ?? - existingRule.responseActions, - alertSuppression: - convertAlertSuppressionToCamel(params.alert_suppression) ?? existingRule.alertSuppression, - }; -}; - -const patchSavedQueryParams = ( - params: SavedQueryRulePatchFields, - existingRule: SavedQueryRuleParams -): SavedQuerySpecificRuleParams => { - return { - type: existingRule.type, - language: params.language ?? existingRule.language, - index: params.index ?? existingRule.index, - dataViewId: params.data_view_id ?? existingRule.dataViewId, - query: params.query ?? existingRule.query, - filters: params.filters ?? existingRule.filters, - savedId: params.saved_id ?? existingRule.savedId, - responseActions: - params.response_actions?.map(transformRuleToAlertResponseAction) ?? - existingRule.responseActions, - alertSuppression: - convertAlertSuppressionToCamel(params.alert_suppression) ?? existingRule.alertSuppression, - }; -}; - -const patchThresholdParams = ( - params: ThresholdRulePatchFields, - existingRule: ThresholdRuleParams -): ThresholdSpecificRuleParams => { - return { - type: existingRule.type, - language: params.language ?? existingRule.language, - index: params.index ?? existingRule.index, - dataViewId: params.data_view_id ?? existingRule.dataViewId, - query: params.query ?? existingRule.query, - filters: params.filters ?? existingRule.filters, - savedId: params.saved_id ?? existingRule.savedId, - threshold: params.threshold - ? normalizeThresholdObject(params.threshold) - : existingRule.threshold, - alertSuppression: params.alert_suppression ?? existingRule.alertSuppression, - }; -}; - -const patchMachineLearningParams = ( - params: MachineLearningRulePatchFields, - existingRule: MachineLearningRuleParams -): MachineLearningSpecificRuleParams => { - return { - type: existingRule.type, - anomalyThreshold: params.anomaly_threshold ?? existingRule.anomalyThreshold, - machineLearningJobId: params.machine_learning_job_id - ? normalizeMachineLearningJobIds(params.machine_learning_job_id) - : existingRule.machineLearningJobId, - alertSuppression: - convertAlertSuppressionToCamel(params.alert_suppression) ?? existingRule.alertSuppression, - }; -}; - -const patchNewTermsParams = ( - params: NewTermsRulePatchFields, - existingRule: NewTermsRuleParams -): NewTermsSpecificRuleParams => { - return { - type: existingRule.type, - language: params.language ?? existingRule.language, - index: params.index ?? existingRule.index, - dataViewId: params.data_view_id ?? existingRule.dataViewId, - query: params.query ?? existingRule.query, - filters: params.filters ?? existingRule.filters, - newTermsFields: params.new_terms_fields ?? existingRule.newTermsFields, - historyWindowStart: params.history_window_start ?? existingRule.historyWindowStart, - alertSuppression: - convertAlertSuppressionToCamel(params.alert_suppression) ?? existingRule.alertSuppression, - }; -}; - -export const patchTypeSpecificSnakeToCamel = ( - params: PatchRuleRequestBody, - existingRule: RuleParams -): TypeSpecificRuleParams => { - // Here we do the validation of patch params by rule type to ensure that the fields that are - // passed in to patch are of the correct type, e.g. `query` is a string. Since the combined patch schema - // is a union of types where everything is optional, it's hard to do the validation before we know the rule type - - // a patch request that defines `event_category_override` as a number would not be assignable to the EQL patch schema, - // but would be assignable to the other rule types since they don't specify `event_category_override`. - switch (existingRule.type) { - case 'eql': { - const result = EqlRulePatchFields.safeParse(params); - if (!result.success) { - throw new BadRequestError(stringifyZodError(result.error)); - } - return patchEqlParams(result.data, existingRule); - } - case 'esql': { - const result = EsqlRulePatchFields.safeParse(params); - if (!result.success) { - throw new BadRequestError(stringifyZodError(result.error)); - } - return patchEsqlParams(result.data, existingRule); - } - case 'threat_match': { - const result = ThreatMatchRulePatchFields.safeParse(params); - if (!result.success) { - throw new BadRequestError(stringifyZodError(result.error)); - } - return patchThreatMatchParams(result.data, existingRule); - } - case 'query': { - const result = QueryRulePatchFields.safeParse(params); - if (!result.success) { - throw new BadRequestError(stringifyZodError(result.error)); - } - return patchQueryParams(result.data, existingRule); - } - case 'saved_query': { - const result = SavedQueryRulePatchFields.safeParse(params); - if (!result.success) { - throw new BadRequestError(stringifyZodError(result.error)); - } - return patchSavedQueryParams(result.data, existingRule); - } - case 'threshold': { - const result = ThresholdRulePatchFields.safeParse(params); - if (!result.success) { - throw new BadRequestError(stringifyZodError(result.error)); - } - return patchThresholdParams(result.data, existingRule); - } - case 'machine_learning': { - const result = MachineLearningRulePatchFields.safeParse(params); - if (!result.success) { - throw new BadRequestError(stringifyZodError(result.error)); - } - return patchMachineLearningParams(result.data, existingRule); - } - case 'new_terms': { - const result = NewTermsRulePatchFields.safeParse(params); - if (!result.success) { - throw new BadRequestError(stringifyZodError(result.error)); - } - return patchNewTermsParams(result.data, existingRule); - } - default: { - return assertUnreachable(existingRule); - } - } -}; - -interface ConvertUpdateAPIToInternalSchemaProps { - existingRule: SanitizedRule; - ruleUpdate: RuleUpdateProps; -} - -export const convertUpdateAPIToInternalSchema = ({ - existingRule, - ruleUpdate, -}: ConvertUpdateAPIToInternalSchemaProps) => { - const alertActions = - ruleUpdate.actions?.map((action) => transformRuleToAlertAction(action)) ?? []; - const actions = transformToActionFrequency(alertActions, ruleUpdate.throttle); - - const typeSpecificParams = typeSpecificSnakeToCamel(ruleUpdate); - - const newInternalRule: InternalRuleUpdate = { - name: ruleUpdate.name, - tags: ruleUpdate.tags ?? [], - params: { - author: ruleUpdate.author ?? [], - buildingBlockType: ruleUpdate.building_block_type, - description: ruleUpdate.description, - ruleId: existingRule.params.ruleId, - falsePositives: ruleUpdate.false_positives ?? [], - from: ruleUpdate.from ?? 'now-6m', - investigationFields: ruleUpdate.investigation_fields, - immutable: existingRule.params.immutable, - ruleSource: convertImmutableToRuleSource(existingRule.params.immutable), - license: ruleUpdate.license, - outputIndex: ruleUpdate.output_index ?? '', - timelineId: ruleUpdate.timeline_id, - timelineTitle: ruleUpdate.timeline_title, - meta: ruleUpdate.meta, - maxSignals: ruleUpdate.max_signals ?? DEFAULT_MAX_SIGNALS, - relatedIntegrations: ruleUpdate.related_integrations ?? [], - requiredFields: addEcsToRequiredFields(ruleUpdate.required_fields), - riskScore: ruleUpdate.risk_score, - riskScoreMapping: ruleUpdate.risk_score_mapping ?? [], - ruleNameOverride: ruleUpdate.rule_name_override, - setup: ruleUpdate.setup, - severity: ruleUpdate.severity, - severityMapping: ruleUpdate.severity_mapping ?? [], - threat: ruleUpdate.threat ?? [], - timestampOverride: ruleUpdate.timestamp_override, - timestampOverrideFallbackDisabled: ruleUpdate.timestamp_override_fallback_disabled, - to: ruleUpdate.to ?? 'now', - references: ruleUpdate.references ?? [], - namespace: ruleUpdate.namespace, - note: ruleUpdate.note, - version: ruleUpdate.version ?? existingRule.params.version, - exceptionsList: ruleUpdate.exceptions_list ?? [], - ...typeSpecificParams, - }, - schedule: { interval: ruleUpdate.interval ?? '5m' }, - actions, - }; - - return newInternalRule; -}; - -// eslint-disable-next-line complexity -export const convertPatchAPIToInternalSchema = ( - nextParams: PatchRuleRequestBody, - existingRule: SanitizedRule -): InternalRuleUpdate => { - const typeSpecificParams = patchTypeSpecificSnakeToCamel(nextParams, existingRule.params); - const existingParams = existingRule.params; - - const alertActions = - nextParams.actions?.map((action) => transformRuleToAlertAction(action)) ?? existingRule.actions; - const throttle = nextParams.throttle ?? transformFromAlertThrottle(existingRule); - const actions = transformToActionFrequency(alertActions, throttle); - - return { - name: nextParams.name ?? existingRule.name, - tags: nextParams.tags ?? existingRule.tags, - params: { - author: nextParams.author ?? existingParams.author, - buildingBlockType: nextParams.building_block_type ?? existingParams.buildingBlockType, - description: nextParams.description ?? existingParams.description, - ruleId: existingParams.ruleId, - falsePositives: nextParams.false_positives ?? existingParams.falsePositives, - investigationFields: nextParams.investigation_fields ?? existingParams.investigationFields, - from: nextParams.from ?? existingParams.from, - immutable: existingParams.immutable, - ruleSource: convertImmutableToRuleSource(existingParams.immutable), - license: nextParams.license ?? existingParams.license, - outputIndex: nextParams.output_index ?? existingParams.outputIndex, - timelineId: nextParams.timeline_id ?? existingParams.timelineId, - timelineTitle: nextParams.timeline_title ?? existingParams.timelineTitle, - meta: nextParams.meta ?? existingParams.meta, - maxSignals: nextParams.max_signals ?? existingParams.maxSignals, - relatedIntegrations: nextParams.related_integrations ?? existingParams.relatedIntegrations, - requiredFields: addEcsToRequiredFields(nextParams.required_fields), - riskScore: nextParams.risk_score ?? existingParams.riskScore, - riskScoreMapping: nextParams.risk_score_mapping ?? existingParams.riskScoreMapping, - ruleNameOverride: nextParams.rule_name_override ?? existingParams.ruleNameOverride, - setup: nextParams.setup ?? existingParams.setup, - severity: nextParams.severity ?? existingParams.severity, - severityMapping: nextParams.severity_mapping ?? existingParams.severityMapping, - threat: nextParams.threat ?? existingParams.threat, - timestampOverride: nextParams.timestamp_override ?? existingParams.timestampOverride, - timestampOverrideFallbackDisabled: - nextParams.timestamp_override_fallback_disabled ?? - existingParams.timestampOverrideFallbackDisabled, - to: nextParams.to ?? existingParams.to, - references: nextParams.references ?? existingParams.references, - namespace: nextParams.namespace ?? existingParams.namespace, - note: nextParams.note ?? existingParams.note, - version: nextParams.version ?? existingParams.version, - exceptionsList: nextParams.exceptions_list ?? existingParams.exceptionsList, - ...typeSpecificParams, - }, - schedule: { interval: nextParams.interval ?? existingRule.schedule.interval }, - actions, - }; -}; - -interface RuleCreateOptions { - immutable?: boolean; - defaultEnabled?: boolean; -} - -// eslint-disable-next-line complexity -export const convertCreateAPIToInternalSchema = ( - input: RuleCreateProps, - options?: RuleCreateOptions -): InternalRuleCreate => { - const { immutable = false, defaultEnabled = true } = options ?? {}; - - const typeSpecificParams = typeSpecificSnakeToCamel(input); - const newRuleId = input.rule_id ?? uuidv4(); - - const alertActions = input.actions?.map((action) => transformRuleToAlertAction(action)) ?? []; - const actions = transformToActionFrequency(alertActions, input.throttle); - - return { - name: input.name, - tags: input.tags ?? [], - alertTypeId: ruleTypeMappings[input.type], - consumer: SERVER_APP_ID, - params: { - author: input.author ?? [], - buildingBlockType: input.building_block_type, - description: input.description, - ruleId: newRuleId, - falsePositives: input.false_positives ?? [], - investigationFields: input.investigation_fields, - from: input.from ?? DEFAULT_FROM, - immutable, - ruleSource: convertImmutableToRuleSource(immutable), - license: input.license, - outputIndex: input.output_index ?? '', - timelineId: input.timeline_id, - timelineTitle: input.timeline_title, - meta: input.meta, - maxSignals: input.max_signals ?? DEFAULT_MAX_SIGNALS, - riskScore: input.risk_score, - riskScoreMapping: input.risk_score_mapping ?? [], - ruleNameOverride: input.rule_name_override, - severity: input.severity, - severityMapping: input.severity_mapping ?? [], - threat: input.threat ?? [], - timestampOverride: input.timestamp_override, - timestampOverrideFallbackDisabled: input.timestamp_override_fallback_disabled, - to: input.to ?? DEFAULT_TO, - references: input.references ?? [], - namespace: input.namespace, - note: input.note, - version: input.version ?? 1, - exceptionsList: input.exceptions_list ?? [], - relatedIntegrations: input.related_integrations ?? [], - requiredFields: addEcsToRequiredFields(input.required_fields), - setup: input.setup ?? '', - ...typeSpecificParams, - }, - schedule: { interval: input.interval ?? '5m' }, - enabled: input.enabled ?? defaultEnabled, - actions, - }; -}; - -// Converts the internal rule data structure to the response API schema -export const typeSpecificCamelToSnake = ( - params: TypeSpecificRuleParams -): RequiredOptional => { - switch (params.type) { - case 'eql': { - return { - type: params.type, - language: params.language, - index: params.index, - data_view_id: params.dataViewId, - query: params.query, - filters: params.filters, - timestamp_field: params.timestampField, - event_category_override: params.eventCategoryOverride, - tiebreaker_field: params.tiebreakerField, - alert_suppression: convertAlertSuppressionToSnake(params.alertSuppression), - }; - } - case 'esql': { - return { - type: params.type, - language: params.language, - query: params.query, - alert_suppression: convertAlertSuppressionToSnake(params.alertSuppression), - }; - } - case 'threat_match': { - return { - type: params.type, - language: params.language, - index: params.index, - data_view_id: params.dataViewId, - query: params.query, - filters: params.filters, - saved_id: params.savedId, - threat_filters: params.threatFilters, - threat_query: params.threatQuery, - threat_mapping: params.threatMapping, - threat_language: params.threatLanguage, - threat_index: params.threatIndex, - threat_indicator_path: params.threatIndicatorPath, - concurrent_searches: params.concurrentSearches, - items_per_search: params.itemsPerSearch, - alert_suppression: convertAlertSuppressionToSnake(params.alertSuppression), - }; - } - case 'query': { - return { - type: params.type, - language: params.language, - index: params.index, - data_view_id: params.dataViewId, - query: params.query, - filters: params.filters, - saved_id: params.savedId, - response_actions: params.responseActions?.map(transformAlertToRuleResponseAction), - alert_suppression: convertAlertSuppressionToSnake(params.alertSuppression), - }; - } - case 'saved_query': { - return { - type: params.type, - language: params.language, - index: params.index, - query: params.query, - filters: params.filters, - saved_id: params.savedId, - data_view_id: params.dataViewId, - response_actions: params.responseActions?.map(transformAlertToRuleResponseAction), - alert_suppression: convertAlertSuppressionToSnake(params.alertSuppression), - }; - } - case 'threshold': { - return { - type: params.type, - language: params.language, - index: params.index, - data_view_id: params.dataViewId, - query: params.query, - filters: params.filters, - saved_id: params.savedId, - threshold: params.threshold, - alert_suppression: params.alertSuppression?.duration - ? { duration: params.alertSuppression?.duration } - : undefined, - }; - } - case 'machine_learning': { - return { - type: params.type, - anomaly_threshold: params.anomalyThreshold, - machine_learning_job_id: params.machineLearningJobId, - alert_suppression: convertAlertSuppressionToSnake(params.alertSuppression), - }; - } - case 'new_terms': { - return { - type: params.type, - query: params.query, - new_terms_fields: params.newTermsFields, - history_window_start: params.historyWindowStart, - index: params.index, - filters: params.filters, - language: params.language, - data_view_id: params.dataViewId, - alert_suppression: convertAlertSuppressionToSnake(params.alertSuppression), - }; - } - default: { - return assertUnreachable(params); - } - } -}; - -// TODO: separate out security solution defined common params from Alerting framework common params -// so we can explicitly specify the return type of this function -export const commonParamsCamelToSnake = (params: BaseRuleParams) => { - return { - description: params.description, - risk_score: params.riskScore, - severity: params.severity, - building_block_type: params.buildingBlockType, - namespace: params.namespace, - note: params.note, - license: params.license, - output_index: params.outputIndex, - timeline_id: params.timelineId, - timeline_title: params.timelineTitle, - meta: params.meta, - rule_name_override: params.ruleNameOverride, - timestamp_override: params.timestampOverride, - timestamp_override_fallback_disabled: params.timestampOverrideFallbackDisabled, - investigation_fields: migrateLegacyInvestigationFields(params.investigationFields), - author: params.author, - false_positives: params.falsePositives, - from: params.from, - rule_id: params.ruleId, - max_signals: params.maxSignals, - risk_score_mapping: params.riskScoreMapping, - severity_mapping: params.severityMapping, - threat: params.threat, - to: params.to, - references: params.references, - version: params.version, - exceptions_list: params.exceptionsList, - immutable: params.immutable, - rule_source: convertObjectKeysToSnakeCase(params.ruleSource), - related_integrations: params.relatedIntegrations ?? [], - required_fields: params.requiredFields ?? [], - setup: params.setup ?? '', - }; -}; - -export const internalRuleToAPIResponse = ( - rule: SanitizedRule | ResolvedSanitizedRule -): RequiredOptional => { - const executionSummary = createRuleExecutionSummary(rule); - - const isResolvedRule = (obj: unknown): obj is ResolvedSanitizedRule => - (obj as ResolvedSanitizedRule).outcome != null; - - const alertActions = rule.actions.map(transformAlertToRuleAction); - const throttle = transformFromAlertThrottle(rule); - const actions = transformToActionFrequency(alertActions, throttle); - - return { - // saved object properties - outcome: isResolvedRule(rule) ? rule.outcome : undefined, - alias_target_id: isResolvedRule(rule) ? rule.alias_target_id : undefined, - alias_purpose: isResolvedRule(rule) ? rule.alias_purpose : undefined, - // Alerting framework params - id: rule.id, - updated_at: rule.updatedAt.toISOString(), - updated_by: rule.updatedBy ?? 'elastic', - created_at: rule.createdAt.toISOString(), - created_by: rule.createdBy ?? 'elastic', - name: rule.name, - tags: rule.tags, - interval: rule.schedule.interval, - enabled: rule.enabled, - revision: rule.revision, - // Security solution shared rule params - ...commonParamsCamelToSnake(rule.params), - // Type specific security solution rule params - ...typeSpecificCamelToSnake(rule.params), - // Actions - throttle: undefined, - actions, - // Execution summary - execution_summary: executionSummary ?? undefined, - }; -}; - -export const convertPrebuiltRuleAssetToRuleResponse = ( - prebuiltRuleAsset: PrebuiltRuleAsset -): RuleResponse => { - const prebuiltRuleAssetDefaults = { - enabled: false, - risk_score_mapping: [], - severity_mapping: [], - interval: DEFAULT_INTERVAL, - to: DEFAULT_TO, - from: DEFAULT_FROM, - exceptions_list: [], - false_positives: [], - max_signals: DEFAULT_MAX_SIGNALS, - actions: [], - related_integrations: [], - required_fields: [], - setup: '', - note: '', - references: [], - threat: [], - tags: [], - author: [], - }; - - const immutable = true; - - const ruleResponseSpecificFields = { - id: uuidv4(), - updated_at: new Date(0).toISOString(), - updated_by: '', - created_at: new Date(0).toISOString(), - created_by: '', - immutable, - rule_source: convertObjectKeysToSnakeCase(convertImmutableToRuleSource(immutable)), - revision: 1, - }; - - return RuleResponse.parse({ - ...prebuiltRuleAssetDefaults, - ...prebuiltRuleAsset, - required_fields: addEcsToRequiredFields(prebuiltRuleAsset.required_fields), - ...ruleResponseSpecificFields, - }); -}; - -export const convertImmutableToRuleSource = (immutable: boolean): RuleSourceCamelCased => { - if (immutable) { - return { - type: 'external', - isCustomized: false, - }; - } - - return { - type: 'internal', - }; -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/utils.test.ts index 61436a04c267..536a314fa6c0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/utils.test.ts @@ -38,7 +38,7 @@ import { getMlRuleParams, getQueryRuleParams, getThreatRuleParams } from '../../ import { createRulesAndExceptionsStreamFromNdJson } from '../logic/import/create_rules_stream_from_ndjson'; import type { RuleExceptionsPromiseFromStreams } from '../logic/import/import_rules_utils'; -import { internalRuleToAPIResponse } from '../normalization/rule_converters'; +import { internalRuleToAPIResponse } from '../logic/detection_rules_client/converters/internal_rule_to_api_response'; type PromiseFromStreams = RuleToImport | Error; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/utils.ts index 66fa635e768a..bf6227ddcbfe 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/utils.ts @@ -17,8 +17,6 @@ import type { SavedObjectsClientContract } from '@kbn/core/server'; import type { RuleAction } from '@kbn/securitysolution-io-ts-alerting-types'; import type { - AlertSuppression, - AlertSuppressionCamel, InvestigationFields, RequiredField, RequiredFieldInput, @@ -33,7 +31,7 @@ import type { BulkError, OutputError } from '../../routes/utils'; import { createBulkErrorObject } from '../../routes/utils'; import type { InvestigationFieldsCombined, RuleAlertType, RuleParams } from '../../rule_schema'; import { hasValidRuleType } from '../../rule_schema'; -import { internalRuleToAPIResponse } from '../normalization/rule_converters'; +import { internalRuleToAPIResponse } from '../logic/detection_rules_client/converters/internal_rule_to_api_response'; type PromiseFromStreams = RuleToImport | Error; const MAX_CONCURRENT_SEARCHES = 10; @@ -347,28 +345,6 @@ export const getInvalidConnectors = async ( return [Array.from(errors.values()), Array.from(rulesAcc.values())]; }; -export const convertAlertSuppressionToCamel = ( - input: AlertSuppression | undefined -): AlertSuppressionCamel | undefined => - input - ? { - groupBy: input.group_by, - duration: input.duration, - missingFieldsStrategy: input.missing_fields_strategy, - } - : undefined; - -export const convertAlertSuppressionToSnake = ( - input: AlertSuppressionCamel | undefined -): AlertSuppression | undefined => - input - ? { - group_by: input.groupBy, - duration: input.duration, - missing_fields_strategy: input.missingFieldsStrategy, - } - : undefined; - /** * In ESS 8.10.x "investigation_fields" are mapped as string[]. * For 8.11+ logic is added on read in our endpoints to migrate diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.ts index 298b7f62d297..dd77122ac456 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.ts @@ -31,7 +31,7 @@ import { type UnifiedQueryRuleParams, } from '../../rule_schema'; import { type BulkError, createBulkErrorObject } from '../../routes/utils'; -import { internalRuleToAPIResponse } from '../normalization/rule_converters'; +import { internalRuleToAPIResponse } from '../logic/detection_rules_client/converters/internal_rule_to_api_response'; export const transformValidateBulkError = ( ruleId: string, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts index c2faa464b75d..7b48e32bf996 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts @@ -23,6 +23,7 @@ import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; import { DEFAULT_PREVIEW_INDEX, DETECTION_ENGINE_RULES_PREVIEW, + SERVER_APP_ID, } from '../../../../../../common/constants'; import { validateCreateRuleProps } from '../../../../../../common/api/detection_engine/rule_management'; import { RuleExecutionStatusEnum } from '../../../../../../common/api/detection_engine/rule_monitoring'; @@ -34,7 +35,6 @@ import { PreviewRulesSchema } from '../../../../../../common/api/detection_engin import type { StartPlugins, SetupPlugins } from '../../../../../plugin'; import { buildSiemResponse } from '../../../routes/utils'; -import { convertCreateAPIToInternalSchema } from '../../../rule_management'; import type { RuleParams } from '../../../rule_schema'; import { createPreviewRuleExecutionLogger } from './preview_rule_execution_logger'; import { parseInterval } from '../../../rule_types/utils/utils'; @@ -64,6 +64,8 @@ import { createSecurityRuleTypeWrapper } from '../../../rule_types/create_securi import { assertUnreachable } from '../../../../../../common/utility_types'; import { wrapScopedClusterClient } from './wrap_scoped_cluster_client'; import { wrapSearchSourceClient } from './wrap_search_source_client'; +import { applyRuleDefaults } from '../../../rule_management/logic/detection_rules_client/mergers/apply_rule_defaults'; +import { convertRuleResponseToAlertingRule } from '../../../rule_management/logic/detection_rules_client/converters/convert_rule_response_to_alerting_rule'; const PREVIEW_TIMEOUT_SECONDS = 60; const MAX_ROUTE_CONCURRENCY = 10; @@ -118,7 +120,7 @@ export const previewRulesRoute = ( }); } - const internalRule = convertCreateAPIToInternalSchema(request.body); + const internalRule = convertRuleResponseToAlertingRule(applyRuleDefaults(request.body)); const previewRuleParams = internalRule.params; const mlAuthz = buildMlAuthz({ @@ -237,6 +239,8 @@ export const previewRulesRoute = ( createdAt: new Date(), createdBy: username ?? 'preview-created-by', producer: 'preview-producer', + consumer: SERVER_APP_ID, + enabled: true, revision: 0, ruleTypeId, ruleTypeName, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.ts index b3000edf895d..5d5065170def 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.ts @@ -5,11 +5,6 @@ * 2.0. */ import type { SanitizedRuleConfig } from '@kbn/alerting-plugin/common'; -import type { - RuleActionArrayCamel, - RuleActionNotifyWhen, - RuleActionThrottle, -} from '@kbn/securitysolution-io-ts-alerting-types'; import type { EQL_RULE_TYPE_ID, ESQL_RULE_TYPE_ID, @@ -22,12 +17,9 @@ import type { THRESHOLD_RULE_TYPE_ID, } from '@kbn/securitysolution-rules'; import * as z from 'zod'; +import type { CreateRuleData } from '@kbn/alerting-plugin/server/application/rule/methods/create'; +import type { UpdateRuleData } from '@kbn/alerting-plugin/server/application/rule/methods/update'; import { RuleResponseAction } from '../../../../../common/api/detection_engine'; -import type { - IsRuleEnabled, - RuleName, - RuleTagArray, -} from '../../../../../common/api/detection_engine/model/rule_schema'; import { AlertsIndex, AlertsIndexNamespace, @@ -334,29 +326,8 @@ export type AllRuleTypes = | typeof THRESHOLD_RULE_TYPE_ID | typeof NEW_TERMS_RULE_TYPE_ID; -export interface InternalRuleCreate { - name: RuleName; - tags: RuleTagArray; - alertTypeId: AllRuleTypes; +export type InternalRuleCreate = CreateRuleData & { consumer: typeof SERVER_APP_ID; - schedule: { - interval: string; - }; - enabled: IsRuleEnabled; - actions: RuleActionArrayCamel; - params: RuleParams; - throttle?: RuleActionThrottle | null; - notifyWhen?: RuleActionNotifyWhen | null; -} +}; -export interface InternalRuleUpdate { - name: RuleName; - tags: RuleTagArray; - schedule: { - interval: string; - }; - actions: RuleActionArrayCamel; - params: RuleParams; - throttle?: RuleActionThrottle | null; - notifyWhen?: RuleActionNotifyWhen | null; -} +export type InternalRuleUpdate = UpdateRuleData; diff --git a/x-pack/plugins/security_solution/server/request_context_factory.ts b/x-pack/plugins/security_solution/server/request_context_factory.ts index 1f36f7ecff23..ea1673fe9a5d 100644 --- a/x-pack/plugins/security_solution/server/request_context_factory.ts +++ b/x-pack/plugins/security_solution/server/request_context_factory.ts @@ -122,10 +122,11 @@ export class RequestContextFactory implements IRequestContextFactory { savedObjectsClient: coreContext.savedObjects.client, }); - return createDetectionRulesClient( - startPlugins.alerting.getRulesClientWithRequest(request), - mlAuthz - ); + return createDetectionRulesClient({ + rulesClient: startPlugins.alerting.getRulesClientWithRequest(request), + savedObjectsClient: coreContext.savedObjects.client, + mlAuthz, + }); }), getDetectionEngineHealthClient: memoize(() => diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/exceptions/workflows/basic_license_essentials_tier/create_rule_exceptions_ess.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/exceptions/workflows/basic_license_essentials_tier/create_rule_exceptions_ess.ts index 940696410617..a1708cf4d6ae 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/exceptions/workflows/basic_license_essentials_tier/create_rule_exceptions_ess.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/exceptions/workflows/basic_license_essentials_tier/create_rule_exceptions_ess.ts @@ -14,7 +14,6 @@ import { getRuleSOById, createRuleThroughAlertingEndpoint, getRuleSavedObjectWithLegacyInvestigationFields, - checkInvestigationFieldSoValue, } from '../../../../utils'; import { createAlertsIndex, @@ -79,25 +78,15 @@ export default ({ getService }: FtrProviderContext) => { }) .expect(200); - /** - * Confirm type on SO so that it's clear in the tests whether it's expected that - * the SO itself is migrated to the inteded object type, or if the transformation is - * happening just on the response. In this case, change will - * NOT include a migration on SO. - */ const { hits: { hits: [{ _source: ruleSO }], }, } = await getRuleSOById(es, ruleWithLegacyInvestigationField.id); - const isInvestigationFieldMigratedInSo = await checkInvestigationFieldSoValue(ruleSO, { - field_names: ['client.address', 'agent.name'], - }); expect( ruleSO?.alert.params.exceptionsList.some((list) => list.type === 'rule_default') ).to.eql(true); - expect(isInvestigationFieldMigratedInSo).to.eql(false); }); }); }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/trial_license_complete_tier/patch_rules_bulk.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/trial_license_complete_tier/patch_rules_bulk.ts index 7e496ea73194..947b191469e3 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/trial_license_complete_tier/patch_rules_bulk.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/trial_license_complete_tier/patch_rules_bulk.ts @@ -532,7 +532,7 @@ export default ({ getService }: FtrProviderContext) => { ); }); - it('should patch a rule with a legacy investigation field and transform field in response', async () => { + it('should patch a rule with a legacy investigation field and migrate field', async () => { // patch a simple rule's name const { body } = await securitySolutionApi .bulkPatchRules({ @@ -548,19 +548,13 @@ export default ({ getService }: FtrProviderContext) => { }); expect(bodyToCompareLegacyField.name).to.eql('some other name'); - /** - * Confirm type on SO so that it's clear in the tests whether it's expected that - * the SO itself is migrated to the inteded object type, or if the transformation is - * happening just on the response. In this case, change should - * NOT include a migration on SO. - */ const isInvestigationFieldMigratedInSo = await checkInvestigationFieldSoValue( undefined, { field_names: ['client.address', 'agent.name'] }, es, body[0].id ); - expect(isInvestigationFieldMigratedInSo).to.eql(false); + expect(isInvestigationFieldMigratedInSo).to.eql(true); }); it('should patch a rule with a legacy investigation field - empty array - and transform field in response', async () => { diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/trial_license_complete_tier/patch_rules_ess.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/trial_license_complete_tier/patch_rules_ess.ts index 30398cd2cd1e..d28358519e30 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/trial_license_complete_tier/patch_rules_ess.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/trial_license_complete_tier/patch_rules_ess.ts @@ -137,7 +137,7 @@ export default ({ getService }: FtrProviderContext) => { ); }); - it('should patch a rule with a legacy investigation field and transform response', async () => { + it('should patch a rule with a legacy investigation field and migrate field', async () => { const { body } = await supertest .patch(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') @@ -152,12 +152,7 @@ export default ({ getService }: FtrProviderContext) => { expect(bodyToCompare.investigation_fields).to.eql({ field_names: ['client.address', 'agent.name'], }); - /** - * Confirm type on SO so that it's clear in the tests whether it's expected that - * the SO itself is migrated to the inteded object type, or if the transformation is - * happening just on the response. In this case, change should - * NOT include a migration on SO. - */ + const isInvestigationFieldMigratedInSo = await checkInvestigationFieldSoValue( undefined, { @@ -166,7 +161,7 @@ export default ({ getService }: FtrProviderContext) => { es, body.id ); - expect(isInvestigationFieldMigratedInSo).to.eql(false); + expect(isInvestigationFieldMigratedInSo).to.eql(true); }); it('should patch a rule with a legacy investigation field - empty array - and transform response', async () => {