From d596631d882ffdabf2a403f66705d3dc407688d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Filho?= Date: Tue, 10 Feb 2026 09:38:07 -0300 Subject: [PATCH 1/2] fix: extract firewall behaviors schema and add runtime validation - Extract firewallRulesBehaviorsSchema from inline definition in azionConfigSchema - Add runtime validation to enforce runFunction as first behavior when using multiple behaviors - Restructure criteria schema error messages for better clarity - Add maxItems constraint (10) for behaviors array with runFunction --- .../src/configProcessor/helpers/schema.ts | 333 +++++++++--------- .../secure/firewallProcessConfigStrategy.ts | 9 + 2 files changed, 172 insertions(+), 170 deletions(-) diff --git a/packages/config/src/configProcessor/helpers/schema.ts b/packages/config/src/configProcessor/helpers/schema.ts index 4192686e..80483677 100644 --- a/packages/config/src/configProcessor/helpers/schema.ts +++ b/packages/config/src/configProcessor/helpers/schema.ts @@ -486,6 +486,161 @@ const schemaKV = { }, }; +const firewallRulesBehaviorsSchema = { + type: 'array', + minItems: 1, + anyOf: [ + { + maxItems: 1, + }, + { + // When there are 2+ behaviors, at least one must be runFunction + // Note: JSON Schema strict mode doesn't support tuple validation to enforce + // that runFunction must be the FIRST item. This validation should be added + // in runtime validation after AJV schema validation passes. + minItems: 2, + maxItems: 10, + contains: { + type: 'object', + required: ['runFunction'], + }, + }, + ], + items: { + type: 'object', + oneOf: [ + { + properties: { + runFunction: { + type: ['string', 'number'], + errorMessage: "The 'runFunction' behavior must be a string or number", + }, + }, + required: ['runFunction'], + additionalProperties: false, + }, + { + properties: { + setWafRuleset: { + type: 'object', + properties: { + wafMode: { + type: 'string', + enum: FIREWALL_WAF_MODES, + errorMessage: `The wafMode must be one of: ${FIREWALL_WAF_MODES.join(', ')}`, + }, + wafId: { + type: ['string', 'number'], + errorMessage: 'The wafId must be a string or number', + }, + }, + required: ['wafMode', 'wafId'], + additionalProperties: false, + errorMessage: { + additionalProperties: 'No additional properties are allowed in the setWafRuleset object', + required: "Both 'wafMode' and 'wafId' fields are required in setWafRuleset", + }, + }, + }, + required: ['setWafRuleset'], + additionalProperties: false, + }, + { + properties: { + setRateLimit: { + type: 'object', + properties: { + type: { + type: 'string', + enum: FIREWALL_RATE_LIMIT_TYPES, + errorMessage: `The rate limit type must be one of: ${FIREWALL_RATE_LIMIT_TYPES.join(', ')}`, + }, + limitBy: { + type: 'string', + enum: FIREWALL_RATE_LIMIT_BY, + errorMessage: `The rate limit must be applied by one of: ${FIREWALL_RATE_LIMIT_BY.join(', ')}`, + }, + averageRateLimit: { + type: 'string', + errorMessage: 'The averageRateLimit must be a string', + }, + maximumBurstSize: { + type: 'string', + errorMessage: 'The maximumBurstSize must be a string', + }, + }, + required: ['type', 'limitBy', 'averageRateLimit', 'maximumBurstSize'], + additionalProperties: false, + errorMessage: { + additionalProperties: 'No additional properties are allowed in the setRateLimit object', + required: + "All fields ('type', 'limitBy', 'averageRateLimit', 'maximumBurstSize') are required in setRateLimit", + }, + }, + }, + required: ['setRateLimit'], + additionalProperties: false, + }, + { + properties: { + deny: { + type: 'boolean', + const: true, + errorMessage: 'The deny behavior must be true', + }, + }, + required: ['deny'], + additionalProperties: false, + }, + { + properties: { + drop: { + type: 'boolean', + const: true, + errorMessage: 'The drop behavior must be true', + }, + }, + required: ['drop'], + additionalProperties: false, + }, + { + properties: { + setCustomResponse: { + type: 'object', + properties: { + statusCode: { + type: ['integer', 'string'], + minimum: 200, + maximum: 499, + errorMessage: 'The statusCode must be a number or string between 200 and 499', + }, + contentType: { + type: 'string', + errorMessage: 'The contentType must be a string', + }, + contentBody: { + type: 'string', + errorMessage: 'The contentBody must be a string', + }, + }, + required: ['statusCode', 'contentType', 'contentBody'], + additionalProperties: false, + errorMessage: { + additionalProperties: 'No additional properties are allowed in the setCustomResponse object', + required: "All fields ('statusCode', 'contentType', 'contentBody') are required in setCustomResponse", + }, + }, + }, + required: ['setCustomResponse'], + additionalProperties: false, + }, + ], + errorMessage: 'Each behavior item must contain exactly one behavior type', + }, + errorMessage: + 'Multiple behaviors are only allowed when the first behavior is runFunction. Otherwise, only one behavior is permitted.', +}; + const azionConfigSchema = { $id: 'azionConfig', definitions: { @@ -1227,171 +1382,7 @@ const azionConfigSchema = { enum: FIREWALL_VARIABLES, errorMessage: `The 'variable' field must be one of: ${FIREWALL_VARIABLES.join(', ')}`, }, - behaviors: { - type: 'array', - minItems: 1, - anyOf: [ - { - maxItems: 1, - }, - { - minItems: 2, - contains: { - type: 'object', - required: ['runFunction'], - }, - items: [ - { - type: 'object', - required: ['runFunction'], - properties: { - runFunction: { - type: ['string', 'number'], - }, - }, - additionalProperties: false, - }, - ], - }, - ], - items: { - type: 'object', - oneOf: [ - { - properties: { - runFunction: { - type: ['string', 'number'], - errorMessage: "The 'runFunction' behavior must be a string or number", - }, - }, - required: ['runFunction'], - additionalProperties: false, - }, - { - properties: { - setWafRuleset: { - type: 'object', - properties: { - wafMode: { - type: 'string', - enum: FIREWALL_WAF_MODES, - errorMessage: `The wafMode must be one of: ${FIREWALL_WAF_MODES.join(', ')}`, - }, - wafId: { - type: ['string', 'number'], - errorMessage: 'The wafId must be a string or number', - }, - }, - required: ['wafMode', 'wafId'], - additionalProperties: false, - errorMessage: { - additionalProperties: - 'No additional properties are allowed in the setWafRuleset object', - required: "Both 'wafMode' and 'wafId' fields are required in setWafRuleset", - }, - }, - }, - required: ['setWafRuleset'], - additionalProperties: false, - }, - { - properties: { - setRateLimit: { - type: 'object', - properties: { - type: { - type: 'string', - enum: FIREWALL_RATE_LIMIT_TYPES, - errorMessage: `The rate limit type must be one of: ${FIREWALL_RATE_LIMIT_TYPES.join(', ')}`, - }, - limitBy: { - type: 'string', - enum: FIREWALL_RATE_LIMIT_BY, - errorMessage: `The rate limit must be applied by one of: ${FIREWALL_RATE_LIMIT_BY.join(', ')}`, - }, - averageRateLimit: { - type: 'string', - errorMessage: 'The averageRateLimit must be a string', - }, - maximumBurstSize: { - type: 'string', - errorMessage: 'The maximumBurstSize must be a string', - }, - }, - required: ['type', 'limitBy', 'averageRateLimit', 'maximumBurstSize'], - additionalProperties: false, - errorMessage: { - additionalProperties: - 'No additional properties are allowed in the setRateLimit object', - required: - "All fields ('type', 'limitBy', 'averageRateLimit', 'maximumBurstSize') are required in setRateLimit", - }, - }, - }, - required: ['setRateLimit'], - additionalProperties: false, - }, - { - properties: { - deny: { - type: 'boolean', - const: true, - errorMessage: 'The deny behavior must be true', - }, - }, - required: ['deny'], - additionalProperties: false, - }, - { - properties: { - drop: { - type: 'boolean', - const: true, - errorMessage: 'The drop behavior must be true', - }, - }, - required: ['drop'], - additionalProperties: false, - }, - { - properties: { - setCustomResponse: { - type: 'object', - properties: { - statusCode: { - type: ['integer', 'string'], - minimum: 200, - maximum: 499, - errorMessage: 'The statusCode must be a number or string between 200 and 499', - }, - contentType: { - type: 'string', - errorMessage: 'The contentType must be a string', - }, - contentBody: { - type: 'string', - errorMessage: 'The contentBody must be a string', - }, - }, - required: ['statusCode', 'contentType', 'contentBody'], - additionalProperties: false, - errorMessage: { - additionalProperties: - 'No additional properties are allowed in the setCustomResponse object', - required: - "All fields ('statusCode', 'contentType', 'contentBody') are required in setCustomResponse", - }, - }, - }, - required: ['setCustomResponse'], - additionalProperties: false, - }, - ], - errorMessage: 'Each behavior item must contain exactly one behavior type', - }, - errorMessage: - 'Multiple behaviors are only allowed when the first behavior is runFunction. Otherwise, only one behavior is permitted.', - }, + behaviors: firewallRulesBehaviorsSchema, criteria: { type: 'array', minItems: 1, @@ -1419,14 +1410,16 @@ const azionConfigSchema = { errorMessage: 'The argument must be a string', }, }, + required: ['conditional', 'variable', 'operator', 'argument'], + additionalProperties: false, + errorMessage: { + additionalProperties: 'No additional properties are allowed in the criteria object', + required: + "The 'variable', 'operator', 'argument' and 'conditional' fields are required in each criteria object", + }, }, - required: ['conditional', 'variable', 'operator', 'argument'], - additionalProperties: false, errorMessage: { type: 'The criteria field must be an array with at least one criteria item', - additionalProperties: 'No additional properties are allowed in the criteria object', - required: - "The 'variable', 'operator', 'argument' and 'conditional' fields are required in each criteria object", }, }, }, diff --git a/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.ts b/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.ts index f79d793d..0e72aaae 100644 --- a/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.ts +++ b/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.ts @@ -68,6 +68,15 @@ class FirewallProcessConfigStrategy extends ProcessConfigStrategy { // eslint-disable-next-line @typescript-eslint/no-explicit-any private transformBehaviorsToManifest(behaviorArray: any[]) { + // Runtime validation: when there are 2+ behaviors, the first must be runFunction + if (behaviorArray.length >= 2 && !behaviorArray[0].runFunction) { + throw new Error( + 'When using multiple behaviors in firewall rules, the first behavior must be runFunction. ' + + 'Other behaviors (setWafRuleset, setRateLimit, deny, drop, setCustomResponse) can only be used ' + + 'after runFunction in the behaviors array.', + ); + } + const behaviors = []; for (const behaviorItem of behaviorArray) { From 5f00f869eb2cd8a255f2a3478c23a2b4b080c93c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Filho?= Date: Tue, 10 Feb 2026 14:04:14 -0300 Subject: [PATCH 2/2] fix: update firewall behaviors validation to enforce terminal behavior rules --- .../src/configProcessor/helpers/schema.ts | 21 ++----------------- .../secure/firewallProcessConfigStrategy.ts | 20 +++++++++++------- 2 files changed, 15 insertions(+), 26 deletions(-) diff --git a/packages/config/src/configProcessor/helpers/schema.ts b/packages/config/src/configProcessor/helpers/schema.ts index 80483677..d7cdf720 100644 --- a/packages/config/src/configProcessor/helpers/schema.ts +++ b/packages/config/src/configProcessor/helpers/schema.ts @@ -489,23 +489,7 @@ const schemaKV = { const firewallRulesBehaviorsSchema = { type: 'array', minItems: 1, - anyOf: [ - { - maxItems: 1, - }, - { - // When there are 2+ behaviors, at least one must be runFunction - // Note: JSON Schema strict mode doesn't support tuple validation to enforce - // that runFunction must be the FIRST item. This validation should be added - // in runtime validation after AJV schema validation passes. - minItems: 2, - maxItems: 10, - contains: { - type: 'object', - required: ['runFunction'], - }, - }, - ], + maxItems: 10, items: { type: 'object', oneOf: [ @@ -637,8 +621,7 @@ const firewallRulesBehaviorsSchema = { ], errorMessage: 'Each behavior item must contain exactly one behavior type', }, - errorMessage: - 'Multiple behaviors are only allowed when the first behavior is runFunction. Otherwise, only one behavior is permitted.', + errorMessage: 'The behaviors array must contain between 1 and 10 behavior items.', }; const azionConfigSchema = { diff --git a/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.ts b/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.ts index 0e72aaae..da40bfa4 100644 --- a/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.ts +++ b/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.ts @@ -68,13 +68,19 @@ class FirewallProcessConfigStrategy extends ProcessConfigStrategy { // eslint-disable-next-line @typescript-eslint/no-explicit-any private transformBehaviorsToManifest(behaviorArray: any[]) { - // Runtime validation: when there are 2+ behaviors, the first must be runFunction - if (behaviorArray.length >= 2 && !behaviorArray[0].runFunction) { - throw new Error( - 'When using multiple behaviors in firewall rules, the first behavior must be runFunction. ' + - 'Other behaviors (setWafRuleset, setRateLimit, deny, drop, setCustomResponse) can only be used ' + - 'after runFunction in the behaviors array.', - ); + // Runtime validation: deny, drop, and setCustomResponse are terminal behaviors + // and cannot be combined with other behaviors + if (behaviorArray.length > 1) { + const firstBehavior = behaviorArray[0]; + const hasTerminalBehavior = firstBehavior.deny || firstBehavior.drop || firstBehavior.setCustomResponse; + + if (hasTerminalBehavior) { + const behaviorType = firstBehavior.deny ? 'deny' : firstBehavior.drop ? 'drop' : 'setCustomResponse'; + throw new Error( + `The behavior '${behaviorType}' is a terminal behavior and must be used alone. ` + + `It cannot be combined with other behaviors in the same rule.`, + ); + } } const behaviors = [];