Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Symfony Security Auditor Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

This is an AI-driven security auditor: it indexes your repository, scrubs secrets and risky patterns, then runs attacker/reviewer model passes over route and controller code. It ships two ways — as a Symfony bundle (bin/console mcp:serve, bin/console audit:run) and as a standalone binary. From a Laravel codebase, use the standalone binary. Do not composer require it into the app; bundle mode is Symfony-kernel oriented, while the binary just needs a path to the repo.

Minimal path:

# 1. Point it at a provider. An OpenAI-compatible gateway is the least friction:
symfony-security-auditor init --provider=generic.internal --base-url=https://ai.internal/v1

# 2. Give it a credential (hidden prompt, or store it once):
symfony-security-auditor auth:set --env-var=AI_GATEWAY_KEY
symfony-security-auditor auth:status    # tells you which key an audit would use, and where it came from

# 3. Verify wiring before spending tokens:
symfony-security-auditor doctor

# 4. First real run, on a legacy app, without breaking CI:
symfony-security-auditor audit:run --generate-baseline

Where to look first: doctor (it names the key that resolved and points at init --provider=<platform> when a bridge is missing), init --help (defaults are now documented), then audit:run --help.

Key resolution order is: exported environment variable > stored key > %env(file:VAR)% file reference. A run with nothing available now opens with No API key available. and names the ways out rather than a stack trace.

The first genuinely valuable use case on a Laravel app is not the LLM pass — it's a baseline run of the secret scrubber plus the LACKS_ACCESS_CHECK-class static findings, committed as a baseline so the team only ever sees new problems.

Implementation Patterns

CI gate with a baseline. Generate once, commit it, then:

symfony-security-auditor audit:run \
  --baseline=.symfony-security-auditor.baseline \
  --fail-on-incomplete

--fail-on-incomplete exits 3 when any file could not be fully analyzed (including when combined with --generate-baseline). Without it you only get a warning — Audit incomplete: N file(s) could not be fully analyzed — on stderr when the report goes to stdout. Treat 3 as "fix the parser coverage or exclude the path", not as "findings".

GitHub Action. Use the fail-on-incomplete input and the new complete output to distinguish "clean" from "clean but blind". Save reports with --output and parse that file; the defusing of ::… / ##[ on runners applies to stdout-bound output only, and the github format plus --output reports are byte-identical to before.

Two-tier configuration. This is the main architectural concept in 1.21:

User config (~) Project .symfony-security-auditor.yaml (committed)
Model ids full ids with options (?temperature=…) plain ids only
cache, privacy, audit.custom_skills, scan.secret_scrubbing, scan.custom_risk_patterns allowed refused
audit.budget set may tighten, may not loosen
%env()%, %name%, %% allowed refused
Section shape any maps only

So: keep tuning, prompt overrides, custom risk patterns and budget ceilings in the user config; commit only the shared, boring model/scope decisions. The audit header now names the layered project file, which makes "why did it behave differently on the runner" answerable.

Provider indirection. provider: <platform>.<instance> selects a named instance of generic, openresponses, azure, bedrock, cache or failover. For Laravel teams the useful shapes are:

provider: generic.gateway          # one OpenAI-compatible endpoint, any vendor behind it
# or
provider: generic.gateway
model: openrouter/auto             # gateway routes; billing follows the answered model

and for air-gapped or cost-sensitive repos, init --provider=ollama now writes a keyless config with a required endpoint instead of one the next run cannot boot.

Bedrock without SDK plumbing. init configures Bedrock through Mantle routes (anthropic.* → messages, google.gemma-* → responses, everything else → completions), reads BEDROCK_API_KEY, and installs the bridge the route needs. --no-api-key switches to SigV4 signing, which is the right choice on an instance role.

Credentials as files, not env. %env(file:VAR)% reads the credential from the file VAR names — designed for Docker/Kubernetes secrets, systemd LoadCredential= and plain 0600 files. Combined with the owner-only credentials.json sitting beside the config file, this is the pattern to standardise on; an exposed (group/world-readable) credentials file is refused rather than used.

MCP for agents. symfony-security-auditor mcp:serve now exists in the standalone binary, so you can expose the auditor as a tool to an IDE/agent without a Symfony console. This is the cheapest way to get "audit this diff" in-editor.

Cost tracking. by_model still reports the configured model; the new additive billed_models key reports what the provider says actually answered. If you have a cost dashboard, read billed_models — with openrouter/auto, a failover platform, or an alias resolved to a dated release, by_model alone silently under-reports.

Gotchas and Tips

  • The project-config hardening is the breaking change. Existing committed .symfony-security-auditor.yaml files that used %env()%, %name%, %-escapes, cache, privacy, audit.custom_skills, scan.secret_scrubbing, scan.custom_risk_patterns, or loosened audit.budget will now stop the run naming the offending key. Move those keys to the user config. Budget a migration commit before bumping the version in CI.
  • You cannot read the environment from a project config, by design. Write the literal value or move the setting up a layer. A project config that is a symlink is refused before it is read, and one expanding to ≥10,000 values is refused.
  • Model options in the project config are rejected. model, attacker_model, reviewer_model and audit.escalation.cheap_model must be plain ids there; ?temperature=… belongs in the user config. Values or keys containing control characters, :: or ##[ are refused — the error names the key but deliberately does not echo the value, so don't expect to see the bad string in the log.
  • Exit codes carry meaning. 2 is a configuration/init refusal (bare or mistyped instance, empty base URL, a model on Bedrock that doesn't name its vendor) — the message lists the configured instances. 3 is an incomplete audit. Don't fold them into a single "failed" bucket in your pipeline.
  • Parse --output, not stdout. On a GitHub Actions runner, reports, errors, progress lines, --show-scanned and audit:diff are defused so a runner can't read them as workflow commands. Only the github format and --output files are unchanged.
  • access_control coverage is now honest — and narrower. A route covered only by a rule the production application never loads (when@test, a non-production config folder, a file nothing imports) is LACKS_ACCESS_CHECK again, and the attacker prompt labels that coverage as "read from the repository's YAML, not verified". For Laravel this matters differently: access_control is a Symfony concept, so route/middleware enforcement will read as uncovered. Scope the scan (or baseline those findings) rather than chasing them.
  • Secret scrubber coverage is still suffix-heuristic. It now catches separator-less keys (SECRETKEY=, APIKEY=, DBPASS=, PGPASSWORD=) and PASS/PW suffixes (DB_PASS=, MYSQL_ROOT_PW=, pass: '…'), while deliberately leaving MONKEY, MAX_PW_LENGTH and BYPASS_CACHE alone. Unusual Laravel env names will still slip through — add scan.custom_risk_patterns in the user config.
  • init is honest about what it can't do. For azure, cartesia, higgsfield, dockermodelrunner, lmstudio, transformersphp it installs the bridge, prints a ready-to-edit YAML block and exits 2 without writing; cache and failover are refused with a reason. Don't wrap init in a script that assumes success on exit 0 alone.
  • Re-run init for Ollama. Configs written by older versions are keyless-less; the new writer emits a keyless config with a required endpoint. Old files may simply not boot.
  • --base-url % handling is strict. An empty URL exits 2; a URL where every % begins a percent-encoded octet is written verbatim; any other % is refused. If you template URLs in a wrapper script, encode them first.
  • Stored keys are write-only in practice. The audit header, auth:status and doctor show only a masked preview plus a fingerprint — if you lose the key, auth:set replaces it, you don't recover it. And init no longer prints a paste-ready export line, precisely so it doesn't land in shell history.
  • Debug order: doctor → auth:status → audit:run --show-scanned (remember it's defused on runners) → only then the model/provider. Most "it broke" reports are key resolution or provider-instance typos, both of which doctor names outright.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package