abdulsalamalkhatib / laravel-guardian
Production correctness engine for Laravel applications.
Package info
github.com/abdulsalamalkhatib96/laravel-guardian
pkg:composer/abdulsalamalkhatib/laravel-guardian
Requires
- php: ^8.2
- ext-json: *
- illuminate/console: ^11.44.2 || ^12.4.1 || ^13.0
- illuminate/contracts: ^11.44.2 || ^12.4.1 || ^13.0
- illuminate/support: ^11.44.2 || ^12.4.1 || ^13.0
- larastan/larastan: ^3.11
- nikic/php-parser: ^5.4
- phpstan/phpstan: ^2.2.2
Requires (Dev)
- orchestra/testbench: ^9.12 || ^10.1 || ^11.0
- phpunit/phpunit: ^11.5 || ^12.5 || ^13.3
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Production correctness engine for Laravel.
Laravel Guardian looks for code that is syntactically valid, type-correct, and often testable, but can still violate production invariants under transactions, concurrency, or serialization.
V1 intentionally ships only three rules:
- GUA-001 — External side effects inside database transactions.
- GUA-002 — Dangerous Eloquent read-modify-write / check-then-act flows.
- GUA-003 — Sensitive data reaching API serialization paths.
Guardian is not a replacement for PHPStan, Larastan, Telescope, Debugbar, or a generic SAST scanner. It focuses on Laravel-specific production semantics.
Requirements
- PHP 8.2+
- Laravel 11.44.2+, 12.4.1+, or 13.x
- PHPStan 2.2.2+
- Larastan 3.11+
Installation
After publishing the package to Packagist:
composer require --dev abdulsalamalkhatib/laravel-guardian php artisan vendor:publish --tag=guardian-config
For local development from this ZIP before publishing:
{
"repositories": [
{
"type": "path",
"url": "../laravel-guardian"
}
]
}
Then:
composer require --dev abdulsalamalkhatib/laravel-guardian:@dev
Quick start
php artisan guardian:scan
or without booting the Laravel application:
vendor/bin/guardian scan
Example output:
Laravel Guardian
────────────────────────────────────────────────────────────────
Scanned 423 files · 184 classes · 3,192 methods · 1.28s
CRITICAL 2 HIGH 7 MEDIUM 3 LOW 0
[GUA-001] CRITICAL · HIGH confidence
External side effect inside database transaction
app/Services/PaymentService.php:91
Risk:
The external operation may succeed irreversibly while the database
transaction later rolls back, leaving systems inconsistent.
GUA-001 — transaction side effects
Unsafe:
DB::transaction(function () use ($payment) { $payment->update(['status' => 'paid']); Http::post($gatewayUrl, [ 'payment_id' => $payment->id, ]); });
The remote system may commit while the DB transaction later rolls back.
Guardian also follows known application calls:
DB::transaction(function () { $this->paymentService->charge(); }); // ... public function charge(): void { $this->gateway->capture(); } public function capture(): void { Http::post(...); }
Safe queued pattern:
DB::transaction(function () use ($payment) { $payment->update(['status' => 'paid']); SendPaymentWebhook::dispatch($payment->id)->afterCommit(); });
Built-in external-effect families include HTTP, mail, notifications, queue dispatch, filesystem writes, process execution, and configured external SDK methods.
GUA-002 — unsafe Eloquent read-modify-write
Unsafe lost update:
$wallet = Wallet::findOrFail($walletId); $wallet->balance += $amount; $wallet->save();
A database transaction by itself does not make this safe.
Safe pessimistic lock:
DB::transaction(function () use ($walletId, $amount) { $wallet = Wallet::query() ->whereKey($walletId) ->lockForUpdate() ->firstOrFail(); $wallet->balance += $amount; $wallet->save(); });
Safe fixed atomic delta:
Wallet::whereKey($walletId)->increment('balance', $amount);
Still unsafe despite using increment():
$wallet = Wallet::findOrFail($walletId); $delta = $targetBalance - $wallet->balance; $wallet->increment('balance', $delta);
The write primitive is atomic, but the delta was derived from stale state. Guardian distinguishes this from a fixed atomic increment.
Guardian also detects check-then-act transitions such as:
if ($withdrawal->status === 'pending') { $withdrawal->status = 'approved'; $withdrawal->save(); }
Use row locks or compare-and-swap semantics for state transitions.
GUA-003 — sensitive exposure
Guardian builds a sensitivity registry from:
- explicit
sensitive_fieldsconfiguration; - Eloquent
$hiddenfields; - a conservative built-in list for secrets such as passwords, API keys, refresh tokens, and 2FA secrets.
Unsafe:
final class PlayerResource extends JsonResource { public function toArray($request): array { return [ 'id' => $this->id, 'phone' => $this->phone, ]; } }
If Player::$hidden or Guardian config marks phone as sensitive, the resource is reported.
Configured sanitizers are treated as declassification boundaries:
return [ 'phone' => Str::mask($this->phone, '*', 3), ];
Guardian also tracks simple local-variable taint into response()->json(), JsonResponse, and resource arrays.
Configuration
Publish config/guardian.php and tune only application-specific policy:
return [ // Relative to the scanned project root so standalone CLI works without booting Laravel. 'paths' => ['app'], 'fail_on' => 'high', 'minimum_confidence' => 'medium', 'rules' => [ 'GUA-001' => [ 'enabled' => true, 'queue_after_commit' => null, 'custom_effects' => [ App\Payments\AcmeGateway::class.'::capture', ], ], 'GUA-002' => [ 'enabled' => true, 'critical_fields' => [ App\Models\Wallet::class => ['balance', 'bonus_balance'], App\Models\Withdrawal::class => ['status'], ], 'trusted_distributed_locks' => false, ], 'GUA-003' => [ 'enabled' => true, 'sensitive_fields' => [ App\Models\Player::class => [ 'phone', 'email', 'date_of_birth', ], ], 'resource_models' => [ App\Http\Resources\PlayerResource::class => App\Models\Player::class, ], 'sanitizers' => [ Illuminate\Support\Str::class.'::mask', ], ], ], ];
Commands
vendor/bin/guardian scan vendor/bin/guardian scan --format=json vendor/bin/guardian scan --format=sarif vendor/bin/guardian scan --format=junit vendor/bin/guardian scan --format=gitlab vendor/bin/guardian scan --fail-on=critical vendor/bin/guardian scan --min-confidence=high vendor/bin/guardian baseline vendor/bin/guardian list vendor/bin/guardian explain GUA-002 vendor/bin/guardian doctor vendor/bin/guardian init
Laravel aliases:
php artisan guardian:scan php artisan guardian:baseline php artisan guardian:list php artisan guardian:explain GUA-001 php artisan guardian:doctor
Exit codes:
| Code | Meaning |
|---|---|
| 0 | Scan succeeded and no blocking finding exists |
| 1 | Blocking finding exists |
| 2 | Invalid configuration / analysis setup |
| 3 | Guardian internal failure |
Severity and confidence are separate
A finding can be:
CRITICAL · HIGH confidence
or:
CRITICAL · MEDIUM confidence
Severity answers how bad the failure would be. Confidence answers how strongly static analysis proved the path.
Baseline
Adopt Guardian on a legacy codebase without blocking every existing issue:
vendor/bin/guardian baseline
This writes stable semantic fingerprints to guardian-baseline.json. Subsequent scans still show counts for suppressed/baselined findings but only new active findings block CI.
Refresh the baseline by running the command again after fixing or intentionally accepting findings.
Narrow suppressions
Inline suppression requires a reason:
// guardian-ignore GUA-003: account owner explicitly requested this field return response()->json([ 'phone' => $player->phone, ]);
Method/class suppression:
use Guardian\Attributes\GuardianIgnore; #[GuardianIgnore( rule: 'GUA-003', reason: 'Internal authenticated endpoint restricted to fraud operations', )] final class FraudResource extends JsonResource { // ... }
For intentional sensitive exposure, prefer the narrower field-level attribute instead of suppressing all of GUA-003:
use Guardian\Attributes\GuardianAllowsSensitive; #[GuardianAllowsSensitive( fields: ['phone'], reason: 'The authenticated account owner explicitly requested their own profile phone number.', )] public function toArray($request): array { return ['phone' => $this->phone]; }
GuardianAllowsSensitive can be placed on a resource method or class and only suppresses the listed sensitive fields.
Avoid broad project-wide ignores. A suppression is an explicit correctness exception and should be reviewable.
PHPStan / Larastan integration
The package ships a PHPStan extension bridge in extension.neon.
With PHPStan extension discovery enabled, or by including it manually:
includes: - vendor/larastan/larastan/extension.neon - vendor/abdulsalamalkhatib/laravel-guardian/extension.neon
A full-directory PHPStan analysis receives Guardian findings as PHPStan errors with identifiers:
guardian.gua001
guardian.gua002
guardian.gua003
Guardian metadata includes severity, confidence, and fingerprint. File-only partial PHPStan runs intentionally do not trigger Guardian's whole-project scan.
CI
Console gate:
vendor/bin/guardian scan \ --format=console \ --fail-on=high \ --min-confidence=medium
SARIF:
vendor/bin/guardian scan --format=sarif > guardian.sarif
GitLab Code Quality:
vendor/bin/guardian scan --format=gitlab > gl-code-quality-report.json
JUnit:
vendor/bin/guardian scan --format=junit > guardian-junit.xml
Plugin SDK
Guardian's registry supports additional rules and domain adapters without modifying core:
use Guardian\Application\GuardianRegistry; use Guardian\Contracts\Plugin; final class PaymentsGuardianPlugin implements Plugin { public function register(GuardianRegistry $registry): void { $registry ->rule(new ProviderIdempotencyRule()) ->effectProvider(new AcmePaymentEffectProvider()) ->sanitizer(new InternalTokenRedactor()) ->concurrencyGuard(new WalletMutexGuard()); } }
Extension contracts:
RuleReporterEffectProviderSanitizerProviderConcurrencyGuardProviderPlugin
Future plugin families can live under separate Composer packages such as Guardian\Concurrency, Guardian\Queues, and Guardian\Performance without bloating the V1 core rule set.
Architecture
SourceFinder
↓
PHP parser + resolved names + parent links
↓
ProgramIndex
├── classes / methods / property types
├── structural facts
└── application call resolution
↓
Rule engines
├── GUA-001 transaction/effect propagation
├── GUA-002 state-dependency/concurrency analysis
└── GUA-003 sensitivity/serialization taint analysis
↓
Finding pipeline
├── severity
├── confidence
├── stable fingerprint
├── inline/attribute suppression
└── baseline
↓
Console / JSON / SARIF / JUnit / GitLab
See docs/ARCHITECTURE.md for the detailed design and correctness boundaries.
Known limits
Static analysis cannot prove all behavior in highly dynamic PHP. Guardian deliberately prefers an explicit confidence level over pretending certainty.
Current V1 limitations include:
- dynamic method names and runtime-selected container bindings may not resolve interprocedurally;
- custom transaction abstractions need a future transaction-provider extension or must wrap supported Laravel primitives;
- GUA-002 is deliberately conservative and does not attempt full symbolic execution;
- GUA-003 is not an authorization theorem prover: conditional authorization lowers confidence rather than proving the exposure safe;
- distributed lock key correctness cannot generally be proven statically.
Zero Guardian findings means no supported invariant violation was proven. It does not mean the application is bug-free.
Development
composer install
composer test
vendor/bin/phpunit
vendor/bin/phpstan analyse
The integration suite contains both unsafe fixtures that must be detected and safe fixtures that must remain clean.
License
MIT.