vinceamstoutz/symfony-security-auditor
A Symfony security auditor that scans your app's security configuration and flags misconfigurations, weak settings, and risky access rules. Get actionable reports to harden firewalls, authentication, and authorization before deployment.
What this is. symfony-security-auditor is a PHP security-audit tool distributed three ways: a Symfony bundle (bin/console), a standalone binary, and a GitHub Action, now also exposing an MCP server (mcp:serve). It performs AI-assisted static analysis of application security posture — routes, access control coverage, secrets, custom risk patterns — by driving LLM providers.
Architecture fit. The critical finding for us: this is not a Laravel package. There is no service provider, no publishable config, no Blade/queue/Octane integration, and the analysis model is explicitly Symfony-shaped (YAML routing, security.yaml, access_control, when@test config folders). The realistic Laravel integration surface is the CLI contract — exit codes, stdout/stderr separation, github output format, --output artifacts — plus the MCP server for developer-tooling and agent workflows. Treat it as a CI security scanner that happens to be written in PHP, not as an application dependency.
What 1.21.0 actually changes (architecture-relevant):
--fail-on-incomplete → exit code 3 when any file is not fully analyzed (including with --generate-baseline); warning to stderr when the report goes to stdout; GitHub Action gains fail-on-incomplete input and complete output. This is the single most valuable change for us: it converts "green build" into "clean vs. couldn't-tell," which is the difference between a scanner and a security control.provider: <platform>.<instance> with instances for generic, openresponses, azure, bedrock, cache, failover; generic.<instance> for any OpenAI-compatible gateway behind a custom URL/token; init --base-url/--endpoint/--no-api-key; Bedrock via Mantle routes with SigV4 signing; new symfony/ai 0.14 platforms (Fireworks, Together, Venice, Eden AI, TypeSafe, Higgsfield). Translation: we can point it at our approved enterprise gateway instead of a public vendor — that is usually the unlock for security-team sign-off.credentials.json, auth:set/status/remove, %env(file:VAR)% for Docker/K8s secrets and systemd LoadCredential=, env var still wins, masked preview + fingerprint only. Also removed the paste-ready export line from init (shell-history leak). This is genuine production readiness, not a nicety.billed_models alongside by_model. Without this, every cost report behind an openrouter/auto-style gateway was quietly wrong..symfony-security-auditor.yaml) may no longer set cache, privacy, audit.custom_skills, scan.secret_scrubbing, scan.custom_risk_patterns, may not loosen audit.budget, may not use %env()%/%name%/%%, may not be a symlink, models must be plain IDs, control chars / :: / ##[ refused, 10k-value expansion cap. Sound design (a repo shouldn't be able to weaken or read its own audit) — but see risk below.access_control rules the production app never loads are LACKS_ACCESS_CHECK again, with the prompt now labeling coverage as repo-YAML-read rather than verified; secret scrubber now catches SECRETKEY=, APIKEY=, DBPASS=, PGPASSWORD=, PASS/PW suffixes while leaving MONKEY/MAX_PW_LENGTH/BYPASS_CACHE alone; GitHub Actions logs defuse :: and ##[ workflow-command injection.Technical risks:
%env()% references, symlinked project config refused, model options (?temperature=…) no longer accepted in project config. Anyone on ^1.21 gets this automatically. Arguably 2.0.0 material.3 alters the CI contract; any wrapper or dashboard keyed on 1/0 needs updating, and 3 must not be conflated with "vulnerabilities found."symfony/* constraints can collide with the symfony/* transitive versions Illuminate already pulls into a Laravel app. Verify in a scratch branch; prefer the standalone binary/PHAR for Laravel to sidestep this entirely.init downloads bridges at init time — network egress and artifact provenance in locked-down CI.Key questions for the maintainer:
--output stable/versioned?--fail-on-incomplete intended as the default recommendation for gating, or opt-in only?cache platform supported for that?audit.budget enforceable org-wide from user config, and what happens on breach — stop, degrade, or continue?Stack fit. For a Laravel estate, integrate at the edge, not into the app:
github output or --output artifacts. Zero runtime coupling to Laravel.mcp:serve from the standalone binary, wired into the IDE/agent tooling, read-only.require-dev in Laravel apps until a Composer resolution test proves no symfony/* conflict.Migration path. There is no prior assessment, so this is net-new adoption; sequence for low blast radius:
--fail-on-incomplete / fail-on-incomplete: true and consume the complete output. Gate on new findings over baseline only.auth:set on developer machines; %env(file:VAR)% for Docker/K8s; never credentials.json on shared runners. Confirm env-var precedence behaves as documented in our runners.Compatibility work before any upgrade or first pin:
1.21.0 exact, not ^1.21..symfony-security-auditor.yaml and move cache, privacy, audit.custom_skills, scan.secret_scrubbing, scan.custom_risk_patterns, any audit.budget loosening, any %env()%/%name%/%%, and any model options into the shared user/org config. Anything left in project config is now a hard stop.::/##[ defusing is GitHub Actions-specific; if we run it on GitLab/Jenkins, sanitization of report text is on us.init can't pull an unapproved bridge.Sequencing recommendation: pin and lock → config migration → CI contract (exit 3 + complete) → credential story → MCP. Do not enable gating before step 3 completes; a scanner that fails the build on "couldn't analyze" without baseline discipline will be disabled by the first annoyed team.
Maintenance. High cadence and a single-author change stream: budget a monthly upgrade review with a named owner (~0.2 FTE platform/security engineer). Treat every minor release as potentially breaking and read the notes — the project-config hardening in this very release proves the version number is not a reliable risk signal. Pin, upgrade deliberately, never float.
Support. Community OSS, no vendor SLA. Internal support model: one owner for upgrades and config, plus a triage rota for findings. False positives need an explicit path — use baselines (--generate-baseline) rather than a repo-level suppression key, since the suppressive keys are now deliberately disallowed in project config. That's the right control, but it means suppression is a user/org-config action with a review trail, which must be documented or it becomes shadow IT.
Scaling. Two axes:
audit.budget centrally in user/org config (project config can no longer loosen it — a genuine win for chargeback discipline). Track spend via billed_models now that gateway routing is billed correctly, and reconcile by_model vs billed_models to catch routing surprises.Audit incomplete on monoliths. The complete output is the scaling guardrail — publish it to a dashboard so "incomplete" is visible as a distinct state, never silently green.Failure modes to instrument:
| Mode | Signal | Required handling |
|---|---|---|
| No/invalid API key | No API key available. at run open |
Treat as infra failure, not a clean pass; alert |
| Partial analysis | exit 3, Audit incomplete: N file(s) |
Distinct CI state; alert on trend, not just failure |
| Provider failover / alias | billed_models differs from by_model |
Cost anomaly review |
| Project config rejected | hard stop naming the key | One repo can break the whole pipeline — config changes need review ownership |
| Log injection | ::…, ##[ in report text |
Covered on GHA only; sanitize ourselves elsewhere |
| Credential exposure | masked preview + fingerprint | Ban credentials.json on shared runners; prefer %env(file:VAR)% |
Ramp-up. Low to run (one CLI call), moderate to interpret (someone must triage security findings in PHP/Laravel semantics), higher to own (credentials, config policy, provider/bridge approvals, cost). Plan for a half-day enablement for engineers and a short brief for stakeholders explaining that on Laravel the tool's route/access-control coverage is structurally thin — otherwise the pilot will read as a weak product rather than a scoped one.
Recommendation. Adopt in a time-boxed spike as a CI-adjacent control, not as a Laravel dependency: standalone binary, exact version pin, advisory first, gating only on new findings over baseline, credentials via %env(file:VAR)%, MCP last. Worth adopting for the secret-scanning/CI-gating and enterprise-gateway flexibility; explicitly not worth adopting as a Symfony-equivalent route/access-control auditor for Laravel. The 1.21.0 exit-code and credential work makes it materially more operable than its version number implies — and its config hardening makes it materially more likely to break on upgrade than its version number implies. Both facts argue for the same discipline: pin, read notes, gate on baselines.
How can I help you explore Laravel packages today?