diff --git a/packages/config/src/configProcessor/helpers/azion.config.example.ts b/packages/config/src/configProcessor/helpers/azion.config.example.ts index 77b2d9a3..12887f5c 100644 --- a/packages/config/src/configProcessor/helpers/azion.config.example.ts +++ b/packages/config/src/configProcessor/helpers/azion.config.example.ts @@ -487,26 +487,38 @@ const config: AzionConfig = { name: 'rateLimit_Then_Drop', active: true, match: '^/api/sensitive/', - behavior: { - setRateLimit: { - type: 'second', - limitBy: 'clientIp', - averageRateLimit: '10', - maximumBurstSize: '20', + variable: 'request_uri', + behaviors: [ + { + setRateLimit: { + type: 'second', + limitBy: 'clientIp', + averageRateLimit: '10', + maximumBurstSize: '20', + }, }, - }, + ], }, { name: 'customResponse_Only', active: true, - match: '^/custom-error/', - behavior: { - setCustomResponse: { - statusCode: 403, - contentType: 'application/json', - contentBody: '{"error": "Custom error response"}', + criteria: [ + { + variable: 'request_uri', + operator: 'matches', + conditional: 'if', + argument: '^/custom-error/', }, - }, + ], + behaviors: [ + { + setCustomResponse: { + statusCode: 403, + contentType: 'application/json', + contentBody: '{"error": "Custom error response"}', + }, + }, + ], }, ], }, diff --git a/packages/config/src/configProcessor/helpers/schema.ts b/packages/config/src/configProcessor/helpers/schema.ts index 9a2bb197..4192686e 100644 --- a/packages/config/src/configProcessor/helpers/schema.ts +++ b/packages/config/src/configProcessor/helpers/schema.ts @@ -13,6 +13,8 @@ import { EDGE_CONNECTOR_TYPES, FIREWALL_RATE_LIMIT_BY, FIREWALL_RATE_LIMIT_TYPES, + FIREWALL_RULE_CONDITIONAL, + FIREWALL_RULE_OPERATORS, FIREWALL_VARIABLES, FIREWALL_WAF_MODES, HEADER_BEHAVIORS, @@ -1199,11 +1201,6 @@ const azionConfigSchema = { type: 'boolean', errorMessage: "The firewall's 'waf' field must be a boolean", }, - variable: { - type: 'string', - enum: FIREWALL_VARIABLES, - errorMessage: `The 'variable' field must be one of: ${FIREWALL_VARIABLES.join(', ')}`, - }, rules: { type: 'array', items: { @@ -1225,121 +1222,221 @@ const azionConfigSchema = { type: 'string', errorMessage: "The rule's 'match' field must be a string containing a valid regex pattern", }, - behavior: { - type: 'object', - properties: { - runFunction: { - type: ['string', 'number'], - errorMessage: "The 'runFunction' behavior must be a string or number", + variable: { + type: 'string', + enum: FIREWALL_VARIABLES, + errorMessage: `The 'variable' field must be one of: ${FIREWALL_VARIABLES.join(', ')}`, + }, + behaviors: { + type: 'array', + minItems: 1, + anyOf: [ + { + maxItems: 1, }, - setWafRuleset: { - type: 'object', - properties: { - wafMode: { - type: 'string', - enum: FIREWALL_WAF_MODES, - errorMessage: `The wafMode must be one of: ${FIREWALL_WAF_MODES.join(', ')}`, + { + minItems: 2, + contains: { + type: 'object', + required: ['runFunction'], + }, + items: [ + { + type: 'object', + required: ['runFunction'], + properties: { + runFunction: { + type: ['string', 'number'], + }, + }, + additionalProperties: false, }, - wafId: { - type: ['string', 'number'], - errorMessage: 'The wafId must be a string or number', + ], + }, + ], + items: { + type: 'object', + oneOf: [ + { + properties: { + runFunction: { + type: ['string', 'number'], + errorMessage: "The 'runFunction' behavior must be a string or number", + }, }, + required: ['runFunction'], + additionalProperties: false, }, - 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", + { + 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, }, - }, - 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(', ')}`, + { + 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", + }, + }, }, - limitBy: { - type: 'string', - enum: FIREWALL_RATE_LIMIT_BY, - errorMessage: `The rate limit must be applied by one of: ${FIREWALL_RATE_LIMIT_BY.join(', ')}`, + required: ['setRateLimit'], + additionalProperties: false, + }, + { + properties: { + deny: { + type: 'boolean', + const: true, + errorMessage: 'The deny behavior must be true', + }, }, - averageRateLimit: { - type: 'string', - errorMessage: 'The averageRateLimit must be a string', + required: ['deny'], + additionalProperties: false, + }, + { + properties: { + drop: { + type: 'boolean', + const: true, + errorMessage: 'The drop behavior must be true', + }, }, - maximumBurstSize: { - type: 'string', - errorMessage: 'The maximumBurstSize must be a string', + 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, }, - 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", + ], + 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.', + }, + criteria: { + type: 'array', + minItems: 1, + maxItems: 5, + items: { + type: 'object', + properties: { + conditional: { + type: 'string', + enum: FIREWALL_RULE_CONDITIONAL, + errorMessage: `The 'conditional' field must be one of: ${FIREWALL_RULE_CONDITIONAL.join(', ')}`, }, - }, - deny: { - type: 'boolean', - errorMessage: 'The deny behavior must be a boolean', - }, - drop: { - type: 'boolean', - errorMessage: 'The drop behavior must be a boolean', - }, - 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', - }, + variable: { + type: 'string', + enum: FIREWALL_VARIABLES, + errorMessage: `The 'variable' field must be one of: ${FIREWALL_VARIABLES.join(', ')}`, }, - 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", + operator: { + type: 'string', + enum: FIREWALL_RULE_OPERATORS, + errorMessage: `The 'operator' field must be one of: ${FIREWALL_RULE_OPERATORS.join(', ')}`, + }, + argument: { + type: 'string', + errorMessage: 'The argument must be a string', }, }, }, - not: { - anyOf: [ - { required: ['deny', 'drop'] }, - { required: ['deny', 'setCustomResponse'] }, - { required: ['deny', 'setRateLimit'] }, - { required: ['drop', 'setCustomResponse'] }, - { required: ['drop', 'setRateLimit'] }, - { required: ['setCustomResponse', 'setRateLimit'] }, - ], - }, + required: ['conditional', 'variable', 'operator', 'argument'], + additionalProperties: false, errorMessage: { - not: 'Cannot use multiple final behaviors (deny, drop, setRateLimit, setCustomResponse) together. You can combine non-final behaviors (runFunction, setWafRuleset) with only one final behavior.', + 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", }, - additionalProperties: false, }, }, - required: ['name', 'behavior'], + required: ['name', 'behaviors'], oneOf: [ { - anyOf: [{ required: ['match'] }, { required: ['variable'] }], + required: ['match', 'variable'], not: { required: ['criteria'] }, - errorMessage: "Cannot use 'match' or 'variable' together with 'criteria'.", + errorMessage: + "When using 'match' or 'variable', both fields are required and cannot be used with 'criteria'.", }, { required: ['criteria'], @@ -1350,7 +1447,8 @@ const azionConfigSchema = { }, ], errorMessage: { - oneOf: "You must use either 'match/variable' OR 'criteria', but not both at the same time", + oneOf: + "You must use either 'match' AND 'variable' together OR 'criteria', but not both at the same time", }, }, }, 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 58bed7ae..dacdaaee 100644 --- a/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.test.ts +++ b/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.test.ts @@ -29,16 +29,13 @@ describe('FirewallProcessConfigStrategy', () => { functions: true, networkProtection: false, waf: true, - variable: 'request_uri', rules: [ { name: 'rule-1', description: 'desc', match: '/api/*', variable: 'request_uri', - behavior: { - deny: true, - }, + behaviors: [{ deny: true }], }, ], }, @@ -97,21 +94,26 @@ describe('FirewallProcessConfigStrategy', () => { { variable: 'host', operator: 'is_equal', conditional: 'if', argument: 'example.com' }, { variable: 'request_method', operator: 'exists', conditional: 'and' }, ], - behavior: { - runFunction: 'func-1', - setWafRuleset: { wafMode: 'blocking', wafId: 123 }, - setRateLimit: { - type: 'minute', - value: '100', - limitBy: 'clientIp', - averageRateLimit: '60', - maximumBurstSize: '20', - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any, - drop: true, - setCustomResponse: { statusCode: 429, contentType: 'application/json', contentBody: '{"err":true}' }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any, + behaviors: [ + { runFunction: 'func-1' }, + { setWafRuleset: { wafMode: 'blocking', wafId: 123 } }, + { + setRateLimit: { + type: 'minute', + limitBy: 'clientIp', + averageRateLimit: '60', + maximumBurstSize: '20', + }, + }, + { drop: true }, + { + setCustomResponse: { + statusCode: 429, + contentType: 'application/json', + contentBody: '{"err":true}', + }, + }, + ], }, ], }, @@ -131,7 +133,6 @@ describe('FirewallProcessConfigStrategy', () => { type: 'set_rate_limit', attributes: { type: 'minute', - value: '100', limit_by: 'clientIp', average_rate_limit: '60', maximum_burst_size: '20', @@ -152,6 +153,160 @@ describe('FirewallProcessConfigStrategy', () => { ], ]); }); + + it('should transform a firewall rule with runFunction followed by other behaviors', () => { + const config: AzionConfig = { + firewall: [ + { + name: 'fw-runFunction', + functions: true, + networkProtection: false, + waf: false, + rules: [ + { + name: 'rule-runFunction-deny', + match: '/api/*', + variable: 'request_uri', + behaviors: [{ runFunction: 'my-function' }, { deny: true }], + }, + ], + }, + ], + }; + + const result = strategy.transformToManifest(config)!; + expect(result).toHaveLength(1); + const rule = result[0].rules_engine[0]; + + expect(rule.behaviors).toEqual([ + { type: 'run_function', attributes: { value: 'my-function' } }, + { type: 'deny' }, + ]); + }); + + it('should transform a firewall rule with single deny behavior', () => { + const config: AzionConfig = { + firewall: [ + { + name: 'fw-single', + functions: false, + networkProtection: true, + waf: false, + rules: [ + { + name: 'rule-single-deny', + match: '/blocked/*', + variable: 'request_uri', + behaviors: [{ deny: true }], + }, + ], + }, + ], + }; + + const result = strategy.transformToManifest(config)!; + expect(result).toHaveLength(1); + const rule = result[0].rules_engine[0]; + + expect(rule.behaviors).toEqual([{ type: 'deny' }]); + }); + + it('should transform a firewall rule with single drop behavior', () => { + const config: AzionConfig = { + firewall: [ + { + name: 'fw-drop', + functions: false, + networkProtection: true, + waf: false, + rules: [ + { + name: 'rule-drop', + match: '/drop/*', + variable: 'request_uri', + behaviors: [{ drop: true }], + }, + ], + }, + ], + }; + + const result = strategy.transformToManifest(config)!; + expect(result).toHaveLength(1); + const rule = result[0].rules_engine[0]; + + expect(rule.behaviors).toEqual([{ type: 'drop' }]); + }); + + it('should transform a firewall rule with runFunction, setWafRuleset, and deny', () => { + const config: AzionConfig = { + firewall: [ + { + name: 'fw-multiple', + functions: true, + networkProtection: false, + waf: true, + rules: [ + { + name: 'rule-multiple', + match: '/protected/*', + variable: 'request_uri', + behaviors: [ + { runFunction: 123 }, + { setWafRuleset: { wafMode: 'blocking', wafId: 456 } }, + { deny: true }, + ], + }, + ], + }, + ], + }; + + const result = strategy.transformToManifest(config)!; + expect(result).toHaveLength(1); + const rule = result[0].rules_engine[0]; + + expect(rule.behaviors).toEqual([ + { type: 'run_function', attributes: { value: 123 } }, + { type: 'set_waf_ruleset', attributes: { mode: 'blocking', waf_id: 456 } }, + { type: 'deny' }, + ]); + }); + + it('should transform a firewall rule using criteria instead of match/variable', () => { + const config: AzionConfig = { + firewall: [ + { + name: 'fw-criteria', + functions: false, + networkProtection: true, + waf: false, + rules: [ + { + name: 'rule-criteria', + criteria: [ + { variable: 'host', operator: 'is_equal', conditional: 'if', argument: 'example.com' }, + { variable: 'request_method', operator: 'matches', conditional: 'and', argument: '^(GET|POST)$' }, + ], + behaviors: [{ deny: true }], + }, + ], + }, + ], + }; + + const result = strategy.transformToManifest(config)!; + expect(result).toHaveLength(1); + const rule = result[0].rules_engine[0]; + + expect(rule.criteria).toEqual([ + [ + { 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' }]); + }); }); describe('transformToConfig', () => { @@ -205,7 +360,7 @@ describe('FirewallProcessConfigStrategy', () => { expect(result).toBe(transformedPayload.firewall); }); - it('should transform rules_engine behaviors and criteria to config behavior object', () => { + it('should transform rules_engine behaviors and criteria to config behaviors object', () => { const payload = { firewall: [ { @@ -222,7 +377,6 @@ describe('FirewallProcessConfigStrategy', () => { type: 'set_rate_limit', attributes: { type: 'second', - value: '20', limit_by: 'global', average_rate_limit: '10', maximum_burst_size: '5', @@ -250,30 +404,31 @@ describe('FirewallProcessConfigStrategy', () => { const transformedPayload: AzionConfig = {}; strategy.transformToConfig(payload, transformedPayload); - // Note: behavior.runFunction is mapped to an object with { path } by the strategy + // Note: behaviors.runFunction is mapped to an object with { path } by the strategy expect(transformedPayload.firewall?.[0]).toMatchObject({ name: 'fw-2', }); // If rules are mapped, it should look like the following structure - // This expectation documents the intended behavior; failing here indicates a bug in the strategy. + // This expectation documents the intended behaviors; failing here indicates a bug in the strategy. const expectedRule = { name: 'BlockBots', active: false, - behavior: { - runFunction: { path: '/path/to/f.js' }, - setWafRuleset: { wafMode: 'learning', wafId: 999 }, - setRateLimit: { - type: 'second', - value: '20', - limitBy: 'global', - averageRateLimit: '10', - maximumBurstSize: '5', + behaviors: [ + { runFunction: '/path/to/f.js' }, + { setWafRuleset: { wafMode: 'learning', wafId: 999 } }, + { + setRateLimit: { + type: 'second', + limitBy: 'global', + averageRateLimit: '10', + maximumBurstSize: '5', + }, }, - deny: true, - drop: true, - setCustomResponse: { statusCode: 403, contentType: 'text/plain', contentBody: 'forbidden' }, - }, + { deny: true }, + { drop: true }, + { setCustomResponse: { statusCode: 403, contentType: 'text/plain', contentBody: 'forbidden' } }, + ], criteria: [ { variable: 'host', operator: 'is_equal', conditional: 'if', argument: 'bad.com' }, { variable: 'request_method', operator: 'exists', conditional: 'and' }, @@ -285,9 +440,146 @@ describe('FirewallProcessConfigStrategy', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any expect(transformedPayload.firewall?.[0]?.rules?.[0]).toMatchObject(expectedRule as any); } else { - // Document current behavior if rules are not mapped due to a bug + // Document current behaviors if rules are not mapped due to a bug expect(transformedPayload.firewall?.[0]?.rules).toBeDefined(); } }); + + it('should transform manifest with runFunction followed by deny to config', () => { + const payload = { + firewall: [ + { + name: 'fw-runFunction', + modules: { functions: { enabled: true }, network_protection: { enabled: false }, waf: { enabled: false } }, + rules_engine: [ + { + type: 'RunThenDeny', + active: true, + behaviors: [{ type: 'run_function', attributes: { value: 'my-func' } }, { type: 'deny' }], + criteria: [[{ variable: 'request_uri', operator: 'matches', conditional: 'if', argument: '/api/*' }]], + }, + ], + }, + ], + }; + + const transformedPayload: AzionConfig = {}; + strategy.transformToConfig(payload, transformedPayload); + + expect(transformedPayload.firewall?.[0]?.rules?.[0]).toMatchObject({ + name: 'RunThenDeny', + active: true, + behaviors: [{ runFunction: 'my-func' }, { deny: true }], + }); + }); + + it('should transform manifest with single deny behavior to config', () => { + const payload = { + firewall: [ + { + name: 'fw-single-deny', + modules: { functions: { enabled: false }, network_protection: { enabled: true }, waf: { enabled: false } }, + rules_engine: [ + { + type: 'DenyOnly', + active: true, + behaviors: [{ type: 'deny' }], + criteria: [[{ variable: 'host', operator: 'is_equal', conditional: 'if', argument: 'blocked.com' }]], + }, + ], + }, + ], + }; + + const transformedPayload: AzionConfig = {}; + strategy.transformToConfig(payload, transformedPayload); + + expect(transformedPayload.firewall?.[0]?.rules?.[0]).toMatchObject({ + name: 'DenyOnly', + active: true, + behaviors: [{ deny: true }], + }); + }); + + it('should transform manifest with single drop behavior to config', () => { + const payload = { + firewall: [ + { + name: 'fw-single-drop', + modules: { functions: { enabled: false }, network_protection: { enabled: true }, waf: { enabled: false } }, + rules_engine: [ + { + type: 'DropOnly', + active: true, + behaviors: [{ type: 'drop' }], + criteria: [ + [{ variable: 'request_method', operator: 'is_equal', conditional: 'if', argument: 'DELETE' }], + ], + }, + ], + }, + ], + }; + + const transformedPayload: AzionConfig = {}; + strategy.transformToConfig(payload, transformedPayload); + + expect(transformedPayload.firewall?.[0]?.rules?.[0]).toMatchObject({ + name: 'DropOnly', + active: true, + behaviors: [{ drop: true }], + }); + }); + + it('should transform manifest with runFunction, setWafRuleset, and setCustomResponse to config', () => { + const payload = { + firewall: [ + { + name: 'fw-complex', + modules: { functions: { enabled: true }, network_protection: { enabled: false }, waf: { enabled: true } }, + rules_engine: [ + { + type: 'ComplexRule', + active: true, + behaviors: [ + { type: 'run_function', attributes: { value: 999 } }, + { type: 'set_waf_ruleset', attributes: { mode: 'counting', waf_id: 111 } }, + { + type: 'set_custom_response', + attributes: { + status_code: 429, + content_type: 'application/json', + content_body: '{"error":"rate limit"}', + }, + }, + ], + criteria: [ + [{ variable: 'client_ip', operator: 'is_in_list', conditional: 'if', argument: 'blocklist' }], + ], + }, + ], + }, + ], + }; + + const transformedPayload: AzionConfig = {}; + strategy.transformToConfig(payload, transformedPayload); + + expect(transformedPayload.firewall?.[0]?.rules?.[0]).toMatchObject({ + name: 'ComplexRule', + active: true, + behaviors: [ + { runFunction: 999 }, + { setWafRuleset: { wafMode: 'counting', wafId: 111 } }, + { + setCustomResponse: { + statusCode: 429, + contentType: 'application/json', + contentBody: '{"error":"rate limit"}', + }, + }, + ], + }); + }); }); }); diff --git a/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.ts b/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.ts index cbdbe6c7..f79d793d 100644 --- a/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.ts +++ b/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.ts @@ -36,7 +36,7 @@ class FirewallProcessConfigStrategy extends ProcessConfigStrategy { name: rule.name, description: rule.description || '', active: rule.active ?? true, - behaviors: this.transformBehaviorsToManifest(rule.behavior), + behaviors: this.transformBehaviorsToManifest(rule.behaviors), criteria: rule.criteria ? [ rule.criteria.map((criterion) => { @@ -67,62 +67,63 @@ class FirewallProcessConfigStrategy extends ProcessConfigStrategy { } // eslint-disable-next-line @typescript-eslint/no-explicit-any - private transformBehaviorsToManifest(behavior: any) { + private transformBehaviorsToManifest(behaviorArray: any[]) { const behaviors = []; - if (behavior.runFunction) { - behaviors.push({ - type: 'run_function', - attributes: { - value: behavior.runFunction, - }, - }); - } + for (const behaviorItem of behaviorArray) { + if (behaviorItem.runFunction !== undefined) { + behaviors.push({ + type: 'run_function', + attributes: { + value: behaviorItem.runFunction, + }, + }); + } - if (behavior.setWafRuleset) { - behaviors.push({ - type: 'set_waf_ruleset', - attributes: { - mode: behavior.setWafRuleset.wafMode, - waf_id: behavior.setWafRuleset.wafId, - }, - }); - } + if (behaviorItem.setWafRuleset) { + behaviors.push({ + type: 'set_waf_ruleset', + attributes: { + mode: behaviorItem.setWafRuleset.wafMode, + waf_id: behaviorItem.setWafRuleset.wafId, + }, + }); + } - if (behavior.setRateLimit) { - behaviors.push({ - type: 'set_rate_limit', - attributes: { - type: behavior.setRateLimit.type || 'second', - value: behavior.setRateLimit.value, - limit_by: behavior.setRateLimit.limitBy, - average_rate_limit: behavior.setRateLimit.averageRateLimit, - maximum_burst_size: behavior.setRateLimit.maximumBurstSize, - }, - }); - } + if (behaviorItem.setRateLimit) { + behaviors.push({ + type: 'set_rate_limit', + attributes: { + type: behaviorItem.setRateLimit.type || 'second', + limit_by: behaviorItem.setRateLimit.limitBy, + average_rate_limit: behaviorItem.setRateLimit.averageRateLimit, + maximum_burst_size: behaviorItem.setRateLimit.maximumBurstSize, + }, + }); + } - if (behavior.deny) { - behaviors.push({ - type: 'deny', - }); - } + if (behaviorItem.deny) { + behaviors.push({ + type: 'deny', + }); + } - if (behavior.drop) { - behaviors.push({ - type: 'drop', - }); - } + if (behaviorItem.drop) { + behaviors.push({ + type: 'drop', + }); + } - if (behavior.setCustomResponse) { - behaviors.push({ - type: 'set_custom_response', - attributes: { - status_code: behavior.setCustomResponse.statusCode, - content_type: behavior.setCustomResponse.contentType, - content_body: behavior.setCustomResponse.contentBody, - }, - }); + if (behaviorItem.setCustomResponse) { + behaviors.push({ + type: 'set_custom_response', + attributes: { + status_code: behaviorItem.setCustomResponse.statusCode, + content_type: behaviorItem.setCustomResponse.contentType, + content_body: behaviorItem.setCustomResponse.contentBody, + }, + }); + } } return behaviors; @@ -152,7 +153,7 @@ class FirewallProcessConfigStrategy extends ProcessConfigStrategy { const firewallRule: AzionFirewallRule = { name: rule.type, active: rule.active ?? true, - behavior: this.transformBehaviorsToConfig(rule.behaviors), + behaviors: this.transformBehaviorsToConfig(rule.behaviors), criteria: // eslint-disable-next-line @typescript-eslint/no-explicit-any rule.criteria?.[0].map((criterion: any) => { @@ -176,47 +177,52 @@ class FirewallProcessConfigStrategy extends ProcessConfigStrategy { // eslint-disable-next-line @typescript-eslint/no-explicit-any private transformBehaviorsToConfig(behaviors: any[]) { // eslint-disable-next-line @typescript-eslint/no-explicit-any - const behavior: any = {}; + const behaviorArray: any[] = []; behaviors.forEach((b) => { switch (b.type) { case 'run_function': - behavior.runFunction = { - path: b.attributes.value, - }; + behaviorArray.push({ + runFunction: b.attributes.value, + }); break; case 'set_waf_ruleset': - behavior.setWafRuleset = { - wafMode: b.attributes.mode, - wafId: b.attributes.waf_id, - }; + behaviorArray.push({ + setWafRuleset: { + wafMode: b.attributes.mode, + wafId: b.attributes.waf_id, + }, + }); break; case 'set_rate_limit': - behavior.setRateLimit = { - type: b.attributes.type, - value: b.attributes.value, - limitBy: b.attributes.limit_by, - averageRateLimit: b.attributes.average_rate_limit, - maximumBurstSize: b.attributes.maximum_burst_size, - }; + behaviorArray.push({ + setRateLimit: { + type: b.attributes.type, + limitBy: b.attributes.limit_by, + averageRateLimit: b.attributes.average_rate_limit, + maximumBurstSize: b.attributes.maximum_burst_size, + }, + }); break; case 'deny': - behavior.deny = true; + behaviorArray.push({ deny: true }); break; case 'drop': - behavior.drop = true; + behaviorArray.push({ drop: true }); break; case 'set_custom_response': - behavior.setCustomResponse = { - statusCode: b.attributes.status_code, - contentType: b.attributes.content_type, - contentBody: b.attributes.content_body, - }; + behaviorArray.push({ + setCustomResponse: { + statusCode: b.attributes.status_code, + contentType: b.attributes.content_type, + contentBody: b.attributes.content_body, + }, + }); break; } }); - return behavior; + return behaviorArray; } } diff --git a/packages/config/src/constants.ts b/packages/config/src/constants.ts index c0f1e383..20de7175 100644 --- a/packages/config/src/constants.ts +++ b/packages/config/src/constants.ts @@ -122,6 +122,8 @@ export const FIREWALL_VARIABLES = [ 'ssl_verification_status', ] as const; +export const FIREWALL_RULE_CONDITIONAL = ['if', 'and', 'or'] as const; + export const NETWORK_LIST_TYPES = ['ip_cidr', 'asn', 'countries'] as const; export const WAF_MODE = ['learning', 'blocking', 'counting'] as const; diff --git a/packages/config/src/types.ts b/packages/config/src/types.ts index 2ca840dc..195e7977 100644 --- a/packages/config/src/types.ts +++ b/packages/config/src/types.ts @@ -562,44 +562,29 @@ export type AzionApplication = { functionsInstances?: AzionFunctionInstance[]; }; +/** + * Individual firewall behavior types + */ +export type AzionFirewallBehaviorItem = + | { runFunction: string | number } + | { setWafRuleset: { wafMode: FirewallWafMode; wafId: string | number } } + | { + setRateLimit: { + type: FirewallRateLimitType; + limitBy: FirewallRateLimitBy; + averageRateLimit: string; + maximumBurstSize: string; + }; + } + | { deny: true } + | { drop: true } + | { setCustomResponse: { statusCode: number | string; contentType: string; contentBody: string } }; + /** * Firewall behavior configuration for Azion. + * Array of behavior items to be applied when the rule matches. */ -export type AzionFirewallBehavior = { - /** Run a serverless function */ - runFunction?: string | number; - /** Set WAF ruleset */ - setWafRuleset?: { - /** WAF mode */ - wafMode: FirewallWafMode; - /** WAF ID */ - wafId: string | number; - }; - /** Set rate limit */ - setRateLimit?: { - /** Rate limit type */ - type: FirewallRateLimitType; - /** Rate limit by */ - limitBy: FirewallRateLimitBy; - /** Average rate limit */ - averageRateLimit: string; - /** Maximum burst size */ - maximumBurstSize: string; - }; - /** Deny the request */ - deny?: boolean; - /** Drop the request */ - drop?: boolean; - /** Set custom response */ - setCustomResponse?: { - /** HTTP status code (200-499) */ - statusCode: number | string; - /** Response content type */ - contentType: string; - /** Response content body */ - contentBody: string; - }; -}; +export type AzionFirewallBehavior = AzionFirewallBehaviorItem[]; export type AzionFirewallCriteriaBase = { /** Variable to be evaluated */ @@ -639,7 +624,7 @@ export type AzionFirewallRule = { /** Array of criteria for complex conditions */ criteria?: AzionFirewallCriteria[]; /** Behavior to be applied when the rule matches */ - behavior: AzionFirewallBehavior; + behaviors: AzionFirewallBehavior; }; /** @@ -656,8 +641,6 @@ export type AzionFirewall = { networkProtection?: boolean; /** Indicates if WAF is enabled */ waf?: boolean; - /** Variable to be used in the match */ - variable?: RuleVariable; /** List of firewall rules */ rules?: AzionFirewallRule[]; /** Debug mode */