From 332c2b973411240564eb9ea49d372c52d085b372 Mon Sep 17 00:00:00 2001 From: Renn F Date: Thu, 15 Oct 2026 20:47:38 +0200 Subject: [PATCH] release(4.4.0): scan budget and IP parity release, lockstep core engines at 4.4.0 (#143) Bumps all five packages to 4.4.0 and writes the 4.4.0 CHANGELOG section. The two engine-version literals in the validation-cache and agent-config tests move with the bump. --- CHANGELOG.md | 17 +++++++++-------- package.json | 2 +- packages/core/package.json | 2 +- .../validation-cache.test.ts | 2 +- .../core/tests/test-models/agent-config.test.ts | 2 +- packages/express/package.json | 2 +- packages/fastify/package.json | 2 +- packages/hono/package.json | 2 +- packages/nestjs/package.json | 2 +- 9 files changed, 17 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61fcace..b1a8546 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,15 +5,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [4.4.0] - 2026-10-15 -### Fixed - -- Client IP resolution now matches the reference `extract_client_ip`. The resolved identity is canonical (an IPv4-mapped IPv6 address such as `::ffff:127.0.0.1`, which is how a dual-stack Node listener reports an IPv4 client, becomes `127.0.0.1`), is computed once in the `route_config` check and stored on `request.state.client_ip`, and is what `ip_security`, `rate_limit`, `suspicious_activity`, `cloud_provider`, `user_agent`, `emergency_mode`, bans, behavioral tracking, request logging and events read, where they previously read the raw connecting address. `X-Forwarded-For` follows the reference chain rules: honored only from a trusted connecting peer, entries counted from the right at `trustedProxyDepth` with empty entries kept, ports and brackets stripped, invalid or wildcard entries rejected, the right-to-left walk when the declared depth over-counts proxy hops, and the redacted spoof report for a header from an untrusted peer. IP list entries and CIDRs (blacklist, whitelist, `exemptIps`, route lists, `emergencyWhitelist`) are compared in canonical form, so an IPv4 entry or an IPv4 CIDR matches a client that connected as IPv4-mapped IPv6. `trustedProxies` accepts the `unix` token, and a request with no peer address resolves through the forwarded chain when it is present. IP entries are validated with strict address parsing. `canonicalizeIp` now follows the reference exactly (scoped addresses keep their zone, `::1.2.3.4` is not read as IPv4-mapped). -- An excluded path, an all-checks bypass route and a request with no client address no longer end the request in the adapter with an empty 200 (the route handler never ran). The Express, Fastify, Hono and NestJS adapters now continue to the real downstream handler in every case where the reference does. An excluded path is exclusion-scoped like the reference (`guard_exclusion_scoped`): only `route_config`, `ip_security` (ban check plus global IP and country rules, without route rules or ban escalation) and `rate_limit` still run, behavior rules are not tracked, and the response still gets security headers, CORS headers and request metrics. An all-checks bypass route runs only `customResponseModifier` on the real response. A request with no client address is answered 403 under the default `failSecure` (with `onBlock`) and otherwise continues as identity `unknown`. `BypassHandler.handlePassthrough` no longer calls its `callNext` argument, and `BypassHandler.beginSecurityBypass` is the announce half of `handleSecurityBypass` for adapters that cannot await the downstream response. Ports guard-core `handle_passthrough` and `enforced_on_excluded_paths`. -- Fastify: a request blocked in `preValidation` no longer reaches the `onSend` response pass, so it emits no request metrics (reference: metrics only for pass-through responses). The response body is captured for string and Buffer payloads, capped at 10000 bytes, so body-based behavioral return rules fire. -- Hono: the response body is read (capped at 10000 bytes, from a clone, never for `text/event-stream`) when a body-based behavioral return rule is configured and `behaviorScanResponseBody` is on, so those rules fire. The client address now resolves by default on Node (`@hono/node-server`), Deno, Bun and Cloudflare Workers (`resolveHonoClientAddress`); the previous default only understood a string `c.env.remoteAddr`. -- The `Cookie` header is scanned one `name=value` pair at a time, like query parameters. As a single value, the `;` between cookies counted as command separators in the semantic score, so an ordinary logged-in header (a JWT session cookie plus `theme` and `_ga`) could reach 0.71 and be blocked. Each pair takes the normal header path, empty pairs are skipped, only spaces around a pair are trimmed (a CR or LF stays in the scanned value), and each pair counts against the value budget. Ports guard-core #162 (issue #161). +The scan-budget release, with client IP, IP list and dynamic rule parity with the reference engine and adapters that reach the real handler. Lockstep release with the core engines at 4.4.0. ### Added @@ -22,11 +16,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **Behavior to know before upgrading.** The Express, Fastify, Hono and NestJS adapters now run the downstream route handler for excluded paths, all-checks bypass routes and requests with no client address, where they used to answer an empty 200. The client identity is canonical (an IPv4-mapped IPv6 address is read as IPv4), so every ban, rate-limit key, IP list match and event now uses the IPv4 form. A configured whitelist now decides alone in `isIpAllowed` and `checkRouteIpAccess`, a route whitelist match no longer overrides a blocked country, and the unknown identity and an ungeolocated client are treated as in the reference (see Fixed). A dynamic rule version no longer carries over the previous version's changes: rules that relied on stacking versions must set every field they want. A request that spends `detectionMaxScanSeconds` of regex CPU time is flagged with a `pattern_timeout` threat. A logged-in `Cookie` header that used to be rejected as a command-separator false positive now passes. - The `enableDynamicRules` description lists exactly what a rule can change. - The detection scan yields to the event loop every ~2 ms of scanning, so one request's scan no longer holds the loop for its whole duration. ### Fixed +- Client IP resolution now matches the reference `extract_client_ip`. The resolved identity is canonical (an IPv4-mapped IPv6 address such as `::ffff:127.0.0.1`, which is how a dual-stack Node listener reports an IPv4 client, becomes `127.0.0.1`), is computed once in the `route_config` check and stored on `request.state.client_ip`, and is what `ip_security`, `rate_limit`, `suspicious_activity`, `cloud_provider`, `user_agent`, `emergency_mode`, bans, behavioral tracking, request logging and events read, where they previously read the raw connecting address. `X-Forwarded-For` follows the reference chain rules: honored only from a trusted connecting peer, entries counted from the right at `trustedProxyDepth` with empty entries kept, ports and brackets stripped, invalid or wildcard entries rejected, the right-to-left walk when the declared depth over-counts proxy hops, and the redacted spoof report for a header from an untrusted peer. IP list entries and CIDRs (blacklist, whitelist, `exemptIps`, route lists, `emergencyWhitelist`) are compared in canonical form, so an IPv4 entry or an IPv4 CIDR matches a client that connected as IPv4-mapped IPv6. `trustedProxies` accepts the `unix` token, and a request with no peer address resolves through the forwarded chain when it is present. IP entries are validated with strict address parsing. `canonicalizeIp` now follows the reference exactly (scoped addresses keep their zone, `::1.2.3.4` is not read as IPv4-mapped). +- An excluded path, an all-checks bypass route and a request with no client address no longer end the request in the adapter with an empty 200 (the route handler never ran). The Express, Fastify, Hono and NestJS adapters now continue to the real downstream handler in every case where the reference does. An excluded path is exclusion-scoped like the reference (`guard_exclusion_scoped`): only `route_config`, `ip_security` (ban check plus global IP and country rules, without route rules or ban escalation) and `rate_limit` still run, behavior rules are not tracked, and the response still gets security headers, CORS headers and request metrics. An all-checks bypass route runs only `customResponseModifier` on the real response. A request with no client address is answered 403 under the default `failSecure` (with `onBlock`) and otherwise continues as identity `unknown`. `BypassHandler.handlePassthrough` no longer calls its `callNext` argument, and `BypassHandler.beginSecurityBypass` is the announce half of `handleSecurityBypass` for adapters that cannot await the downstream response. Ports guard-core `handle_passthrough` and `enforced_on_excluded_paths`. +- Fastify: a request blocked in `preValidation` no longer reaches the `onSend` response pass, so it emits no request metrics (reference: metrics only for pass-through responses). The response body is captured for string and Buffer payloads, capped at 10000 bytes, so body-based behavioral return rules fire. +- Hono: the response body is read (capped at 10000 bytes, from a clone, never for `text/event-stream`) when a body-based behavioral return rule is configured and `behaviorScanResponseBody` is on, so those rules fire. The client address now resolves by default on Node (`@hono/node-server`), Deno, Bun and Cloudflare Workers (`resolveHonoClientAddress`); the previous default only understood a string `c.env.remoteAddr`. +- The `Cookie` header is scanned one `name=value` pair at a time, like query parameters. As a single value, the `;` between cookies counted as command separators in the semantic score, so an ordinary logged-in header (a JWT session cookie plus `theme` and `_ga`) could reach 0.71 and be blocked. Each pair takes the normal header path, empty pairs are skipped, only spaces around a pair are trimmed (a CR or LF stays in the scanned value), and each pair counts against the value budget. Ports guard-core #162 (issue #161). +- The IP list and route country checks now match the reference engine (issues #139, #141). A configured whitelist decides alone: `isIpAllowed` and `checkRouteIpAccess` consult the blacklist only when no whitelist is configured, where they used to run the blacklist first. `checkRouteIpAccess` combines the route IP list verdict with the route country verdict (false if either is false, true if either is true, otherwise no verdict), so a route whitelist match no longer overrides a blocked country. `isIpAllowed` allows the unknown client identity unless a whitelist or a country whitelist is configured, and a whitelist match skips the country verdict. `checkIpCountry` blocks a client with no geolocation when a country whitelist is configured. Each path is covered by tables generated by running the reference. - Received dynamic rules are now applied. `DynamicRuleManager` fetched, validated, stored and persisted rules but never acted on them, so an `ipBlacklist`, `emergencyMode` or `globalRateLimit` rule had no effect on request handling. The apply step (the reference `_dynamic_rule_application`) now bans the `ipBlacklist` IPs for `ipBanDuration`, unbans the `ipWhitelist` IPs, adds `suspiciousPatterns` through the pattern validator, and replaces the live country lists, rate limits, cloud providers, `blockedUserAgents` (ReDoS validated), feature toggles, auto-ban threshold and duration, and emergency mode with its whitelist (which also halves the auto-ban threshold). A failed apply restores the previous config, an expiring rule restores the pre-rule config, and boot hydration applies the last-known rules. The rule model gains the `autoBanThreshold`, `autoBanDuration` and `enableRateLimitAutoBan` overrides. `SusPatternsManager.addPattern` now resolves a boolean (false when the validator refuses the pattern). - Each dynamic rule version now applies over the base config instead of over the previous version's result. The apply step restores the base snapshot taken before the first rule, then applies the new version, so the emergency auto-ban halving no longer compounds (16 went 8, 4, 2 across versions 1 to 3, and now stays 8), a change from an earlier version no longer lingers when a later version omits it, and expiry restores the true base. A failed apply restores the config as it was before the call, so the previously active version keeps running. `suspiciousPatterns` the registry already holds are skipped instead of re-added. Ports guard-core #169. diff --git a/package.json b/package.json index 753270b..724fd41 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "guardcore-ts", - "version": "4.3.3", + "version": "4.4.0", "private": true, "description": "TypeScript port of guard-core — framework-agnostic security middleware engine", "scripts": { diff --git a/packages/core/package.json b/packages/core/package.json index 80bc8a7..fa9d566 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@guardcore/core", - "version": "4.3.3", + "version": "4.4.0", "description": "Framework-agnostic security middleware engine for Node.js and edge runtimes", "type": "module", "main": "./dist/index.cjs", diff --git a/packages/core/tests/test-detection-engine/validation-cache.test.ts b/packages/core/tests/test-detection-engine/validation-cache.test.ts index 338c8bf..0d77335 100644 --- a/packages/core/tests/test-detection-engine/validation-cache.test.ts +++ b/packages/core/tests/test-detection-engine/validation-cache.test.ts @@ -97,7 +97,7 @@ describe('PatternValidationCache', () => { const values = Object.values(stored); expect(values.length).toBe(1); expect(values[0]['version']).toBe(ENGINE_VERSION); - expect(engineVersion()).toBe('4.3.3'); + expect(engineVersion()).toBe('4.4.0'); }); it('a corrupt cache starts empty and recovers', async () => { diff --git a/packages/core/tests/test-models/agent-config.test.ts b/packages/core/tests/test-models/agent-config.test.ts index 4eda9e7..c2b5a34 100644 --- a/packages/core/tests/test-models/agent-config.test.ts +++ b/packages/core/tests/test-models/agent-config.test.ts @@ -93,7 +93,7 @@ describe('toAgentConfig (the reference to_agent_config seam)', () => { const result = toAgentConfig(config({ onError })); expect(result?.onError).toBe(onError); expect(result?.guardCoreVersion).toBe(ENGINE_VERSION); - expect(result?.guardCoreVersion).toBe('4.3.3'); + expect(result?.guardCoreVersion).toBe('4.4.0'); }); it('rejects an out-of-range status interval and overflow policy at the schema', () => { diff --git a/packages/express/package.json b/packages/express/package.json index ec3c882..a362c1a 100644 --- a/packages/express/package.json +++ b/packages/express/package.json @@ -1,6 +1,6 @@ { "name": "@guardcore/express", - "version": "4.3.3", + "version": "4.4.0", "description": "Express adapter for @guardcore/core security middleware", "type": "module", "main": "./dist/index.cjs", diff --git a/packages/fastify/package.json b/packages/fastify/package.json index 03614ea..afab631 100644 --- a/packages/fastify/package.json +++ b/packages/fastify/package.json @@ -1,6 +1,6 @@ { "name": "@guardcore/fastify", - "version": "4.3.3", + "version": "4.4.0", "description": "Fastify adapter for @guardcore/core security middleware", "type": "module", "main": "./dist/index.cjs", diff --git a/packages/hono/package.json b/packages/hono/package.json index 98fd388..6b0e270 100644 --- a/packages/hono/package.json +++ b/packages/hono/package.json @@ -1,6 +1,6 @@ { "name": "@guardcore/hono", - "version": "4.3.3", + "version": "4.4.0", "description": "Hono adapter for @guardcore/core security middleware (edge-safe)", "type": "module", "main": "./dist/index.cjs", diff --git a/packages/nestjs/package.json b/packages/nestjs/package.json index c88bfb3..446dbde 100644 --- a/packages/nestjs/package.json +++ b/packages/nestjs/package.json @@ -1,6 +1,6 @@ { "name": "@guardcore/nestjs", - "version": "4.3.3", + "version": "4.4.0", "description": "NestJS adapter for @guardcore/core security middleware", "type": "module", "main": "./dist/index.cjs",