andreapollastri / checkpoint
Laravel security scanner — checks CVEs, hardcoded secrets, SQL injection, XSS and more via a single Artisan command.
Requires
- php: ^8.1
- illuminate/console: ^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0 || ^13.0
- illuminate/support: ^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0 || ^13.0
Requires (Dev)
- orchestra/testbench: ^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0 || ^11.0
- phpunit/phpunit: ^9.5.10 || ^10.0 || ^11.0 || ^12.0 || ^13.0
README
A Laravel security scanner that audits your application for common vulnerabilities — from known CVEs to hardcoded secrets — via a single Artisan command.
php artisan checkpoint:scan
What it checks
| # | Check | Severity |
|---|---|---|
| 1 | Composer CVE Audit — runs composer audit and reports known advisories |
FAIL |
| 2 | NPM CVE Audit — runs npm audit and flags critical/high vulnerabilities |
FAIL / WARN |
| 3 | Environment Configuration — APP_DEBUG, APP_KEY, APP_URL, SESSION_SECURE_COOKIE |
WARN |
| 4 | .gitignore Sensitive Files — ensures .env, *.key, *.pem are excluded; detects if .env is tracked by git |
FAIL |
| 5 | File Permissions — flags world-readable .env or world-writable storage/ |
WARN |
| 6 | Hardcoded Secrets — scans PHP/JS files for API keys, Stripe tokens, AWS keys, GitHub PATs, PEM headers | FAIL |
| 7 | SQL Injection Risks — detects raw queries with variable interpolation (DB::select("… $var"), ->whereRaw(…)) |
FAIL |
| 8 | Mass Assignment Vulnerabilities — finds $guarded = [], Model::unguard(), or models with no fillable/guarded definition |
WARN |
| 9 | XSS (Cross-Site Scripting) Risks — flags unescaped {!! $var !!} in Blade views and raw echo of request input |
WARN |
| 10 | CSRF Protection — detects forms with POST/PUT/PATCH/DELETE missing @csrf, and checks middleware is present |
FAIL |
| 11 | Open Redirect Risks — spots redirect($request->…) or header('Location: ' . $var) with unvalidated input |
WARN |
| 12 | Command Injection Risks — finds exec, shell_exec, system, passthru, proc_open called with unescaped variables |
FAIL |
| 13 | Insecure Deserialization — detects unserialize($userInput) and the classic unserialize(base64_decode(…)) exploit chain |
FAIL |
| 14 | Debug Functions in Production Code — finds var_dump, dd, dump, ray left outside of test files |
WARN |
| 15 | Sensitive Data Exposure — flags display_errors = 1, logging of passwords/tokens, and Telescope always-on config |
WARN |
| 16 | SSRF Risks — detects Http::get($request->…), Guzzle/cURL/file_get_contents called with user-controlled URLs |
FAIL |
| 17 | TLS Certificate Verification — flags withoutVerifying(), 'verify' => false, CURLOPT_SSL_VERIFYPEER => false |
FAIL |
| 18 | CORS Configuration — flags allowed_origins => ['*'] combined with supports_credentials => true and other loose configs |
FAIL / WARN |
| 19 | Package Freshness (Supply Chain) — fails the scan if any Composer package was released within the last N days (default 3); whitelist via config | FAIL |
| 20 | Supply Chain Tooling — when package.json is present, warns if no npm install-time guard (Safe-Chain, Socket CLI) is on PATH |
WARN |
| 21 | Path Traversal Risks — detects Storage::get($request->…), file_get_contents/include/require with user-controlled paths |
FAIL |
| 22 | Weak Cryptography — flags mcrypt_*, ECB mode, DES/3DES/RC4, and md5/sha1 used near password/token/HMAC keywords |
FAIL / WARN |
| 23 | Insecure RNG — detects rand/mt_rand/uniqid used in security contexts (tokens, CSRF, password reset, OTP) |
FAIL |
| 24 | Session & Cookie Security — audits config/session.php for http_only=false, same_site=null/none, secure=false, encrypt=false |
WARN |
| 25 | EOL Versions — flags Composer-locked Laravel and the running PHP when they are past or approaching upstream security cutoff | FAIL / WARN |
| 26 | Suspicious Vendor Autoload — flags packages under vendor/ that register PHP via autoload.files outside a baked-in whitelist (the mechanism abused by the May 2026 Laravel-Lang supply-chain attack) |
WARN |
Requirements
- PHP
^8.1is the package floor. Newer Laravel versions may require a higher PHP, so your effective minimum is whatever your Laravel major demands. - Laravel
8–13(same major asilluminate/*8.x–13.x used by your app)
Installation
composer require --dev andreapollastri/checkpoint
The package auto-discovers itself via Laravel's package discovery — no manual registration needed.
Recommended companion tools
Checkpoint is a static scanner — it inspects your codebase but doesn't intercept package installs. To harden the npm install path against the next event-stream/ua-parser-js/chalk-style supply-chain attack, pair Checkpoint with defense in depth: run installs inside containers and add a runtime guard on the host when you must install locally.
Docker (recommended)
Run composer install, npm install, and php artisan checkpoint:scan inside a container, not directly on your laptop or server shell. Supply-chain malware often executes during a package's post-install script — if that runs on your host, it has access to your SSH keys, browser cookies, and filesystem. A disposable dev container limits blast radius to an isolated filesystem that you can throw away.
Typical workflow:
docker compose up -d docker compose exec app composer install docker compose exec app npm install docker compose exec app php artisan checkpoint:scan
Use whatever Docker setup fits your project — Docker Compose, a devcontainer, or a CI image such as composer:2 with Node in the same job. The goal is the same: never let untrusted install scripts run on bare metal; treat the host as a control plane only.
Checkpoint's CI scaffolds (GitHub Actions, GitLab CI) already run the scan inside ephemeral containers. Mirror that pattern locally.
Safe-Chain (recommended)
Safe-Chain by Aikido is a free shell shim that blocks known-malicious npm packages before their install scripts run. Install it once, globally:
npm install -g @aikidosec/safe-chain safe-chain setup
When you do run npm on the host (CI runners, one-off scripts), Safe-Chain blocks known-malicious packages before their install scripts run. Checkpoint's Supply Chain Tooling check verifies whether Safe-Chain (or an equivalent like Socket CLI) is on your PATH and emits a WARN if no protection is present.
Why Checkpoint doesn't install it for you: Safe-Chain works as a global shell shim, not a project dependency. A
composer requireshould never invoke another ecosystem's package manager (and can't reliably do so on hosts without Node). Installing it explicitly keeps the install path auditable. Prefer Docker for day-to-day installs; keep Safe-Chain as a safety net where containers aren't practical.
Usage
Run all checks
php artisan checkpoint:scan
Run only specific checks
php artisan checkpoint:scan --only="SQL Injection Risks,CSRF Protection"
Skip specific checks
php artisan checkpoint:scan --skip="NPM CVE Audit,Debug Functions in Production Code"
JSON output (for CI/CD pipelines)
php artisan checkpoint:scan --json
The command exits with code 1 if any check returns FAIL, making it suitable as a pipeline gate.
Configuration
Checkpoint works out of the box with sensible defaults. Publish the config file when you need to toggle individual checks or tune the freshness gate:
php artisan vendor:publish --tag=checkpoint-config
This creates config/checkpoint.php with two sections:
Enabling / disabling checks
Every default check is listed and enabled. Set any entry to false to exclude it from the scan:
'checks' => [ Checks\ComposerAuditCheck::class => true, Checks\NpmAuditCheck::class => false, // skip npm audit on a PHP-only project Checks\EnvironmentCheck::class => true, // … Checks\PackageFreshnessCheck::class => true, Checks\SupplyChainToolingCheck::class => true, ],
Checks not listed in the map fall back to enabled. When you upgrade Checkpoint and new checks are added, you keep the protection without re-publishing the config — re-publish only when you want to see the full updated list.
Package Freshness tuning
'package_freshness' => [ 'minimum_age_days' => 3, 'whitelist' => [ 'andreapollastri/checkpoint', // bundled — see note below // 'laravel/framework', // 'symfony/console', ], ],
minimum_age_days— packages released more recently than this fail the scan. Default3. Set to0to bypass the freshness gate entirely without removing the check from the scanner.whitelist— fully-qualified package names (vendor/package) exempt from the freshness check. Use sparingly and ideally with an inline comment explaining why each entry is allowed.
Checkpoint ships with
andreapollastri/checkpointalready whitelisted by default — a fresh release of the scanner itself should never block its own user's deploy. Remove the entry if you want to gate even Checkpoint upgrades through the freshness window.
Suppressing individual findings
Every WARN or FAIL finding is shown with a stable 12-character hash:
FAIL Hardcoded Secrets
3 potential hardcoded secret(s) found.
✗ app/Services/PaymentService.php:14 — 'api_key' => 'sk_live_…' [a1b2c3d4e5f6]
✗ config/services.php:8 — $secret = 'super…' [9f8e7d6c5b4a]
To silence one — false positive, accepted legacy code, internal test fixture — copy the bracketed hash into config/checkpoint.php:
'suppressed' => [ 'a1b2c3d4e5f6', '9f8e7d6c5b4a', ],
On the next run those findings are filtered out. If every finding of a given check ends up suppressed, the check is downgraded to PASS with an explicit "All N finding(s) suppressed via config." message — so the suppression is visible in the output, not silently ignored.
The hash is content-stable: refactors that only shift line numbers within the same file will not invalidate the suppression. The hash does change if you alter the file path or the finding content itself, which is the intended safety net.
The
--only/--skipCLI flags still work and override the config for the current run, which is handy for ad-hoc scans without editing the config.
Example output
██████╗██╗ ██╗███████╗ ██████╗██╗ ██╗██████╗ ██████╗ ██╗███╗ ██╗████████╗
██╔════╝██║ ██║██╔════╝██╔════╝██║ ██╔╝██╔══██╗██╔═══██╗██║████╗ ██║╚══██╔══╝
██║ ███████║█████╗ ██║ █████╔╝ ██████╔╝██║ ██║██║██╔██╗██║ ██║
██║ ██╔══██║██╔══╝ ██║ ██╔═██╗ ██╔═══╝ ██║ ██║██║██║╚████║ ██║
╚█████╗██║ ██║███████╗ ╚█████╗██║ ██╗██║ ╚█████╔╝██║██║ ╚███║ ██║
╚════╝╚═╝ ╚═╝╚══════╝ ╚════╝╚═╝ ╚═╝╚═╝ ╚════╝ ╚═╝╚═╝ ╚══╝ ╚═╝
Laravel Security Scanner — andreapollastri/checkpoint
Scanning: /var/www/my-app
PASS Composer CVE Audit
No known CVEs in Composer dependencies.
FAIL Hardcoded Secrets
3 potential hardcoded secret(s) found.
✗ app/Services/PaymentService.php:14 — 'api_key' => 'sk_live_abc123…' [a1b2c3d4e5f6]
✗ config/services.php:8 — $secret = 'supersecretvalue' [9f8e7d6c5b4a]
✗ app/Http/Controllers/WebhookController.php:31 — 'api_key' => 'ghp_…' [3e2d1c0b9a8f]
FAIL Path Traversal Risks
1 potential path traversal risk(s) found.
✗ app/Http/Controllers/DownloadController.php:24 — Storage::get($request->path) [7b6a5f4e3d2c]
WARN Environment Configuration
3 environment issue(s) found.
⚑ APP_DEBUG is true — full stack traces will be exposed to end users. [5c4b3a2d1e0f]
⚑ SESSION_SECURE_COOKIE is not enabled. [1a2b3c4d5e6f]
⚑ APP_URL is set to "http://localhost" — update it for production. [6e5d4c3b2a1f]
─────────────────────────────────────────────────────────
Summary 19 passed 4 warning(s) 2 failed (25 checks total)
Scan result: FAIL — fix the issues above before deploying.
Extending with custom checks
Create a class that extends AbstractCheck and return a CheckResult:
use Checkpoint\Checks\AbstractCheck; use Checkpoint\Checks\CheckResult; class MyCustomCheck extends AbstractCheck { public function name(): string { return 'My Custom Check'; } public function run(): CheckResult { // your logic here return CheckResult::pass('Everything looks good.'); // or: CheckResult::warn('Something to review.', ['detail one', 'detail two']); // or: CheckResult::fail('Critical issue found.', ['detail']); } }
Then register it by building a Scanner manually instead of using the default:
use Checkpoint\Scanner; $scanner = Scanner::withDefaultChecks(base_path()) ->add(new MyCustomCheck());
CI/CD integration
Checkpoint can scaffold a ready-to-use pipeline for either provider in one command.
GitHub Actions
php artisan checkpoint:github
Creates .github/workflows/checkpoint.yml — triggers on push to main/master and on every pull request. Uses actions/checkout@v4, shivammathur/setup-php@v2 (PHP 8.2), Composer cache, and runs php artisan checkpoint:scan. Pass --force to overwrite an existing file.
GitLab CI
php artisan checkpoint:gitlab
Creates .gitlab-ci.yml — runs on merge requests and default-branch pushes using the composer:2 image with a Composer cache. If you already have a .gitlab-ci.yml, the command refuses to overwrite and prints the snippet to stdout so you can paste it into your existing pipeline. Use --force to overwrite.
Custom usage
If you prefer to wire Checkpoint into a pipeline you already maintain, just call:
- name: Security audit run: php artisan checkpoint:scan --json | tee checkpoint-report.json
Composer hooks
Run Checkpoint automatically after every composer install / composer update:
php artisan checkpoint:install-hooks
This appends @php artisan checkpoint:scan to scripts.post-update-cmd and scripts.post-install-cmd in your composer.json. The command:
- Confirms interactively before touching
composer.json(skip the prompt with--no-interactionfor CI). - Is idempotent: re-running on an already-installed setup is a no-op.
- Preserves any existing hooks (append-only); does not overwrite scripts owned by other tools.
- Supports
--removeto cleanly uninstall and--forceto replace stale Checkpoint entries.
Why only
post-*, notpre-*? Composer'spre-update-cmdfires before dependencies are resolved, so the scanner would only see the codebase pre-update — useless against a malicious package that's about to be installed. And on a fresh clone there is novendor/, sophp artisandoes not exist yet andpre-install-cmdwould crash. Real-time interception of malicious installs is the job of Docker (preferred) and Safe-Chain on the host.
Exit codes
| Code | Meaning |
|---|---|
0 |
All checks passed (or warnings only) |
1 |
At least one check returned FAIL |
Architecture
src/
├── CheckpointServiceProvider.php # auto-registers the command
├── Scanner.php # orchestrates all checks
├── Commands/
│ ├── ScanCommand.php # php artisan checkpoint:scan
│ ├── GithubPipelineCommand.php # php artisan checkpoint:github
│ ├── GitlabPipelineCommand.php # php artisan checkpoint:gitlab
│ └── InstallHooksCommand.php # php artisan checkpoint:install-hooks
└── Checks/
├── AbstractCheck.php # base class
├── CheckResult.php # pass / warn / fail value object
├── ComposerAuditCheck.php
├── NpmAuditCheck.php
├── EnvironmentCheck.php
├── GitIgnoreCheck.php
├── FilePermissionsCheck.php
├── HardcodedSecretsCheck.php
├── SqlInjectionCheck.php
├── MassAssignmentCheck.php
├── XssCheck.php
├── CsrfCheck.php
├── OpenRedirectCheck.php
├── CommandInjectionCheck.php
├── InsecureDeserializationCheck.php
├── DebugFunctionsCheck.php
├── SensitiveExposureCheck.php
├── SsrfCheck.php
├── TlsVerificationCheck.php
├── CorsConfigCheck.php
├── PackageFreshnessCheck.php
├── SuspiciousVendorAutoloadCheck.php
├── SupplyChainToolingCheck.php
├── PathTraversalCheck.php
├── WeakCryptographyCheck.php
├── InsecureRngCheck.php
├── SessionSecurityCheck.php
└── EolVersionCheck.php
License
MIT — Andrea Pollastri