rennf93/guard-core-php
Framework-agnostic core for Guard, the PHP authentication and authorization library. Provides reusable primitives for guards, identity checks, and access control, usable in standalone PHP apps or alongside Laravel and other frameworks.
This package is a request-level detection engine (WAF-style) with an optional agent for dynamic rules and metrics. Day to day you interact with it through middleware, config/*.php, and two config objects: SecurityConfig (global) and RouteConfig (per route).
Minimal setup:
composer require the package, then publish the config (php artisan vendor:publish) and open the config file first — that is where every knob from this release lives. docs/configuration.md and docs/agent.md are the two files to read after that.web (or a dedicated) group. Decide placement early: put it before session/auth middleware if you want to block bad traffic before Laravel boots the heavy stack.trustedProxies to your load balancer / ingress CIDRs. This is the single most important setting — client identity, IP lists, rate limits and bans all key off it. If you are behind a Unix-socket proxy, trustedProxies now accepts the unix token.[PASSIVE MODE] Suspicious activity detected from <ip>: ... - Reason: <reason> - Headers: ... — build your alerting regex against the Reason: field, not against the whole line.First real use case: attach the middleware to a public-facing group (login, search, comments), leave detectionMaxScanSeconds at the default 10.0, and tune exclusions based on passive logs. Only turn on enableAgent + enableDynamicRules once you are enforcing and have somewhere to consume metrics.
Global base config + per-route overlay. Keep SecurityConfig conservative and use RouteConfig on route groups to tighten or loosen. Route config is merged with global, and excludedSemanticDetectionHeaders merges too — so a global exclusion of Cookie does not have to be repeated per route, and a route can add Authorization without touching the global file. There is also a fluent detectionExclusion(semanticHeaders: [...]) form if you build config in a service provider.
Semantic-only exclusions for noisy headers. excludedSemanticDetectionHeaders skips the semantic analyzer only; the regex scan still runs. That is the right lever for headers you consider "expected weird" (session blobs, base64 payloads), while keeping regex coverage for actual attack signatures. Do not use it to silence a header entirely — it won't.
One response modifier as the seam. Put all response mutation in customResponseModifier (or call GuardEngine::applyResponseModifier() from your own adapter). It runs once, after the security headers and before CORS, on every response the engine returns — denials and pass-throughs alike. This is where you add correlation IDs, custom block pages or structured JSON bodies. For adapters wrapping the engine, applyResponseModifier() is the documented entry point; use it rather than re-implementing the ordering.
Dynamic rules as an overlay, not a diff. With enableAgent and enableDynamicRules on, each rule version is applied over the base config:
ip_blacklist goes through the engine ban manager for ip_ban_duration seconds; ip_whitelist unbans.blocked_countries, whitelist_countries, block_cloud_providers, blocked_user_agents and endpoint_rate_limits replace base values.So author rules as complete state, not incremental patches, and treat a rule removal as "revert to base" rather than "delete".
Interval-gated fetching. dynamicRuleInterval (default 300, min 60) is enforced in execute(), so the first request fetches and then one fetch per interval. Combined with dynamicRulesCachePath, this is what makes multi-node deployments sane: point all nodes at a shared cache path and let the interval do the coordination. Do not set it below 60 — validation rejects it.
Metrics pipeline. Attach an agent handler that implements sendMetric and turn on agentEnableMetrics; processResponse then emits response_time, request_count and error_rate. Push those into your existing Prometheus/StatsD path rather than inventing new dashboards — these are the three signals that tell you whether a detectionMaxScanSeconds value is actually appropriate for your endpoints.
Scan budget as a latency control. Lower detectionMaxScanSeconds on latency-sensitive endpoints (JSON APIs, autocomplete) and leave it generous on authenticated form posts. Exhaustion is not silent: it records a weighted pattern_timeout threat with pattern scan_time_budget_exhausted, so you can alert on that pattern name and raise the budget where it fires.
Trusted IP checks in your own code. Use CanonicalIp::ipInList() for any custom allow/deny logic instead of hand-rolled in_array/CIDR comparisons. It canonicalizes entries so an IPv4 entry, IPv4 CIDR, IPv4-mapped IPv6 entry and bracketed entry all match a client connecting as IPv4-mapped IPv6.
enableDynamicRules and enableEnrichment now require enableAgent. This is the upgrade trap. A config that sets either without enableAgent no longer validates — you get a validation failure at boot, not a runtime warning. Check every environment's config, especially staging copies.
Whitelist semantics changed and will change who gets blocked. A configured whitelist now decides alone; the blacklist applies only when no whitelist is configured. The same rule applies per route: a route whitelist skips the global lists, and checkRouteIpAccess only consults the route blacklist when the route has no whitelist. If you previously maintained "whitelist for known-good, blacklist for known-bad" in parallel, the blacklist entries are now dead config. Audit before you deploy.
Some clients that were not matched before now match. Client identity resolution was brought to reference behavior: X-Forwarded-For entries lose their port, empty entries are kept (which shifts which entry the depth selects), and an over-counting depth falls back to a right-to-left walk. Bans, rate limits and list entries can therefore start hitting clients they used to miss. Re-check your trustedProxies depth against real traffic headers after upgrading; this is the highest-risk change in the release.
Mapped peers inside IPv4 CIDRs are no longer trusted. If trustedProxies lists 10.0.0.0/8 and a peer arrives as ::ffff:10.0.0.1, it will no longer be trusted. Add the mapped form if you rely on it.
Malformed CIDRs no longer abort the search. Good for resilience, bad for silent misconfiguration — a typo in trustedProxies is now skipped quietly rather than loudly failing. Validate that list in CI.
Spoofing events fire once per request. An X-Forwarded-For from an untrusted peer raises spoofing_detected and logs the spoof line once, with a redacted chain. Do not build handlers that expect an event per hop, and do not log the raw chain yourself.
detect() is not budget-capped. Calling GuardEngine::detect() directly (jobs, console commands, unit tests) bypasses detectionMaxScanSeconds entirely. Convenient for tests, dangerous if you assume the cap protects a queue worker scanning user content.
Response modifier exceptions are swallowed. An exception inside the modifier leaves the response unmodified — you get no error surfaced. Wrap your own logic in try/catch and log to your own channel, otherwise a broken modifier looks identical to an unset one.
Cookie header is scored per pair now. The ; between cookies no longer counts as a command separator, so logged-in requests with several cookies stop being false positives — but an attack split across cookie boundaries scores lower than it used to. Keep regex coverage on the header rather than relying on the semantic score alone.
SecurityConfig::with() used to drop keys. enableAgent, enableDynamicRules, agentEnableEvents, agentEnableMetrics and dynamicRulesCachePath are no longer dropped. If you worked around this by assigning properties after with(), the workaround is harmless but no longer needed — and if you were relying on a key staying unset, verify.
Dynamic rule versions no longer inherit. A new version does not carry over the previous version's changes. Every version must be authored against the base config as if it were the first, which makes rule authoring idempotent and cache-safe but means a partial rule set silently removes previously active state.
Emergency mode halves the auto-ban threshold exactly once per activation (emergency_mode_activated event). Do not model it as a persistent multiplier or stack it with your own thresholds.
Passive log wording changed. A line without a trigger now includes - Reason: <reason>. Update any log-based alerting or grep before rotating to this version. The stale logSensitiveHeaders address-header workaround in the docs is gone — if you copied it, remove it, since it skipped those headers from detection entirely.
How can I help you explore Laravel packages today?