andreapollastri/checkpoint
GitHub: andreapollastri/checkpoint
Stars: 56 | Forks: 1
# Checkpoint
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.1` is 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 as `illuminate/*` 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.
### Safe-Chain (recommended)
[Safe-Chain](https://www.npmjs.com/package/@aikidosec/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.
## 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,
],
### 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. Default `3`. **Set to `0` to 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.
### 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.
## 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-interaction` for 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 `--remove` to cleanly uninstall and `--force` to replace stale Checkpoint entries.
### 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](https://andreapollastri.net)
标签:ffuf