Symfony middleware adapter for [guard-core-php](https://github.com/Guard-Core/guard-core-php): maps Symfony `HttpFoundation` Request objects to the guard-core engine and translates block verdicts back to Symfony-native responses. Works with Symfony 6.4 LTS and 7.x.
Website · Docs · Playground · Dashboard · Discord
Guard Core is the Python engine. Framework adapters are thin wrappers that translate native request/response types into Guard Core's protocols. The telemetry agents ship security events and metrics to the monitoring backend. Parallel engine implementations exist for Go, PHP, TypeScript (on npm), and Rust (on crates.io) - all ports of the same reference semantics, conformance-tested against the shared adversarial corpus.
| Package | Role | PyPI |
|---|---|---|
| guard-core | Framework-agnostic security engine | |
| guard-agent | Telemetry agent | |
| fastapi-guard | FastAPI / Starlette adapter | |
| flaskapi-guard | Flask adapter | |
| djapi-guard | Django adapter | |
| tornadoapi-guard | Tornado adapter |
Go modules published via GitHub releases. Production-ready.
| Package | Role | Release |
|---|---|---|
| guard-core-go | Go engine | |
| nethttp-guard | net/http adapter | |
| gin-guard | Gin adapter | |
| echo-guard | Echo (v4) adapter | |
| fiber-guard | Fiber (v3) adapter | |
| guard-agent-go | Telemetry agent |
Published on Packagist under the rennf93 vendor. Production-ready.
| Package | Role | Packagist |
|---|---|---|
| guard-core-php | PHP engine | |
| laravel-guard | Laravel adapter | |
| symfony-guard | Symfony adapter | |
| psr15-guard | PSR-15 adapter | |
| slim-guard | Slim 4 adapter | |
| guard-agent-php | Telemetry agent |
Published under the @guardcore npm scope; source in the guard-core-ts monorepo. Production-ready.
| Package | Role | npm |
|---|---|---|
| @guardcore/core | Core engine | |
| @guardcore/express | Express adapter | |
| @guardcore/nestjs | NestJS adapter | |
| @guardcore/fastify | Fastify adapter | |
| @guardcore/hono | Hono (edge) adapter | |
| guardagent | Telemetry agent |
Published on crates.io. Production-ready.
| Package | Role | crates.io |
|---|---|---|
| guard-core-engine | Core engine crate | |
| guard-core-rs | Facade crate (consumer entry point) | |
| actix-guard-rs | Actix Web adapter | |
| axum-guard-rs | Axum adapter | |
| tower-guard-rs | Tower adapter | |
| rocket-guard-rs | Rocket adapter | |
| guard-agent-rs | Telemetry agent |
| Package | Role | PyPI |
|---|---|---|
| guard-core-mcp | MCP server: config validation, docs search, detection sandbox |
- The full guard-core-php engine pipeline over Symfony HttpFoundation requests: rate limiting, IP lists, payload inspection, security headers
- Lifecycle integration with Symfony's middleware stack
- Configurable behavior notes for passive preview
📚 Documentation - full technical documentation for this package.
🛡️ Guard Core - the engine's reference documentation.
🤖 Monitoring Agent Integration - monitor your Guard instance with a monitoring agent.
composer require rennf93/symfony-guardWrap your kernel with the middleware (front controller or wherever the kernel is assembled):
use RenzoFranceschini\GuardCore\Config\SecurityConfig;
use RenzoFranceschini\GuardCore\Engine\GuardEngine;
use RenzoFranceschini\GuardCoreSymfony\GuardMiddleware;
use Symfony\Component\HttpFoundation\Request;
// Anywhere your App\Kernel is created
$kernel = new GuardMiddleware(
new App\Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']),
new GuardEngine(new SecurityConfig(
enableRedis: false,
blacklist: ['192.0.2.0/24'],
rateLimit: 100,
rateLimitWindow: 60,
enableRateLimiting: true,
))
);// public/index.php
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);Blocked requests get the engine's block verdict translated exactly (status, body, headers) as a Symfony\Component\HttpFoundation\Response: 403 Forbidden for a blacklisted IP, 429 Too many requests with Retry-After for a rate limit hit. Passing requests continue into the wrapped kernel untouched.
Pass-through responses are finished by the middleware too: the engine's security headers and CORS verdict headers are merged on top of the kernel's response, and the engine's behavioral return rules observe the response status code plus a body prefix bounded by behaviorMaxResponseBodyInspectBytes (only while behaviorScanResponseBody is on). Per-route configuration attaches through the middleware's route map or a custom resolver:
use RenzoFranceschini\GuardCore\Routing\RouteConfig;
use RenzoFranceschini\GuardCoreSymfony\GuardMiddleware;
return new GuardMiddleware(
$kernel,
new GuardEngine($config),
routes: [
'/docs/' => new RouteConfig(enableSuspiciousDetection: false),
'/api/' => new RouteConfig(rateLimit: 5, rateLimitWindow: 10),
],
// optional: country resolver for RouteConfig geoRateLimits tiers
geoRateLimitResolver: $myResolver,
// optional: custom resolver receiving the Symfony request
routeResolver: fn (Request $r) => str_starts_with($r->getPathInfo(), '/admin') ? new RouteConfig(bypassedChecks: ['rate_limit']) : null,
);routes patterns match a path exactly or as a prefix when they end with /. Geo rate-limit tiers need a country resolver: pass geoRateLimitResolver explicitly, or configure geoIpHandler together with blockedCountries/whitelistCountries (the engine keeps the injected handler only when country lists are set) and the middleware bridges it onto the engine's rate-limit handler automatically.
The adapter forwards an optional agentHandler to the engine (GuardEngine::setAgentHandler()), so security events, metrics and dashboard dynamic rules flow through guard-agent-php. Install it with composer require rennf93/guard-agent-php, create and start the agent where your App\Kernel is assembled, pass it as the agentHandler argument, and declare it in the config with enableAgent: true:
use RenzoFranceschini\GuardAgent\GuardAgent;
use RenzoFranceschini\GuardCore\Config\SecurityConfig;
use RenzoFranceschini\GuardCore\Engine\GuardEngine;
$agent = new GuardAgent([
'apiKey' => $_ENV['GUARD_API_KEY'],
'endpoint' => 'https://api.guard-core.com',
'projectId' => 'my-project',
]);
$agent->start();
$config = new SecurityConfig(
enableRedis: true,
enableAgent: true,
enableDynamicRules: true,
dynamicRuleInterval: 300,
enableEnrichment: true,
agentProjectId: 'my-project',
);
$kernel = new GuardMiddleware($kernel, new GuardEngine($config), agentHandler: $agent);
register_shutdown_function(static function () use ($agent): void {
$agent->flushBuffer();
$agent->stop();
});enableDynamicRules and enableEnrichment require enableAgent, and dynamic rules need an agent handler that exposes getDynamicRules() (GuardAgent does). The engine builds the dynamic rule manager itself and checks for new rules on the first request and then every dynamicRuleInterval seconds (default 300, minimum 60), because a PHP request process has no background loop. Without an agent handler the engine warns and stays on the static config. agentStats() on the adapter reports {enabled: false, degraded: false} without a handler and merges the handler's getStats() map with one. Symfony can ship the buffered telemetry from a kernel.terminate listener; the shutdown function above does the same.
SecurityConfig::customResponseModifier is a Closure(GuardResponse): GuardResponse that the engine applies once to every response it returns itself (blocks, redirects and the fail-closed response), after the security headers and before the CORS verdict headers. This adapter applies the same modifier to the pass-through responses it sends, through GuardEngine::applyResponseModifier(), including excluded-path responses and responses of requests that bypass all checks, and it never applies it twice to a response the engine already modified. The order on a pass-through response is security headers, modifier, then CORS verdict headers, so the CORS policy has the last word on its own headers.
use RenzoFranceschini\GuardCore\Request\GuardResponse;
$config = new SecurityConfig(
customResponseModifier: static function (GuardResponse $response): GuardResponse {
$response->headers()->set('X-Served-By', 'edge-1');
return $response;
},
);The modifier sees the status, the headers (Set-Cookie is not exposed) and the body, and the changes are merged back onto the Symfony Response; a streamed or file response keeps its body (only its status and headers can change). A modifier that throws leaves the response unmodified. Nothing is read from the response while no modifier is configured. The modifier seam needs guard-core-php 4.4.0 or later.
PHP shared-nothing applies: construct GuardEngine (and therefore GuardMiddleware) per request in classic FPM, or per worker under long-running runtimes (FrankenPHP, RoadRunner, workerman). The middleware holds no mutable state of its own. In-memory fallbacks are per-request safety nets; distributed rate limits, IP bans, and cloud-range caches require Redis (set enableRedis: true and point REDIS_HOST/REDIS_PORT at your instance).
- Fail-closed: 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. - Main request only: the middleware screens the main kernel request and passes sub-requests straight into the wrapped kernel. Sub-requests are internal and derived from a main request that was already screened; screening them again would double-count rate-limit hits (fragments, forwards).
- Bounded body read: the request body is scanned as a prefix of at most 256 KiB (
SymfonyGuardRequest::MAX_BODY_BYTES, matching the engine's full-scan window). The framework materializes the full body in memory; the engine only ever sees the capped prefix. Payloads beyond the prefix, or signatures split across its boundary, are not detected. - Client address: the adapter maps the engine's
clientHost()onto$request->getClientIp(). Without Symfony's trusted-proxies configuration that is the connectingREMOTE_ADDR, and the engine's owntrusted_proxies/X-Forwarded-Forresolution applies. With Symfony trusted proxies configured, Symfony resolves the forwarded chain first and the engine sees the resolved client. Pick one side to do the resolving; configuring both can double-hop. - With
redisFailOpen: truethe middleware constructs and serves requests even when Redis is unreachable; withredisFailOpen: falseconstruction fails closed. - Security headers and CORS: the adapter computes none of them. On a pass-through response it merges the engine's security header set, then the engine's
customResponseModifieroutput, then the engine's CORS verdict headers (CORS last, so the CORS policy wins on a shared header name). Blocked responses are exact translations of the engine's verdict.
composer lint
composer testcomposer test runs the plain-PHP suite in bin/test_symfony.php (unit coverage always; set REDIS_HOST to a reachable Redis to include the shared-state integration cases).
Install the latest release from Packagist. The engine floor is rennf93/guard-core-php ^4.4.0, which provides GuardEngine::applyResponseModifier() and the agent and dynamic rule validation described above.
MIT