Security audit tool for Laravel. Scans your application's filesystem, PHP configuration, HTTP headers, and Composer dependencies for common vulnerabilities and misconfigurations. Ships with 15 checks out of the box, a scoring system, and an optional Filament v5 dashboard.
- 15 built-in checks grouped into 5 categories (filesystem, configuration, HTTP headers, dependencies, extended)
- Filesystem scanning for malicious JS, dangerous uploads, wrong permissions, and exposed sensitive files
- Configuration audits covering PHP.ini,
.env, session, and CORS settings - HTTP header validation (HSTS, CSP, X-Frame-Options, etc.)
- Composer audit integration to flag vulnerable packages
- Hardcoded secret detection with smart
env()filtering to reduce false positives - Security score from 0 to 100 based on check results
- File integrity monitoring via SHA-256 baseline comparison
- Filament v5 dashboard (optional) with scan history, score widgets, and one-click scanning
- JSON and table output, CI/CD-friendly exit codes
- Extensible with custom checks via a simple interface
- PHP 8.2 enums for status, severity, and category values
We kept running into the same issues across projects: .env files accessible via HTTP, debug mode left on in production, API keys committed to repos, malicious files sneaking into upload directories. Vigil automates the checks we were doing manually.
A few things we focused on:
- Low noise. Each check is tuned to avoid false positives. The secret scanner, for example, understands
env()calls and won't flag them. - Useful output. Failed checks include the exact file, line number, and a concrete fix suggestion.
- Reasonable performance. File scanning respects size limits so it won't choke on large codebases.
- PHP 8.2+
- Laravel 11.x or 12.x
- Filament 5.x (optional, for the dashboard)
composer require filastudio/laravel-vigilThe service provider registers automatically.
Publish the config:
php artisan vendor:publish --tag=vigil-configIf you want scan history stored in the database:
php artisan vendor:publish --tag=vigil-migrations
php artisan migrateThis creates vigil_scans and vigil_check_results tables. Old records are cleaned up automatically based on the retention setting in config.
Run your first scan:
php artisan vigil:auditThe configuration file config/vigil.php allows you to enable/disable individual checks and customize behavior:
return [
// Enable or disable individual checks
'checks' => [
'fs.public_folder' => true,
'fs.malicious_js' => true,
'fs.storage_dangerous' => true,
'fs.permissions' => true,
'fs.sensitive_exposure' => true,
'cfg.php_ini' => true,
'cfg.env' => true,
'cfg.session' => true,
'cfg.cors' => true,
'http.headers' => true,
'dep.composer_audit' => true,
'ext.hardcoded_secrets' => true,
'ext.debug_routes' => true,
'ext.telescope_debugbar' => true,
'ext.file_integrity' => false, // Requires baseline
],
// Allowed file extensions in public directory
'public_allowed_extensions' => [
'css', 'js', 'jpg', 'jpeg', 'png', 'gif', 'svg', 'webp',
'ico', 'woff', 'woff2', 'ttf', 'eot', 'pdf', 'map', 'txt',
],
// Dangerous file extensions in storage
'storage_dangerous_extensions' => [
'php', 'phtml', 'phar', 'php3', 'php4', 'php5', 'php7',
'exe', 'bat', 'cmd', 'sh', 'bash', 'bin', 'js', 'vbs', 'ps1',
],
// Store scan results in database
'store_results' => true,
// Retention period for scan results (days)
'results_retention_days' => 90,
// Notification settings (future feature)
'notifications' => [
'enabled' => false,
'channels' => ['mail'],
'notify_on_severity' => ['critical', 'high'],
'mail_to' => env('VIGIL_MAIL_TO', null),
],
];Runs all enabled checks and prints results as a table:
php artisan vigil:auditOptions:
--category=filesystem,configurationβ filter by category--check=fs.public_folder,cfg.envβ run specific checks only--fail-on=critical,highβ exit with code 1 on matching severity (default:critical)--format=jsonβ JSON output instead of table--output=/path/to/file.jsonβ write JSON to file--detailedβ show full messages, file paths, line numbers, and fix suggestions
Examples:
# Only filesystem checks
php artisan vigil:audit --category=filesystem
# Specific checks with details
php artisan vigil:audit --check=fs.public_folder,cfg.env --detailed
# JSON report
php artisan vigil:audit --format=json --output=storage/security-report.json
# CI/CD: fail on critical or high
php artisan vigil:audit --fail-on=critical,highGenerates SHA-256 hashes of files in public/ and storage/app/public/ for integrity monitoring:
php artisan vigil:baselineSaves to storage/app/vigil_baseline.json. Once a baseline exists, enable the ext.file_integrity check in config to compare against it on subsequent scans.
Shows all 15 checks with their category, severity, and enabled/disabled status:
php artisan vigil:listThe --detailed flag adds full context to each check result: untruncated messages, exact file paths with line numbers, and specific recommendations.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β FAILED - Hardcoded Secrets Detection β
β ID: ext.hardcoded_secrets | Category: extended | Severity: CRITICAL β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Message: Found 3 potential hardcoded secrets in your codebase β
β β
β Details: β
β β’ app/Services/PaymentService.php:42 β
β $apiKey = 'sk_live_abc123def456'; β
β β
β β’ config/services.php:18 β
β 'secret' => 'hardcoded_secret_key', β
β β
β Recommendation: β
β Move all secrets to .env file and use env() helper: β
β $apiKey = env('PAYMENT_API_KEY'); β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Useful for investigating failures, generating compliance reports, or just understanding what exactly triggered a check.
You can also run scans from code:
use FilaStudio\Vigil\SecurityScanner;
$scanner = app(SecurityScanner::class);
// Run all enabled checks
$results = $scanner->run();
foreach ($results as $checkId => $entry) {
$check = $entry['check']; // SecurityCheck instance
$result = $entry['result']; // CheckResult instance
echo "{$check->title()}: {$result->status}\n";
if ($result->status === 'failed') {
echo "Message: {$result->message}\n";
echo "Recommendation: {$result->recommendation}\n";
print_r($result->details);
}
}
// Filter by check ID or category
$results = $scanner->run(['fs.public_folder', 'cfg.env']);
$results = $scanner->run(null, ['filesystem', 'configuration']);
// Security score
$passed = count(array_filter($results, fn($e) => $e['result']->status === 'passed'));
$skipped = count(array_filter($results, fn($e) => $e['result']->status === 'skipped'));
$score = $scanner->calculateSecurityScore($passed, count($results), $skipped);
// List all checks (including disabled)
$allChecks = $scanner->getAllChecks();
// Delete old scan records (respects retention config)
$deletedCount = $scanner->cleanupOldScans();If you use Filament v5, Vigil ships with a dashboard page and widgets.
Register the plugin in your AdminPanelProvider:
use FilaStudio\Vigil\Filament\VigilPlugin;
public function panel(Panel $panel): Panel
{
return $panel
->plugins([
VigilPlugin::make()
->navigationGroup('Security')
->navigationSort(99),
]);
}The dashboard includes:
- Security score widget β 0-100 score, color-coded by severity
- Critical issues counter β number of failed critical checks
- Recent scans table β history with scores, timestamps, and issue counts
- Detailed results β click into any scan to see per-check results with messages and recommendations
- Run scan button β trigger scans from the browser without CLI access
Handy for tracking trends over time, sharing results with the team, or pulling up a quick overview during a meeting.
| Check ID | Title | Category | Severity | Description |
|---|---|---|---|---|
fs.public_folder |
Public Folder Security | filesystem | high | Scans for unexpected files in public directory |
fs.malicious_js |
Malicious JavaScript Detection | filesystem | critical | Detects obfuscated/malicious JS patterns |
fs.storage_dangerous |
Dangerous Files in Storage | filesystem | critical | Finds executable files in public storage |
fs.permissions |
File Permissions Check | filesystem | high | Verifies Unix file permissions (Linux/Mac only) |
fs.sensitive_exposure |
Sensitive Files Exposure | filesystem | critical | Checks if .env, composer.json, .git are accessible via HTTP |
cfg.php_ini |
PHP Configuration Check | configuration | high | Validates PHP ini directives for security |
cfg.env |
Environment Configuration | configuration | critical | Checks APP_DEBUG, APP_KEY, APP_ENV settings |
cfg.session |
Session Configuration | configuration | medium | Validates session security settings |
cfg.cors |
CORS Configuration | configuration | high | Detects dangerous CORS misconfigurations |
http.headers |
Security Headers Check | http_headers | high | Ensures HSTS, CSP, X-Frame-Options, etc. are set |
dep.composer_audit |
Composer Dependencies Audit | dependencies | critical | Runs composer audit to find vulnerable packages |
ext.hardcoded_secrets |
Hardcoded Secrets Detection | extended | critical | Scans for hardcoded passwords, API keys, tokens |
ext.debug_routes |
Debug Routes Detection | extended | high | Finds debug endpoints (phpinfo, dd, dump) in routes |
ext.telescope_debugbar |
Telescope & Debugbar Check | extended | high | Ensures debug tools are secured in production |
ext.file_integrity |
File Integrity Check | extended | critical | Compares files against baseline to detect tampering |
Implement the SecurityCheck interface:
namespace App\Security\Checks;
use FilaStudio\Vigil\Checks\Contracts\SecurityCheck;
use FilaStudio\Vigil\Checks\Results\CheckResult;
use FilaStudio\Vigil\Enums\Severity;
use FilaStudio\Vigil\Enums\Category;
class DatabaseSSLCheck implements SecurityCheck
{
public function id(): string { return 'custom.database_ssl'; }
public function title(): string { return 'Database SSL Connection Check'; }
public function description(): string { return 'Ensures database connections use SSL in production'; }
public function category(): string { return Category::Configuration->value; }
public function severity(): string { return Severity::High->value; }
public function run(): CheckResult
{
if (app()->environment('local')) {
return CheckResult::skipped('Not applicable in local environment');
}
$issues = [];
foreach (config('database.connections') as $name => $config) {
if (($config['driver'] ?? '') === 'mysql') {
if (empty($config['options'][\PDO::MYSQL_ATTR_SSL_CA])) {
$issues[] = "Connection '{$name}' does not use SSL";
}
}
}
if (!empty($issues)) {
return CheckResult::failed(
'Database connections without SSL detected',
['connections' => $issues],
'Add SSL options to config/database.php for production connections'
);
}
return CheckResult::passed('All database connections use SSL');
}
}Register it in a service provider:
use FilaStudio\Vigil\SecurityScanner;
public function boot()
{
app(SecurityScanner::class)->registerCheck(new \App\Security\Checks\DatabaseSSLCheck());
}After that, php artisan vigil:audit will include your check alongside the built-in ones.
CheckResult has four static constructors: passed(), failed(), warning(), skipped(). Use skipped() when a check doesn't apply to the current environment.
PHP 8.2 backed enums are available for type-safe values:
CheckStatus:Passed,Failed,Warning,SkippedSeverity:Critical,High,Medium,Low,InfoCategory:Filesystem,Configuration,HttpHeaders,Dependencies,Extended
Scan results accumulate in the database over time. The retention period is configurable:
// config/vigil.php
'results_retention_days' => 90,To delete records older than the retention period:
app(SecurityScanner::class)->cleanupOldScans();Or schedule it:
// app/Console/Kernel.php
$schedule->call(function () {
app(\FilaStudio\Vigil\SecurityScanner::class)->cleanupOldScans();
})->weekly();The --fail-on flag makes Vigil work as a pipeline gate. If any check with the specified severity fails, the command exits with code 1.
php artisan vigil:audit --fail-on=critical,highGitHub Actions example:
- name: Security Audit
run: php artisan vigil:audit --fail-on=critical,highGitLab CI:
security_audit:
stage: test
script:
- php artisan vigil:audit --fail-on=critical,highJenkins:
sh 'php artisan vigil:audit --fail-on=critical,high'You can adjust the threshold: --fail-on=critical for lenient, --fail-on=critical,high,medium for strict.
# Tests (PestPHP)
composer test
# Code style (Laravel Pint)
composer pint:test
# Static analysis (PHPStan level 8)
composer phpstan
# All at once
composer test && composer pint:test && composer phpstanPull requests are welcome.
MIT. See LICENSE.
