diff --git a/CHANGELOG.md b/CHANGELOG.md index ed1ab791..6c756f4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## 3.1.2-stage.2 (2026-03-03) + +* fix: update firewall schema to accept variables (#396) ([607d246](https://github.com/aziontech/lib/commit/607d246)), closes [#396](https://github.com/aziontech/lib/issues/396) + +## 3.1.2-stage.1 (2026-03-02) + +* fix: firewall functions instances support (#395) ([8d24afc](https://github.com/aziontech/lib/commit/8d24afc)), closes [#395](https://github.com/aziontech/lib/issues/395) + ## 3.1.1 (2026-02-13) * Merge pull request #389 from aziontech/stage ([c9bff59](https://github.com/aziontech/lib/commit/c9bff59)), closes [#389](https://github.com/aziontech/lib/issues/389) diff --git a/package.json b/package.json index 82a6ca84..17cfe724 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "azion", - "version": "3.1.1", + "version": "3.1.2-stage.2", "description": "Azion Packages for Edge Computing.", "scripts": { "prepare": "husky", diff --git a/packages/config/src/configProcessor/helpers/azion.config.example.ts b/packages/config/src/configProcessor/helpers/azion.config.example.ts index 12887f5c..0a95b7ca 100644 --- a/packages/config/src/configProcessor/helpers/azion.config.example.ts +++ b/packages/config/src/configProcessor/helpers/azion.config.example.ts @@ -477,40 +477,56 @@ const config: AzionConfig = { ], firewall: [ { - name: 'my_firewall', + name: 'my-firewall', active: true, functions: true, networkProtection: true, waf: true, + functionsInstances: [ + { + name: 'my_func_instance', + ref: 'my_func_name', + }, + ], rules: [ { - name: 'rateLimit_Then_Drop', - active: true, - match: '^/api/sensitive/', - variable: 'request_uri', + name: 'rule-network', + description: 'Rule Network List', + criteria: [ + { + variable: 'network', + conditional: 'if', + operator: 'is_in_list', + argument: 'my-ip-allowlist', + }, + { + variable: 'network', + conditional: 'and', + operator: 'is_in_list', + argument: 'trusted-asn-list', + }, + ], behaviors: [ { - setRateLimit: { - type: 'second', - limitBy: 'clientIp', - averageRateLimit: '10', - maximumBurstSize: '20', - }, + deny: true, }, ], }, { - name: 'customResponse_Only', + name: 'rateLimit_Then_Drop', active: true, criteria: [ { variable: 'request_uri', - operator: 'matches', conditional: 'if', - argument: '^/custom-error/', + operator: 'matches', + argument: '^/api/sensitive/', }, ], behaviors: [ + { + runFunction: 'my_func_instance', + }, { setCustomResponse: { statusCode: 403, diff --git a/packages/config/src/configProcessor/helpers/schema.ts b/packages/config/src/configProcessor/helpers/schema.ts index b900b633..f27d3e57 100644 --- a/packages/config/src/configProcessor/helpers/schema.ts +++ b/packages/config/src/configProcessor/helpers/schema.ts @@ -624,6 +624,32 @@ const firewallRulesBehaviorsSchema = { errorMessage: 'The behaviors array must contain between 1 and 10 behavior items.', }; +const firewallFunctionsInstances = { + type: 'object', + properties: { + name: { + type: 'string', + minLength: 1, + maxLength: 100, + errorMessage: 'The name must be a string between 1 and 100 characters long', + }, + args: { + type: 'object', + default: {}, + errorMessage: "The 'args' field must be an object", + }, + ref: { + type: ['string', 'number'], + errorMessage: "The 'ref' field must be a string or number referencing an existing Function name or ID", + }, + }, + required: ['name', 'ref'], + additionalProperties: false, + errorMessage: { + additionalProperties: 'No additional properties are allowed in the firewallFunctionsInstance object', + }, +}; + const azionConfigSchema = { $id: 'azionConfig', definitions: { @@ -1361,9 +1387,19 @@ const azionConfigSchema = { errorMessage: "The rule's 'match' field must be a string containing a valid regex pattern", }, variable: { - type: 'string', - enum: FIREWALL_VARIABLES, - errorMessage: `The 'variable' field must be one of: ${FIREWALL_VARIABLES.join(', ')}`, + anyOf: [ + { + type: 'string', + pattern: '^\\$\\{(' + [...new Set(FIREWALL_VARIABLES)].join('|') + ')\\}$', + errorMessage: "The 'variable' field must be a valid variable wrapped in ${}", + }, + { + type: 'string', + enum: [...FIREWALL_VARIABLES], + errorMessage: "The 'variable' field must be a valid firewall variable", + }, + ], + errorMessage: "The 'variable' field must be a valid variable (with or without ${})", }, behaviors: firewallRulesBehaviorsSchema, criteria: { @@ -1379,9 +1415,19 @@ const azionConfigSchema = { errorMessage: `The 'conditional' field must be one of: ${FIREWALL_RULE_CONDITIONAL.join(', ')}`, }, variable: { - type: 'string', - enum: FIREWALL_VARIABLES, - errorMessage: `The 'variable' field must be one of: ${FIREWALL_VARIABLES.join(', ')}`, + anyOf: [ + { + type: 'string', + pattern: '^\\$\\{(' + [...new Set(FIREWALL_VARIABLES)].join('|') + ')\\}$', + errorMessage: "The 'variable' field must be a valid variable wrapped in ${}", + }, + { + type: 'string', + enum: [...FIREWALL_VARIABLES], + errorMessage: "The 'variable' field must be a valid firewall variable", + }, + ], + errorMessage: "The 'variable' field must be a valid variable (with or without ${})", }, operator: { type: 'string', @@ -1389,8 +1435,8 @@ const azionConfigSchema = { errorMessage: `The 'operator' field must be one of: ${FIREWALL_RULE_OPERATORS.join(', ')}`, }, argument: { - type: 'string', - errorMessage: 'The argument must be a string', + type: ['string', 'number'], + errorMessage: 'The argument must be a string or number', }, }, required: ['conditional', 'variable', 'operator', 'argument'], @@ -1428,6 +1474,10 @@ const azionConfigSchema = { }, }, }, + functionsInstances: { + type: 'array', + items: firewallFunctionsInstances, + }, }, required: ['name'], additionalProperties: false, @@ -2026,10 +2076,9 @@ const azionConfigSchema = { }, }, additionalProperties: false, - required: ['build', 'applications', 'workloads'], errorMessage: { additionalProperties: - 'Config can only contain the following properties: build, functions, applications, workloads, purge, edgefirewall, networkList, waf, connectors, customPages', + 'Config can only contain the following properties: build, functions, applications, workloads, purge, firewall, networkList, waf, connectors, customPages', type: 'Configuration must be an object', }, }, diff --git a/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.test.ts b/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.test.ts index dacdaaee..e4a6fb4d 100644 --- a/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.test.ts +++ b/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.test.ts @@ -148,8 +148,8 @@ describe('FirewallProcessConfigStrategy', () => { // criteria wrapped into array-of-arrays expect(rule.criteria).toEqual([ [ - { variable: 'host', operator: 'is_equal', conditional: 'if', argument: 'example.com' }, - { variable: 'request_method', operator: 'exists', conditional: 'and' }, + { variable: '${host}', operator: 'is_equal', conditional: 'if', argument: 'example.com' }, + { variable: '${request_method}', operator: 'exists', conditional: 'and' }, ], ]); }); @@ -301,8 +301,8 @@ describe('FirewallProcessConfigStrategy', () => { expect(rule.criteria).toEqual([ [ - { variable: 'host', operator: 'is_equal', conditional: 'if', argument: 'example.com' }, - { variable: 'request_method', operator: 'matches', conditional: 'and', argument: '^(GET|POST)$' }, + { variable: '${host}', operator: 'is_equal', conditional: 'if', argument: 'example.com' }, + { variable: '${request_method}', operator: 'matches', conditional: 'and', argument: '^(GET|POST)$' }, ], ]); expect(rule.behaviors).toEqual([{ type: 'deny' }]); diff --git a/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.ts b/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.ts index da40bfa4..8d61a493 100644 --- a/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.ts +++ b/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.ts @@ -1,4 +1,10 @@ -import { AzionConfig, AzionFirewall, AzionFirewallCriteriaWithValue, AzionFirewallRule } from '../../../../types'; +import { + AzionConfig, + AzionFirewall, + AzionFirewallCriteriaWithValue, + AzionFirewallRule, + AzionFunction, +} from '../../../../types'; import ProcessConfigStrategy from '../../processConfigStrategy'; /** @@ -7,6 +13,22 @@ import ProcessConfigStrategy from '../../processConfigStrategy'; * @description This class is implementation of the Firewall ProcessConfig Strategy. */ class FirewallProcessConfigStrategy extends ProcessConfigStrategy { + /** + * Validate that referenced Function exists + */ + private validateFunctionReference( + functions: AzionFunction[] | undefined, + functionNameOrId: string | number, + instanceName: string, + ) { + // Only validate if it's a string (name), skip validation for numbers (IDs) + if (typeof functionNameOrId === 'string') { + if (!Array.isArray(functions) || !functions.find((func) => func.name === functionNameOrId)) { + throw new Error(`Function instance "${instanceName}" references non-existent Function "${functionNameOrId}".`); + } + } + } + transformToManifest(config: AzionConfig) { if (!config.firewall || !Array.isArray(config.firewall)) { return []; @@ -44,7 +66,7 @@ class FirewallProcessConfigStrategy extends ProcessConfigStrategy { const { argument, ...rest } = criterion as AzionFirewallCriteriaWithValue; return { ...rest, - variable: criterion.variable, + variable: criterion.variable.startsWith('${') ? criterion.variable : `\${${criterion.variable}}`, ...(isWithArgument && { argument }), }; }), @@ -62,6 +84,18 @@ class FirewallProcessConfigStrategy extends ProcessConfigStrategy { })); } + if (fw.functionsInstances && fw.functionsInstances.length > 0) { + payload.functions_instances = fw.functionsInstances.map((instance) => { + this.validateFunctionReference(config.functions, instance.ref, instance.name); + return { + name: instance.name, + args: instance.args || {}, + active: instance.active ?? true, + function: instance.ref, + }; + }); + } + return payload; }); } @@ -184,6 +218,16 @@ class FirewallProcessConfigStrategy extends ProcessConfigStrategy { }); } + if (fw.functions_instance && fw.functions_instance.length > 0) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + firewallConfig.functionsInstances = fw.functions_instance.map((instance: any) => ({ + name: instance.name, + args: instance.args || {}, + active: instance.active ?? true, + ref: instance.function, + })); + } + return firewallConfig; }); return transformedPayload.firewall; diff --git a/packages/config/src/types.ts b/packages/config/src/types.ts index 1646efb4..1e5268f1 100644 --- a/packages/config/src/types.ts +++ b/packages/config/src/types.ts @@ -53,7 +53,8 @@ export type CommonVariable = | 'request_method' | 'request_uri' | 'scheme' - | 'uri'; + | 'uri' + | 'network'; export type RequestVariable = | CommonVariable | 'server_addr' @@ -82,7 +83,9 @@ export type RuleOperatorWithValue = | 'starts_with' | 'does_not_start_with' | 'matches' - | 'does_not_match'; + | 'does_not_match' + | 'is_in_list' + | 'is_not_in_list'; export type RuleOperatorWithoutValue = 'exists' | 'does_not_exist'; export type RuleConditional = 'if' | 'and' | 'or'; @@ -597,7 +600,7 @@ export type AzionFirewallCriteriaWithValue = AzionFirewallCriteriaBase & { /** Operator for comparison that requires input value */ operator: RuleOperatorWithValue; /** Argument for comparison */ - argument: string; + argument: string | number; }; export type AzionFirewallCriteriaWithoutValue = AzionFirewallCriteriaBase & { @@ -627,6 +630,17 @@ export type AzionFirewallRule = { behaviors: AzionFirewallBehavior; }; +export type AzionFirewallFunctionsInstance = { + /** Function instance name */ + name: string; + /** Function instance arguments */ + args?: Record; + /** Active */ + active?: boolean; + /** Reference to Function name or ID */ + ref: string | number; +}; + /** * Firewall configuration for Azion. */ @@ -645,6 +659,8 @@ export type AzionFirewall = { rules?: AzionFirewallRule[]; /** Debug mode */ debugRules?: boolean; + /** Functions Instances */ + functionsInstances?: AzionFirewallFunctionsInstance[]; }; // WAF V4 Types