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 e460581c02a1c..448df6b581a3b 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 @@ -277,6 +277,27 @@ describe('DetectionRulesClient.patchRule', () => { expect(rulesClient.create).not.toHaveBeenCalled(); }); + it('throws an error if rule has external rule source and non-customizable fields are changed', async () => { + // Mock the existing rule + const existingRule = { + ...getRulesSchemaMock(), + rule_source: { type: 'external', is_customized: true }, + }; + (getRuleByRuleId as jest.Mock).mockResolvedValueOnce(existingRule); + + // Mock the rule update + const rulePatch = getCreateRulesSchemaMock('query-rule-id'); + rulePatch.license = 'new license'; + + // 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 expect(detectionRulesClient.patchRule({ rulePatch })).rejects.toThrow( + 'Cannot update "license" field for prebuilt rules' + ); + }); + describe('actions', () => { it("updates the rule's actions if provided", async () => { // Mock the existing rule 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 a660e5c5e8747..cbd0fb1fe3680 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 @@ -498,5 +498,26 @@ describe('DetectionRulesClient.updateRule', () => { }) ); }); + + it('throws an error if rule has external rule source and non-customizable fields are changed', async () => { + // Mock the existing rule + const existingRule = { + ...getRulesSchemaMock(), + rule_source: { type: 'external', is_customized: true }, + }; + + (getRuleByRuleId as jest.Mock).mockResolvedValueOnce(existingRule); + + // Mock the rule update + const ruleUpdate = { ...getCreateRulesSchemaMock(), author: ['new user'] }; + + // 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 expect(detectionRulesClient.updateRule({ ruleUpdate })).rejects.toThrow( + 'Cannot update "author" field for prebuilt rules' + ); + }); }); }); 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 1218991bf388e..fe7ec902d6153 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 @@ -16,6 +16,7 @@ import type { MlAuthz } from '../../../../../machine_learning/authz'; 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 { validateNonCustomizablePatchFields } from '../../../utils/validate'; 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'; @@ -51,6 +52,11 @@ export const patchRule = async ({ await validateMlAuth(mlAuthz, rulePatch.type ?? existingRule.type); + // We don't allow non-customizable fields to be changed for prebuilt rules + if (existingRule.rule_source && existingRule.rule_source.type === 'external') { + validateNonCustomizablePatchFields(rulePatch, existingRule); + } + const patchedRule = await applyRulePatch({ prebuiltRuleAssetClient, existingRule, 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 cd84788026870..91f909c312fdb 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 @@ -11,6 +11,7 @@ import type { RuleResponse } from '../../../../../../../common/api/detection_eng import type { MlAuthz } from '../../../../../machine_learning/authz'; import { applyRuleUpdate } from '../mergers/apply_rule_update'; import { getIdError } from '../../../utils/utils'; +import { validateNonCustomizableUpdateFields } from '../../../utils/validate'; import { convertRuleResponseToAlertingRule } from '../converters/convert_rule_response_to_alerting_rule'; import { ClientError, toggleRuleEnabledOnUpdate, validateMlAuth } from '../utils'; @@ -50,6 +51,11 @@ export const updateRule = async ({ throw new ClientError(error.message, error.statusCode); } + // We don't allow non-customizable fields to be changed for prebuilt rules + if (existingRule.rule_source && existingRule.rule_source.type === 'external') { + validateNonCustomizableUpdateFields(ruleUpdate, existingRule); + } + const ruleWithUpdates = await applyRuleUpdate({ prebuiltRuleAssetClient, existingRule, 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 96aaef64b57c9..e077e81e33fbb 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 @@ -14,6 +14,7 @@ import type { EsqlRule, NewTermsRule, QueryRule, + RulePatchProps, } from '../../../../../common/api/detection_engine'; import { type ResponseAction, @@ -38,6 +39,7 @@ import { } from '../../rule_schema'; import { type BulkError, createBulkErrorObject } from '../../routes/utils'; import { internalRuleToAPIResponse } from '../logic/detection_rules_client/converters/internal_rule_to_api_response'; +import { ClientError } from '../logic/detection_rules_client/utils'; export const transformValidateBulkError = ( ruleId: string, @@ -128,3 +130,25 @@ function ruleObjectContainsResponseActions( ): rule is Rule { return rule != null && 'params' in rule && 'responseActions' in rule?.params; } + +export const validateNonCustomizableUpdateFields = ( + ruleUpdate: RuleUpdateProps, + existingRule: RuleResponse +) => { + if (!isEqual(ruleUpdate.author, existingRule.author)) { + throw new ClientError(`Cannot update "author" field for prebuilt rules`, 400); + } else if (ruleUpdate.license !== existingRule.license) { + throw new ClientError(`Cannot update "license" field for prebuilt rules`, 400); + } +}; + +export const validateNonCustomizablePatchFields = ( + rulePatch: RulePatchProps, + existingRule: RuleResponse +) => { + if (rulePatch.author && !isEqual(rulePatch.author, existingRule.author)) { + throw new ClientError(`Cannot update "author" field for prebuilt rules`, 400); + } else if (rulePatch.license && rulePatch.license !== existingRule.license) { + throw new ClientError(`Cannot update "license" field for prebuilt rules`, 400); + } +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules.ts index a567eb78a776d..41f207c90f319 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules.ts @@ -16,6 +16,9 @@ import { removeServerGeneratedPropertiesIncludingRuleId, getSimpleRuleOutputWithoutRuleId, updateUsername, + createHistoricalPrebuiltRuleAssetSavedObjects, + installPrebuiltRules, + createRuleAssetSavedObject, } from '../../../utils'; import { createAlertsIndex, @@ -238,6 +241,25 @@ export default ({ getService }: FtrProviderContext) => { }); }); + it('throws an error if rule has external rule source and non-customizable fields are changed', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects(es, [ + createRuleAssetSavedObject({ rule_id: 'rule-1', author: ['elastic'] }), + ]); + await installPrebuiltRules(es, supertest); + + const { body } = await securitySolutionApi + .patchRule({ + body: { + rule_id: 'rule-1', + author: ['new user'], + }, + }) + .expect(400); + + expect(body.message).toEqual('Cannot update "author" field for prebuilt rules'); + }); + describe('max signals', () => { afterEach(async () => { await deleteAllRules(supertest, log); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules_bulk.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules_bulk.ts index 086909fc4945b..7929b912768ff 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules_bulk.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules_bulk.ts @@ -16,6 +16,9 @@ import { getSimpleRuleOutputWithoutRuleId, removeServerGeneratedPropertiesIncludingRuleId, updateUsername, + createHistoricalPrebuiltRuleAssetSavedObjects, + installPrebuiltRules, + createRuleAssetSavedObject, } from '../../../utils'; import { createAlertsIndex, @@ -347,6 +350,41 @@ export default ({ getService }: FtrProviderContext) => { }, ]); }); + + it('throws an error if rule has external rule source and non-customizable fields are changed', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects(es, [ + createRuleAssetSavedObject({ rule_id: 'rule-1', author: ['elastic'] }), + createRuleAssetSavedObject({ rule_id: 'rule-2', license: 'basic' }), + ]); + await installPrebuiltRules(es, supertest); + + const { body } = await securitySolutionApi + .bulkPatchRules({ + body: [ + { rule_id: 'rule-1', author: ['new user'] }, + { rule_id: 'rule-2', license: 'new license' }, + ], + }) + .expect(200); + + expect([body[0], body[1]]).toEqual([ + { + error: { + message: 'Cannot update "author" field for prebuilt rules', + status_code: 400, + }, + rule_id: 'rule-1', + }, + { + error: { + message: 'Cannot update "license" field for prebuilt rules', + status_code: 400, + }, + rule_id: 'rule-2', + }, + ]); + }); }); }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules.ts index 60e7bfe3ff88f..c84236a14eb37 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules.ts @@ -18,6 +18,9 @@ import { getSimpleMlRuleUpdate, getSimpleRule, updateUsername, + createHistoricalPrebuiltRuleAssetSavedObjects, + installPrebuiltRules, + createRuleAssetSavedObject, } from '../../../utils'; import { createAlertsIndex, @@ -309,6 +312,33 @@ export default ({ getService }: FtrProviderContext) => { expect(updatedRuleResponse).toMatchObject(expectedRule); }); }); + + it('throws an error if rule has external rule source and non-customizable fields are changed', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects(es, [ + createRuleAssetSavedObject({ rule_id: 'rule-1', license: 'elastic' }), + ]); + await installPrebuiltRules(es, supertest); + + const { body: existingRule } = await securitySolutionApi + .readRule({ + query: { rule_id: 'rule-1' }, + }) + .expect(200); + + const { body } = await securitySolutionApi + .updateRule({ + body: getCustomQueryRuleParams({ + ...existingRule, + rule_id: 'rule-1', + id: undefined, + license: 'new license', + }), + }) + .expect(400); + + expect(body.message).toEqual('Cannot update "license" field for prebuilt rules'); + }); }); }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules_bulk.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules_bulk.ts index f9faee0481bf6..cdca9e3ca6e1a 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules_bulk.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules_bulk.ts @@ -17,6 +17,9 @@ import { getSimpleRuleUpdate, getSimpleRule, updateUsername, + createHistoricalPrebuiltRuleAssetSavedObjects, + installPrebuiltRules, + createRuleAssetSavedObject, } from '../../../utils'; import { createAlertsIndex, @@ -370,6 +373,30 @@ export default ({ getService }: FtrProviderContext) => { }, ]); }); + + it('throws an error if rule has external rule source and non-customizable fields are changed', async () => { + // Install base prebuilt detection rule + await createHistoricalPrebuiltRuleAssetSavedObjects(es, [ + createRuleAssetSavedObject({ rule_id: 'rule-1', author: ['elastic'] }), + ]); + await installPrebuiltRules(es, supertest); + + const { body } = await securitySolutionApi + .bulkUpdateRules({ + body: [getCustomQueryRuleParams({ rule_id: 'rule-1', author: ['new user'] })], + }) + .expect(200); + + expect([body[0]]).toEqual([ + { + error: { + message: 'Cannot update "author" field for prebuilt rules', + status_code: 400, + }, + rule_id: 'rule-1', + }, + ]); + }); }); }); };