laravel-audit / package
Extensible Laravel code audit package with Pint, PHPStan/Larastan, and purpose-built analyzers.
Requires
- php: ^8.2
- illuminate/console: ^10.0|^11.0|^12.0|^13.0
- illuminate/contracts: ^10.0|^11.0|^12.0|^13.0
- illuminate/database: ^10.0|^11.0|^12.0|^13.0
- illuminate/filesystem: ^10.0|^11.0|^12.0|^13.0
- illuminate/http: ^10.0|^11.0|^12.0|^13.0
- illuminate/queue: ^10.0|^11.0|^12.0|^13.0
- illuminate/routing: ^10.0|^11.0|^12.0|^13.0
- illuminate/support: ^10.0|^11.0|^12.0|^13.0
- illuminate/view: ^10.0|^11.0|^12.0|^13.0
- nikic/php-parser: ^5.0
- symfony/process: ^6.4|^7.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.0
- orchestra/testbench: ^8.0|^9.0|^10.0|^11.0
- phpunit/phpunit: ^10.5|^11.0|^12.0
This package is auto-updated.
Last update: 2026-06-30 00:19:24 UTC
README
Extensible Laravel audit package inspired by Laravel-focused tools such as ShieldCI. It combines Laravel-specific static analyzers with optional Pint and PHPStan/Larastan runners, pattern inference, optional LLM confirmation, and a web dashboard for browsing saved reports.
Requirements
- PHP 8.2+
- Laravel 10–13
- Optional:
laravel/pint,phpstan/phpstan,larastan/larastanin the host application - Optional: OpenAI-compatible LLM endpoint (LM Studio, Ollama, etc.) for pattern confirmation
- Web dashboard: no database required by default (reports are stored as JSON files)
Installation
composer require laravel-audit/package --dev
Publish configuration:
php artisan vendor:publish --tag=laravel-audit-config
Publish dashboard migrations only when using database report storage:
php artisan vendor:publish --tag=laravel-audit-migrations php artisan migrate
Quick start
CLI
# Full audit (analyzers + Pint + PHPStan when available) php artisan audit:analyze # Fast run without external tools php artisan audit:analyze --no-tools # Save report for the web panel php artisan audit:analyze --no-tools --store
Web panel
After installation the panel is available at /audit by default.
- Open
http://your-app.test/audit - Use Run Analysis to start an audit
- Watch progress on the run page
- Open the finished report from All Reports
For production, run a queue worker (see Dashboard runners).
Commands
audit:analyze
Main analysis command. Runs built-in analyzers and, unless disabled, Pint and PHPStan/Larastan.
php artisan audit:analyze [options]
| Option | Description |
|---|---|
--format=console |
Output format: console (default), json, or sarif |
--fail-on= |
Minimum severity for a non-zero exit code: info, warning, error, critical (default from config: error) |
--only= |
Comma-separated categories: security, performance, reliability, best-practices, code-quality |
--no-tools |
Skip Pint and PHPStan runners |
--patterns |
Enable heuristic refactoring pattern scoring |
--llm |
Confirm top heuristic hypotheses with an LLM using method source code |
--llm-pick= |
Confirm only selected hypothesis keys (repeatable; format pattern:file::method) |
--store |
Save the report for the web panel (when dashboard is enabled) |
Examples
php artisan audit:analyze --format=json php artisan audit:analyze --format=sarif --fail-on=warning php artisan audit:analyze --only=security,performance php artisan audit:analyze --no-tools --patterns php artisan audit:analyze --patterns --llm --format=json php artisan audit:analyze --patterns --llm-pick=action:app/Http/Controllers/UserController.php::store --format=json php artisan audit:analyze --no-tools --store
Exit code is 0 on success and 1 when at least one issue meets the --fail-on threshold.
audit:run-stored
Internal command used by the web dashboard to execute a queued run. You can also run it manually:
php artisan audit:run-stored {uuid}
{uuid} is the run id from /audit/runs/{uuid} or from storage/app/laravel-audit/runs/{uuid}.json.
Web dashboard
Routes
| Method | Path | Description |
|---|---|---|
| GET | /audit |
Overview and recent reports |
| GET | /audit/reports |
All saved reports |
| GET | /audit/reports/create |
Start a new analysis |
| POST | /audit/reports |
Queue/create a run and redirect to progress |
| GET | /audit/reports/{uuid} |
View a saved report |
| POST | /audit/reports/{uuid}/confirm-patterns |
Confirm selected heuristic hypotheses with LLM (sync) |
| GET | /audit/runs/{uuid} |
Progress page with live status |
| GET | /audit/runs/{uuid}/status |
JSON progress payload (polling) |
| POST | /audit/runs/{uuid}/kick |
Re-dispatch background runner |
| POST | /audit/runs/{uuid}/execute |
Run synchronously in the HTTP request (blocks UI; fallback) |
Default path prefix is audit. Configure with LARAVEL_AUDIT_DASHBOARD_PATH.
Dashboard runners
Panel audits run asynchronously so the UI stays responsive.
| Runner | Config value | How it works | When to use |
|---|---|---|---|
| Queue (default) | queue |
Dispatches RunStoredAuditJob |
Production, local dev with queue:work |
| Process | process |
Detached nohup php artisan audit:run-stored |
No queue infrastructure; requires exec |
Queue runner (recommended)
LARAVEL_AUDIT_DASHBOARD_RUNNER=queue QUEUE_CONNECTION=database # or redis, sqs, etc.
# One-time setup for database driver php artisan queue:table php artisan migrate # Keep a worker running. For LLM audits, set worker timeout above the job timeout. php artisan queue:work --timeout=1800
With QUEUE_CONNECTION=sync, jobs run inline during the HTTP request (fine for tests, but blocks the create request).
Process runner (fallback)
LARAVEL_AUDIT_DASHBOARD_RUNNER=process
Logs are written to storage/logs/laravel-audit-{uuid}.log.
Report storage
| Driver | Config value | Location |
|---|---|---|
| File (default) | file |
storage/app/laravel-audit/reports/{uuid}.json |
| Database | database |
audit_reports table |
No database is required for the default file storage.
Protecting the panel
By default the panel uses the web middleware group only. In production, add authentication in config/laravel-audit.php:
'dashboard' => [ 'middleware' => ['web', 'auth'], ],
Configuration
All options live in config/laravel-audit.php after publishing.
Scan paths
'paths' => ['app', 'routes', 'config', 'database/migrations', 'tests'], 'exclude' => ['vendor', 'storage', 'bootstrap/cache', 'node_modules'],
Tools (Pint & PHPStan)
| Key | Env | Default | Description |
|---|---|---|---|
tools.pint.enabled |
— | true |
Run Pint in --test mode |
tools.pint.binary |
LARAVEL_AUDIT_PINT_BINARY |
vendor/bin/pint |
Pint executable |
tools.phpstan.enabled |
— | true |
Run PHPStan |
tools.phpstan.binary |
LARAVEL_AUDIT_PHPSTAN_BINARY |
vendor/bin/phpstan |
PHPStan executable |
tools.phpstan.auto_larastan |
LARAVEL_AUDIT_PHPSTAN_AUTO_LARASTAN |
true |
Auto-generate Larastan config when no phpstan.neon exists |
tools.phpstan.level |
LARAVEL_AUDIT_PHPSTAN_LEVEL |
10 |
PHPStan level for auto Larastan config (max by default) |
When Larastan is installed, the PHPStan runner generates storage/framework/cache/laravel-audit-phpstan/audit-phpstan.neon with writable cache paths and your scan paths.
Fatal bootstrap failures (missing traits, autoload errors, worker crashes) appear in the report as tooling.phpstan.runner issues parsed from PHPStan's top-level JSON errors[].
After updating this package, restart queue workers so they load the new runner code:
php artisan queue:restart
The queue worker timeout must exceed the audit job timeout. For example, if dashboard.job_timeout is 180, run queue:work with --timeout=300 or higher.
Reporting
| Key | Default | Description |
|---|---|---|
reporting.default_format |
console |
Default CLI output format |
reporting.fail_on |
error |
Default --fail-on threshold |
Dashboard
| Key | Env | Default | Description |
|---|---|---|---|
dashboard.enabled |
LARAVEL_AUDIT_DASHBOARD |
true |
Enable routes and views |
dashboard.path |
LARAVEL_AUDIT_DASHBOARD_PATH |
audit |
URL prefix |
dashboard.middleware |
— | ['web'] |
Route middleware |
dashboard.storage |
LARAVEL_AUDIT_DASHBOARD_STORAGE |
file |
file or database |
dashboard.storage_path |
LARAVEL_AUDIT_DASHBOARD_STORAGE_PATH |
— | Custom reports directory |
dashboard.runs_path |
LARAVEL_AUDIT_DASHBOARD_RUNS_PATH |
— | Custom in-progress runs directory |
dashboard.runner |
LARAVEL_AUDIT_DASHBOARD_RUNNER |
queue |
queue or process |
dashboard.queue_connection |
LARAVEL_AUDIT_DASHBOARD_QUEUE_CONNECTION |
— | Queue connection (default: app default) |
dashboard.queue |
LARAVEL_AUDIT_DASHBOARD_QUEUE |
default |
Queue name |
dashboard.job_timeout |
LARAVEL_AUDIT_DASHBOARD_JOB_TIMEOUT |
180 |
Base queue job timeout (seconds) |
dashboard.llm_job_timeout |
LARAVEL_AUDIT_DASHBOARD_LLM_JOB_TIMEOUT |
— | Optional fixed timeout for LLM audits |
Default storage paths:
- Reports:
storage/app/laravel-audit/reports/ - Runs:
storage/app/laravel-audit/runs/
Thresholds
| Key | Default | Description |
|---|---|---|
thresholds.nesting_depth |
4 |
Max nesting depth before code-quality.nesting-depth fires |
Pattern inference
| Key | Env | Default | Description |
|---|---|---|---|
patterns.enabled |
LARAVEL_AUDIT_PATTERNS |
false |
Enable heuristic patterns from CLI/config (without --patterns flag) |
patterns.min_confidence |
LARAVEL_AUDIT_PATTERN_MIN_CONFIDENCE |
0.55 |
Minimum score to include a suggestion |
patterns.limit |
LARAVEL_AUDIT_PATTERN_LIMIT |
20 |
Max heuristic suggestions |
patterns.model_path |
LARAVEL_AUDIT_PATTERN_MODEL |
package pattern-model.json |
Custom model file |
patterns.llm.enabled |
LARAVEL_AUDIT_PATTERN_LLM |
false |
Enable LLM from config |
patterns.llm.provider |
LARAVEL_AUDIT_PATTERN_LLM_PROVIDER |
openai_compatible |
LLM provider |
patterns.llm.endpoint |
LARAVEL_AUDIT_PATTERN_LLM_ENDPOINT |
http://127.0.0.1:1234/v1/chat/completions |
Chat completions URL |
patterns.llm.model |
LARAVEL_AUDIT_PATTERN_LLM_MODEL |
local-model |
Model name |
patterns.llm.api_key |
LARAVEL_AUDIT_PATTERN_LLM_API_KEY |
— | API key (optional for local servers) |
patterns.llm.timeout |
LARAVEL_AUDIT_PATTERN_LLM_TIMEOUT |
120 |
HTTP timeout (seconds) |
patterns.llm.max_attempts |
LARAVEL_AUDIT_PATTERN_LLM_MAX_ATTEMPTS |
auto | Max LLM HTTP calls per audit |
patterns.llm.review_limit |
LARAVEL_AUDIT_PATTERN_LLM_REVIEW_LIMIT |
3 |
Max methods sent to LLM |
patterns.llm.concurrency |
LARAVEL_AUDIT_PATTERN_LLM_CONCURRENCY |
1 |
Parallel LLM HTTP requests per batch |
patterns.llm.refine_top |
LARAVEL_AUDIT_PATTERN_LLM_REFINE_TOP |
3 |
Legacy alias for review limit |
Rules
Each analyzer can be toggled in rules:
'rules' => [ 'security.raw-sql' => true, // ... ],
Set a rule to false to disable it without removing the analyzer from the package.
Environment examples
Minimal (CLI + panel, no LLM)
LARAVEL_AUDIT_DASHBOARD=true LARAVEL_AUDIT_DASHBOARD_PATH=audit LARAVEL_AUDIT_DASHBOARD_RUNNER=queue QUEUE_CONNECTION=database LARAVEL_AUDIT_PATTERNS=false LARAVEL_AUDIT_PATTERN_LLM=false
Full (patterns + local LLM + queue)
LARAVEL_AUDIT_DASHBOARD=true LARAVEL_AUDIT_DASHBOARD_RUNNER=queue QUEUE_CONNECTION=database LARAVEL_AUDIT_PATTERNS=true LARAVEL_AUDIT_PATTERN_LLM=true LARAVEL_AUDIT_PATTERN_LLM_ENDPOINT=http://127.0.0.1:1234/v1/chat/completions LARAVEL_AUDIT_PATTERN_LLM_MODEL=google/gemma-4-e4b
No queue, no database (process runner + file storage)
LARAVEL_AUDIT_DASHBOARD=true LARAVEL_AUDIT_DASHBOARD_RUNNER=process LARAVEL_AUDIT_DASHBOARD_STORAGE=file
Built-in analyzers
Security
| Rule ID | Detects |
|---|---|
security.raw-sql |
Raw SQL / DB::raw usage |
security.mass-assignment |
Missing $fillable/$guarded, empty $guarded, or $guarded-only models |
security.weak-validation |
Weak inline validation |
security.debug-configuration |
Debug-friendly defaults in config |
security.command-injection |
Shell/command injection risks |
security.eval-usage |
eval() usage |
security.hardcoded-credentials |
Hardcoded secrets |
security.unguarded-model |
Model::unguard() usage |
security.unvalidated-mass-create |
Model::create($request->all()) without validation |
security.sensitive-field-exposure |
Raw model/Inertia/JSON responses exposing unhidden sensitive columns |
Performance
| Rule ID | Detects |
|---|---|
performance.n-plus-one-candidate |
Possible N+1 query patterns |
performance.sync-heavy-job |
Heavy work in non-queue jobs |
Reliability
| Rule ID | Detects |
|---|---|
reliability.missing-transaction |
Multi-step writes without transactions |
reliability.env-access-outside-config |
env() outside config files |
reliability.global-variables |
Superglobal access |
Best practices
| Rule ID | Detects |
|---|---|
best-practices.missing-form-request |
Inline validation instead of Form Requests |
best-practices.shared-form-request |
Same Form Request reused across multiple controller actions |
best-practices.fat-controller |
Oversized controllers |
best-practices.logic-in-routes |
Business logic in route closures |
best-practices.silent-failure |
Empty catch blocks / swallowed errors |
best-practices.debug-statement |
dd(), dump(), and similar debug helpers in app code |
Code quality
| Rule ID | Detects |
|---|---|
code-quality.long-method |
Methods exceeding length threshold |
code-quality.large-class |
Classes with too many lines |
code-quality.nesting-depth |
Deep nesting |
code-quality.redundant-boolean-return |
Redundant boolean return patterns |
code-quality.redundant-null-coalesce |
Useless null coalesce |
code-quality.redundant-empty-foreach-guard |
Empty check before foreach |
code-quality.redundant-catch-rethrow |
Catch/rethrow without value |
code-quality.redundant-else-after-exit |
Else after return/throw |
code-quality.redundant-type-guard |
Redundant type/is/instance checks |
code-quality.redundant-method-exists |
method_exists() on typed dependencies |
code-quality.redundant-class-exists |
class_exists() for app classes |
code-quality.redundant-config-fallback |
config('key') ?? $default instead of config('key', $default) |
Tooling
When not using --no-tools, findings from Pint and PHPStan/Larastan are merged into the report.
Pattern inference
Two independent layers:
1. Heuristic model (--patterns or patterns.enabled)
Extracts AST features per method and scores them against resources/pattern-model.json (logistic-style weighted model).
Known patterns:
strategyaction(Action / Use Case)repositoryvalue_objectguard_clausesextract_methoddependency_injectionform_requestpolicypipelineenumapi_resource
Output uses source: heuristic. Existing audit findings can boost pattern scores (finding_boosts in the model file).
2. LLM confirmation (--llm or patterns.llm.enabled)
Does not pick patterns from scratch. For each heuristic hypothesis it sends:
- pattern slug and title
- structural facts (lines, branches, etc.)
- method source code
The model must confirm or reject the hypothesis with code evidence. Confirmed items use source: confirmed; rejected ones use source: refuted with the LLM rationale.
Automatic (top N): with --llm and no explicit keys, the advisor picks the strongest hypotheses per method, up to patterns.llm.review_limit.
Manual selection:
| Flow | How |
|---|---|
| CLI | Repeat --llm-pick=pattern:path/to/file.php::methodName (hypothesis key shown on each heuristic row in JSON/console) |
| Panel | Run with patterns only (leave LLM unchecked on create). On the report page, check hypotheses and click Confirm selected with LLM |
Hypothesis key format: {pattern}:{relative-file}::{method} — e.g. action:app/Http/Controllers/UserController.php::store.
php artisan audit:analyze --patterns # heuristic only php artisan audit:analyze --llm # LLM only (needs config/flag for heuristics to produce hypotheses) php artisan audit:analyze --patterns --llm # both layers (auto top-N) php artisan audit:analyze --patterns --llm-pick=repository:app/Repositories/UserRepository.php::findForUpdate
Compatible with OpenAI-compatible APIs (LM Studio, Ollama, etc.).
Report formats
Console (default)
Human-readable summary grouped by severity, suitable for local development and CI logs.
JSON
php artisan audit:analyze --format=json
Structured payload with summary, issues, tools, patternSuggestions, and durationSeconds. Suitable for scripting and dashboards.
SARIF
php artisan audit:analyze --format=sarif
SARIF 2.1.0 for GitHub Code Scanning and compatible tools.
CI integration
- name: Run Laravel Audit run: php artisan audit:analyze --no-tools --fail-on=error
With Pint and PHPStan in CI:
- name: Run Laravel Audit run: php artisan audit:analyze --fail-on=error --format=sarif > audit.sarif
Store reports from CI into the panel:
- name: Run and store audit run: php artisan audit:analyze --no-tools --store
Ensure the CI environment has write access to storage/app/laravel-audit/ when using file storage.
Extending the package
Adding analyzers
- Implement
LaravelAudit\Analysis\AnalyzerInterface - Return normalized
LaravelAudit\Analysis\Issueobjects - Register the analyzer in
AuditServiceProvideror a consuming app service provider
use LaravelAudit\Analysis\AnalyzerInterface; use LaravelAudit\Analysis\AnalysisContext; use LaravelAudit\Analysis\Category; final class CustomAnalyzer implements AnalyzerInterface { public function id(): string { return 'best-practices.custom-rule'; } public function category(): Category { return Category::BestPractices; } public function analyze(AnalysisContext $context): array { return []; } }
Add a toggle under rules in config and register the class in AuditServiceProvider.
Analyzers should report evidence and recommendations. If a rule cannot prove a defect statically, phrase the issue as a candidate or risk.
Custom pattern model
Publish or point to your own JSON model:
LARAVEL_AUDIT_PATTERN_MODEL=/path/to/custom-pattern-model.json
Troubleshooting
Panel audit stuck on QUEUED
- Queue runner: ensure
php artisan queue:workis running - Check failed jobs:
php artisan queue:failed - Verify
QUEUE_CONNECTIONis notsyncif you want a non-blocking UI - Use Retry start on the progress page or run manually:
php artisan audit:run-stored {uuid}
Panel blocks the whole site during audit
- You are likely using
--execute/ foreground fallback orQUEUE_CONNECTION=sync - Switch to
LARAVEL_AUDIT_DASHBOARD_RUNNER=queuewith a real queue driver and a worker
Process runner does nothing
- Check
function_exists('exec')is true - Inspect
storage/logs/laravel-audit-{uuid}.log - Switch to queue runner for production
LLM audit times out in queue
RunStoredAuditJobnow sets a longer timeout automatically when LLM is enabled- The queue worker timeout must still be high enough:
php artisan queue:work --timeout=1800 - Or set a fixed ceiling with
LARAVEL_AUDIT_DASHBOARD_LLM_JOB_TIMEOUT=3600 - Reduce load with
LARAVEL_AUDIT_PATTERN_LLM_REVIEW_LIMITorLARAVEL_AUDIT_PATTERN_LLM_MAX_ATTEMPTS
LLM returns empty or invalid JSON
- LM Studio: use an OpenAI-compatible endpoint; some models need
/no_thinkor lowerreview_limit - Increase
LARAVEL_AUDIT_PATTERN_LLM_TIMEOUTfor slow local models - Test with
--patterns --llm --format=jsonbefore using the panel
Permission denied writing storage/app/laravel-audit/reports/*.json
- The web request that confirms patterns must be able to write the same directory as the queue worker / CLI that created the report
- Fix ownership and permissions (adjust user/group for your stack):
sudo mkdir -p storage/app/laravel-audit/reports storage/app/laravel-audit/runs sudo chown -R www-data:www-data storage/app/laravel-audit sudo chmod -R ug+rwx storage/app/laravel-audit
- In Docker: run
chowninside the PHP-FPM and queue-worker containers, or mountstoragewith a shared UID - Alternative: switch panel storage to database with
LARAVEL_AUDIT_DASHBOARD_STORAGE=database
LM Studio log: 'messages' field is required with body {}
- Usually means the HTTP client failed to encode the request JSON (invalid UTF-8 in method source) and sent an empty payload
- Recent package versions throw instead of posting
{}; update the package if you still see{}requests - A successful log entry followed by this error can also be a second hypothesis or an unrelated probe to the same endpoint
PHPStan/Larastan not found
- Install dev dependencies in the host app:
composer require --dev larastan/larastan phpstan/phpstan - Or use
--no-toolsto run built-in analyzers only
PHPStan shows non-JSON response or Could not write file: /tmp/phpstan/resultCache.php
- The audit job is probably running on an old queue worker process. Run
php artisan queue:restartaftercomposer update - Ensure
storage/framework/cacheis writable by the queue worker user (www-datain Docker) - Increase the queue worker
--timeoutabovedashboard.job_timeout(for examplequeue:work --timeout=300)
License
MIT