Repository navigation
Releases: Guard-Core/psr15-guard
Release list
v1.6.1
v1.6.1 (2026-10-17)
The request body fix: a handler behind the guard reads the full request body again (v1.6.1)
Fixed
- A downstream handler got an empty request body.
PsrGuardRequest::body()read the PSR-7 body stream forward and left it at end of stream, and the middleware hands the original request to the handler, so every request that passed the guard reached its handler with a body thatgetBody()->getContents()or aread()loop returned as an empty string. Any PSR-15 stack whose handlers or body parsers read the body that way was affected, including Slim apps throughslim-guard. Reads through(string) $request->getBody()were not affected, because a string cast rewinds the stream. A seekable body is now restored to the position it had before the scan (normally 0), and a non-seekable body the engine read is forwarded as aphp://tempcopy holding the bytes already read plus the rest of the stream, copied in 64 KiB chunks so a large body is never held as one string. The 256 KiB scan window (MAX_BODY_BYTES) is unchanged, and block verdicts are unchanged.
Behavior to know before upgrading
- The handler receives the original request instance, except when the body stream is non-seekable and the engine read it. In that case it receives a copy made with
withBody()whose body is the replayedphp://tempstream. If the engine never reads the body (scanning off, or a block before the body stage), nothing is read and the original request is passed on untouched. - No engine floor change: this release still requires guard-core-php ^4.4.0.
v1.6.0
v1.6.0 (2026-10-16)
The response modifier release: the engine floor moves to guard-core-php ^4.4.0, the engine's customResponseModifier reaches pass-through responses, and the agent wiring is documented (v1.6.0)
Changed
- Engine floor raised to guard-core-php ^4.4.0, the shipped Packagist release.
composer.jsonfloorsrennf93/guard-core-phpat^4.4.0andcomposer.lockresolves it at v4.4.0.GuardEngine::applyResponseModifier()and the 4.4.0 config validation are not present in earlier engines.
Added
- The engine response modifier runs on pass-through responses.
finish()callsGuardEngine::applyResponseModifier()between the engine security headers and the CORS verdict headers, soSecurityConfig::customResponseModifiernow applies to the responses the downstream handler produced, including excluded-path and all-checks-bypass responses, and never twice to a response the engine already modified. Nothing is read from the response while no modifier is configured.Set-Cookieis not exposed to the modifier, an unchanged header keeps its original value list, and a body that cannot be read as a string keeps its body (only its status and headers can change). A modifier that throws leaves the response unmodified. - Agent and dynamic rule documentation. The README and
docs/usage.mddescribe attaching guard-agent-php (enableAgent,enableDynamicRules,dynamicRuleInterval,enableEnrichment) and the lifecycle hooks, andcomposer.jsonlistsrennf93/guard-agent-phpundersuggest.
Behavior to know before upgrading
The engine floor moves to guard-core-php 4.4.0, so these engine changes reach every PSR-15 install that takes this release. They come from the engine, not from this adapter, and nothing in this adapter's own API or config surface is removed or renamed.
-
enableDynamicRulesandenableEnrichmentnow requireenableAgent, like the referencevalidate_agent_config. ASecurityConfigthat sets either withoutenableAgent: trueno longer validates and the engine throws when it is built. AddenableAgent: true(and pass an agent handler) or drop the option. -
IP list precedence follows the reference. A configured whitelist decides alone and the blacklist applies only when no whitelist is configured. A route whitelist skips the global lists for the request, and the route blacklist is consulted only when the route has no whitelist.
-
Client IP canonicalization follows the reference.
X-Forwarded-Forentries lose their port, a mapped peer inside an IPv4 CIDR oftrustedProxiesis no longer trusted, and list matching canonicalizes the entry, so a ban, a rate limit or a list entry can match a client that it did not match before. -
Dynamic rule versions no longer stack. Each new version applies over the base config and no longer carries over the previous version's changes. An
ip_blacklistdynamic rule is now a timed ban (foripBanDurationseconds) instead of a merge into the static list. -
A new per-request scan budget,
detectionMaxScanSeconds(default 10.0, range 0.1 to 60.0), caps regex scan CPU time. Once it is spent the remaining patterns are skipped and the request records a weightedpattern_timeoutthreat, so a very expensive request can now be flagged where it was scanned to the end before. -
A passive-mode suspicious log line without a trigger has new wording (
[PASSIVE MODE] Suspicious activity detected from <ip>: ... - Reason: <reason> - Headers: ...), so update any log match on the old text.
The modifier seam is what slim-guard composes: a slim-guard that picks up this release applies customResponseModifier to Slim pass-through responses with no change of its own.
Verification
- Local gates on php 8.5.11 with a private Redis on port 6390:
composer install, the php -l sweep,composer validate --strictandcomposer auditexit 0,bin/test_psr15.php112/112 checks green (including the modifier assertions), and the 100% line coverage gate passes.composer.lockresolves guard-core-php v4.4.0 from packagist.
v1.5.0
v1.5.0 (2026-10-09)
The decorator-seam release: the engine constraint repins to guard-core-php ^4.3.2 plus the decorator handler pass-through, the agent_stats/reset/refreshCloudIpRanges guard surface, and method-scoped route patterns (v1.5.0)
Changed
- Engine constraint repinned to guard-core-php ^4.3.2, the shipped parity release.
composer.jsonfloorsrennf93/guard-core-phpat the released^4.3.2andcomposer.lockresolves it at v4.3.2 from packagist, replacing thedev-master as 4.3.99alias the decorator seam PR (#25) carried as registered debt.
Added
- Agent surface (PR #23).
GuardMiddlewarekeeps the injected agent handler and exposesagentStats():{enabled: false, degraded: false}without a handler, the handlergetStats()map merged underenabled: truewith one (fastapi-guardmiddleware.agent_statssemantics).reset()delegates to the engine rate-limit handler's reset (fastapi-guardmiddleware.reset).refreshCloudIpRanges()mirrorsmiddleware.refresh_cloud_ip_ranges: a no-op while cloud blocking is disabled, a store-backed refreshAsync of the configured providers with the configured ttl otherwise. - Method-scoped route patterns (PR #23, the method half of A3). Route patterns may carry a method prefix ("GET /api/users"): method-scoped patterns match one method, bare patterns match every method, and a method-scoped pattern wins a same-path tie (the method half of the reference
_match_router). - The decorator handler seam (PR #25, the set_decorator_handler analog). The constructor's
decoratorHandlerparameter accepts the engine decorator family handler (RenzoFranceschini\GuardCore\Decorators\SecurityDecorator): its decorated routes (route pattern keys and callable-endpoint route ids) merge under the explicit$routesmap (an explicit entry wins a shared pattern) and the handler is wired into the engine (GuardEngine::setDecoratorHandler) so requests stamped with a route id resolve their RouteConfig through the handler's registry. The routes map is de-promoted to a plain readonly property assigned once in the constructor (the merge happens before the single assignment). - Upstream drift stamp (PR #26). The daily drift workflow checks out engine master and requires it as
dev-master(the mounted checkout IS engine master), so the job stays green against the shipped-range adapter constraint. - Post-transfer metadata sweep (PR #27). Repo URLs, the Pages host, the lock's engine source/dist URLs, the drift workflow checkout slug, mkdocs repo_url, FUNDING, the CODE_OF_CONDUCT enforcement contact, and a new
.github/CODEOWNERSpoint at the Guard-Core org.
Verification
- Local gates on php 8.5.11:
composer validateexit 0,composer audit --lockedclean,make lintexit 0,bin/test_psr15.phpall checks green with host Redis on 6379 (including the decorator handler and method-scoped pattern sections),composer.lockresolving guard-core-php v4.3.2 from packagist. PHPStan level 8 via theghcr.io/phpstan/phpstan:latestimage: no errors.
v1.4.0
v1.4.0 (2026-10-07)
The engine-repin release: guard-core-php ^4.3.1 from packagist plus the longest-path route patterns, optional agent wiring and the JSON status handler (v1.4.0)
Changed
- Engine constraint repinned to guard-core-php ^4.3.1, the shipped parity release.
composer.jsonfloorsrennf93/guard-core-phpat the released^4.3.1andcomposer.lockresolves it at v4.3.1 from packagist, replacing the dev-master alias; everything resolves from the registry with no path or VCS repository entries.
Added
- FP-PHP parity surface (PR #19). Route patterns resolve most-specific-first (length-descending), so the longest matching path pattern wins regardless of insertion order, closing the first-match insertion-order divergence with the reference implementations. An optional
agentHandlerconstructor argument wires a duck-typed agent (sendEvent) into the engine's event bus.GuardStatusRequestHandler(PSR-15RequestHandlerInterface) servesGuardEngine::initializationStatus()as JSON, mirroring fastapi-guard's status route.
Verification
- Local gates on php 8.5.11:
composer validateexit 0,composer audit --lockedclean (zero open advisories),make lintexit 0, andbin/test_psr15.php112/112 checks green with host Redis on 6379 (includes the three new parity assertions), withcomposer.lockresolving guard-core-php v4.3.1. PHPStan level 8 via theghcr.io/phpstan/phpstan:latestimage: no errors. Dockerized live smoke (examples/simple_app, port 8080): all six workflow assertions green.
v1.3.0
v1.3.0 (2026-10-01)
Changed
- Engine floor raised to guard-core-php ^4.3.0, the safety-corpus release. 1.3.0 floors
rennf93/guard-core-phpto ^4.3.0 and picks up the 4.3.0 engine train: the spec 12 event bus with dynamic rules and Redis-backed metrics persistence, the spec 10 geo download and refresh lifecycle, the spec 04 ReDoS safety gates over PCRE plus the performance monitor, and the pattern_safety / events / redis_interop conformance runners. Nothing in the PSR-15 surface changes: the middleware is a thin translation layer, so the new engine surfaces ride config through the unchangedGuardMiddlewareoption map. The CI engine checkout stamps the mounted sibling path checkout at 4.3.99 (ci.yml, release.yml, static-analysis.yml, upstream-drift.yml, scheduled-lint.yml) so the floor resolves against the engine master while keeping the published constraint ^4.3.0, andcomposer.lockresolves the engine at the released v4.3.0.
Added
- Process scaffold (PR #15).
SECURITY.md(supported versions, advisory reporting),CONTRIBUTING.md,CODE_OF_CONDUCT.md,.github/FUNDING.yml, the PR template and the issue templates, modeled on the Go family and the fastapi-guard baseline..github/workflows/static-analysis.yml: phpstan via the docker pattern these repos already use (composer:2 install with the same ci-only path-repository patch as ci.yml, thenghcr.io/phpstan/phpstan:latest analyse src --level=8), on push/pr to master plus the weekly Monday cron; level 8 is the highest level with zero findings on master (max reports one return-type generics finding)..github/workflows/live-smoke.ymlgains thelive-smoke-advancedjob (the existing simple_app job untouched): a dockerized run ofexamples/advanced_appon host port 8081 assertingGET /andGET /health200 plus one blocked-request check (GET /admin/check?ip=203.0.113.9withoutX-Admin-Tokenreturns the admin gate's 400 "Missing required header: X-Admin-Token"). CodeQL was drafted and dropped after the first run proved GitHub CodeQL no longer supports PHP (Did not recognize the following languages: php);.github/workflows/semgrep.ymlis the substitute, running the pinned semgrep/semgrep:1.177.0 container with the p/security-audit and p/secrets rulesets over src/ (path-filtered push/PR triggers plus a weekly Monday cron, metrics off). - 100% line coverage gate (PR #16, the PHP-family pilot). The bespoke
bin/test_psr15.phpsuite stays as it is;.github/coverage-runner.php(phpunit/php-code-coverage ^11 + pcov) executes the suite under coverage and the new CI coverage job gates on 100.00% measured lines in src/. The suite already measured 100.00% lines locally (pcov, php 8.3), so no test changes were needed; the job mirrors ci.yml's path-repo sibling pattern (php 8.3, coverage: pcov) and suite pass/fail stays gated by the existing test job.
Verification
make lintexit 0 andmake testexit 0 on php 8.5.11 (host Redis on 6379, 112/112 checks green), withcomposer.lockresolving guard-core-php v4.3.0.
v1.2.0
v1.2.0 (2026-09-27)
Changed
- Engine floor raised to guard-core-php ^4.2.0, the parity release. The 4.1.0 family tags were a version-accuracy error and were yanked/unpublished, so the published ^4.1.0 constraint of v1.1.0 does not resolve publicly; 1.2.0 floors
rennf93/guard-core-phpto ^4.2.0 and picks up the 4.2.0 engine train: the on_block hook payloads now carry the reference log-format reasons, route-level IP rules (RouteConfigipWhitelist/ipBlacklist/blockedCountries/whitelistCountries) are enforced, and the spec 4.1.0 corpus runs with an empty divergence registry (219 cases, pipeline gate 35 passed / 0 failed / 0 xfail). The CI engine checkout stamps the mounted sibling path checkout at 4.2.99 so the floor resolves against the engine master while keeping the published constraint ^4.2.0.
Added
- Parity pass-through surface.
GuardMiddlewarenow finishes pass-through responses the way the reference response factory does: the engine's security headers (securityHeaders) and CORS verdict headers (enableCors,corsAllow*) are merged onto the handler's response, and the engine's behavioral return rules observe the response status plus a body prefix bounded bybehavior_max_response_body_inspect_bytes(captured from the PSR-7 stream only whilebehavior_scan_response_bodyis on, stream rewound afterwards when seekable), soglobalBehaviorRulesand routebehaviorRuleswithreturn_patternrules act on what the application actually served. - Per-route configuration. The middleware takes
routes(path pattern toRouteConfig, exact or trailing-slash prefix match) and an optionalrouteResolverclosure receiving the raw PSR-7 server request, attaching per-route behavior rules, detection exclusions, rate-limit tiers and check bypasses to the engine's request state. - Geo rate-limit resolver. A
geoRateLimitResolveroption injects the country resolver that powersRouteConfiggeoRateLimitstiers; when absent, the middleware bridges the engine config'sgeo_ip_handler(kept by the engine only when country lists are configured). - Reachability tests for every new surface in
bin/test_psr15.php(108 checks green) and pass-through/route/geo documentation in the README anddocs/configuration.md.
v1.1.0
v1.1.0 (2026-09-26)
guard-core-php floor to ^4.1.0 with the required text/plain block-response assertion (v1.1.0)
- Compatibility - The
rennf93/guard-core-phpengine floor rises from^4.0.4to^4.1.0; this release tracks the guard-core-php 4.1.0 engine. - Tests - The explicit
text/plainblock-response Content-Type is now a required assertion in the suite (matching the engine's reference response factory), no longer optional.
v1.0.0
First stable release of psr15-guard, the PSR-15 middleware adapter for guard-core-php 4.0.4.
Added
- PSR-15 middleware adapter for guard-core-php 4.0.4.
GuardMiddlewaretranslates any PSR-7ServerRequestInterfaceinto the guard-core engine (RenzoFranceschini\GuardCore\Engine\GuardEngine) and translates block verdicts back to PSR-7 responses, so the guard works with Slim 4, Mezzio, Symfony PSR-15 bridges, or any PSR-7/PSR-15 stack. - Exact block translation. Blocked requests get the engine's
BlockResponsetranslated status-for-status, body-for-body and header-for-header:403 Forbiddenfor a blacklisted IP,429 Too many requestswithRetry-Afterfor a rate limit hit. Passing requests continue down the stack untouched. - Fail-closed behavior. If the engine throws, the middleware returns the engine's fail-closed response (
500 Security check failed, honorably overridden bycustomErrorResponses) instead of letting the request through. - Bounded body read. The request body is scanned as a prefix of at most 256 KiB (
PsrGuardRequest::MAX_BODY_BYTES, matching the engine's full-scan window). - Distributed state via Redis. Distributed rate limits, IP bans, and cloud-range caches require Redis (
enableRedis: true);redisFailOpen: truekeeps serving when Redis is unreachable,falsefails closed at construction.
Changed
- The engine dependency is pinned to
rennf93/guard-core-php^4.0.4, the first stable engine release, replacing the^0.1.0pin to the burned pre-release snapshot tag.
Internal (v1.0.0)
- Plain-PHP test suite in
bin/test_psr15.php(unit coverage always; Redis-backed shared-state integration cases run whenREDIS_HOSTpoints at a reachable Redis), plus aphp -lsweep andcomposer auditin CI across PHP 8.2, 8.3 and 8.4. - Community workflows, a MkDocs documentation site, and simple and advanced example apps landed via the parity-polish pass.