Search by

watcher-php / laravel

chukwudi34

Security intelligence and protection layer for Laravel applications: static analysis, runtime threat detection, incident management and notifications.

Package info

github.com/chukwudi34/watcher

pkg:composer/watcher-php/laravel

Statistics

Installs: 6

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.2 2026-11-04 09:54 UTC

This package is auto-updated.

Last update: 2026-11-04 10:06:57 UTC


README

Security should never be an afterthought.

Watcher is a security intelligence and protection layer for Laravel applications. It identifies potentially insecure code before deployment, monitors runtime behavior for suspicious activity, correlates related events into investigable incidents, and notifies your team — without claiming to prevent all attacks or guarantee application security.

What it does

  • Prevent — static analysis integration (Psalm taint analysis for SQLi/XSS/SSRF/etc., a custom PHPStan rule for Eloquent mass-assignment, composer audit for known dependency vulnerabilities), plus #[RequirePermission]/#[Guarded]/#[Audit] attributes enforced on controller actions.
  • Understand — deterministic (no AI) business-logic discovery: reads your routes, controllers and models to classify sensitive operations and check for missing ownership/access-control checks (IDOR). See Business Logic Intelligence.
  • Detect — listens to real Laravel events (authentication, authorization denials, queue failures, database errors, sensitive model access) and correlates repeated failures per subject (user or IP) into incidents, instead of raising one alert per event.
  • Investigate — a protected dashboard showing events, incidents (with evidence, timeline and recommended steps), static-analysis findings, business-logic findings, and the notification log.
  • Notify — mail, Slack and generic webhook channels, with a severity threshold and a per-incident cooldown so a sustained attack doesn't flood your team.

Watcher does not implement its own static analyser, taint tracker, or SQL parser — it integrates Psalm, PHPStan and Composer's own advisory database, and extends them with a small number of Laravel-specific rules where generic tools don't reach (e.g. protected $guarded = [];).

Requirements

  • PHP ^8.1 (the floor is set by enums and readonly properties, used throughout src/Core/; Laravel 10 itself requires ^8.1, so this matches the lowest supported framework version rather than being arbitrary)
  • Laravel (illuminate/support, illuminate/console, illuminate/database) ^10.0 | ^11.0 | ^12.0
  • Optional, for watcher:analyse: vimeo/psalm and/or phpstan/phpstan as dev dependencies of your application, and composer itself on PATH.

Installation

composer require watcher-php/laravel
php artisan watcher:install

watcher:install publishes config/watcher.php and the migrations, and offers to run them. It will warn you that the dashboard is inaccessible until you define its gate:

// e.g. in a service provider's boot()
Gate::define('viewWatcherDashboard', fn ($user) => $user->isAdmin());

There is no default-open dashboard route — if you never define this gate, the dashboard denies everyone.

Check everything is wired up:

php artisan watcher:status

Quick start

  1. Define the dashboard gate (above).

  2. Visit /watcher (configurable via watcher.dashboard.path) for the overview, events, incidents, findings and notifications.

  3. Protect a controller action:

    use Watcher\Attributes\{Guarded, RequirePermission, Audit};
    
    #[RequirePermission('manage-users')]
    #[Audit('user-deletion')]
    public function destroy(User $user)
    {
        $user->delete();
    }

    RequirePermission is checked against your own Gate/policies; Guarded just requires an authenticated user; Audit records every invocation regardless of outcome. Read Limitations before relying on these — they only protect HTTP requests routed through a controller action with the enforcement middleware in the pipeline (on by default, via the web group).

  4. Run static analysis against your own app:

    composer require --dev vimeo/psalm phpstan/phpstan
    vendor/bin/psalm --init --taint-analysis
    php artisan watcher:analyse

    Findings are persisted and shown on the dashboard's Findings page; each run replaces that tool's prior findings.

  5. Trigger a real incident to see the pipeline end to end: see docs/incident-scenarios.md, or use the bundled example application below.

Example application

workbench/ is a real, runnable Laravel app (via Orchestra Workbench) with Watcher installed, a demo user, and routes exercising every attribute. From a clone of this repository:

composer install
vendor/bin/testbench workbench:build
vendor/bin/testbench db:seed --class="Workbench\Database\Seeders\WorkbenchDatabaseSeeder"
composer run serve

Then, in a browser or with curl:

URL What it shows
/demo/open A route with no Watcher attributes — unaffected.
/demo/guarded #[Guarded] — 403 until you log in.
/demo/manage-users #[RequirePermission('manage-users')] + #[Audit] — 403 unless you're the demo admin (user 1); the call is audited either way.
/demo/invoices/{invoice} Secure: checks #[WatcherOwnership('user_id')] via a real $this->authorize() call. watcher:discover catalogues it, raises no finding.
/demo/invoices/{invoice}/vulnerable Deliberately vulnerable: same model, no ownership check. watcher:discover flags it — see Business Logic Intelligence.
/_workbench/login/1 Quick-login as the seeded demo admin (Workbench's own helper — not a Watcher feature).
/watcher The dashboard, once logged in (the demo's WorkbenchServiceProvider grants viewWatcherDashboard to any authenticated user — a real app must authorize this properly, not like the demo does).

See docs/incident-scenarios.md for a walkthrough that also triggers a real detected incident.

Configuration

Published to config/watcher.php. Highlights:

Key Purpose
enabled Kill switch — disables every listener/observer/middleware without uninstalling the package.
dashboard.gate, dashboard.path The authorization gate and URL prefix for the dashboard.
exclude.routes Route patterns (Str::is() wildcards) never observed.
sensitive_models Allow-list of Eloquent models to observe for access — nothing is watched unless listed here.
redaction.allowed_context_keys Empty by default (deny-list mode: secret-shaped keys like password/token are always redacted, everything else passes through). Populate to switch to strict allow-list mode.
sampling.rate Drops a fraction of low-severity events under load; critical/high severity is never sampled away.
collection.queue Persist events via a queued job instead of inline on the request.
detection.repeated_failure.* Threshold/window for correlating repeated auth/authz failures into one incident.
notifications.channels.*, minimum_severity, cooldown_minutes Mail/Slack/webhook channels, the severity floor for alerting, and the per-incident re-alert cooldown.
attributes.enforce Whether #[RequirePermission]/#[Guarded]/#[Audit] are checked automatically on the web group.
business_logic.* Column-name heuristics (ownership/tenant/financial) and sensitive route keywords watcher:discover uses, plus controller namespaces it never analyses. See Business Logic Intelligence.

Architecture

  • src/Core/ — framework-independent: events, detection rules, incident correlation, redaction, the business-rule shape (Core/BusinessLogic/). No Illuminate\* imports.
  • src/Laravel/ — adapters: service provider, listeners/observers, the dashboard controller, notification channels, Eloquent repositories, business-logic discovery services (Laravel/BusinessLogic/).
  • src/Attributes/ — RequirePermission, Guarded, Audit, plus the business-logic declarations WatcherSensitive, WatcherOwnership, WatcherTenantIsolation, WatcherStateTransition. Plain PHP 8 attributes with their own autoload root; zero framework dependency, since they're pure metadata.
  • src/StaticAnalysis/ — the bundled PHPStan rules, kept separate from both Core and Laravel since they're neither runtime logic nor a Laravel integration.

Runtime flow: a Laravel event/observer/middleware builds a SecurityEvent via SecurityRecorder (applies sampling + redaction) → an EventCollectorInterface persists it → if detection is enabled, DetectionEngine evaluates rules against a window of recent events for the same subject → a matching Finding is correlated into an Incident by IncidentManager (new incident, or folded into an existing open one within the correlation window) → NotificationDispatcher applies the severity threshold and cooldown and sends through configured channels.

Commands

Command Does
watcher:install Publishes config + migrations, offers to run them.
watcher:status Reports whether Watcher is enabled, tables exist, and the dashboard gate is defined.
watcher:analyse Runs Psalm, PHPStan and composer audit (whichever are available) and persists findings. Findings are tracked across runs by a stable fingerprint: one missing from a run is marked resolved, and one that reappears after being resolved is flagged as regressed (and audited as a vulnerability_regressed event) rather than silently treated as new.
watcher:verify {finding} {url} Fires a real OWASP ZAP active scan — live attack payloads — at a URL you give it, to confirm whether a specific flagged finding is actually exploitable there. Disabled by default (watcher.verification.enabled); requires a ZAP instance you already have running (Watcher doesn't start or bundle one); the URL is never inferred, only ever the one you pass; prompts for confirmation in production unless --force.
watcher:discover Runs deterministic business-logic discovery and analysis — see Business Logic Intelligence.

Watcher's own PHPStan rules also include SensitiveAttributeMassAssignmentRule, which flags an Eloquent model whose $fillable includes a privilege-shaped column (is_admin, role, permissions, ...) — the concrete, buildable slice of "identify privilege escalation possibilities": a column like that being mass-assignable means a normal update()/create() call can grant it, not just an obviously-dangerous one.

Business Logic Intelligence

php artisan watcher:discover reads your application's routes, controllers and models — via Laravel's own Router, PHP reflection, real database schema introspection (Schema::getColumnListing()), and a static AST read of your source (via nikic/php-parser) — and never executes application business methods to figure out what they do.

What it builds:

  • A Business Rule Registry (watcher_business_rules table / Business Logic dashboard page): every route Watcher classifies as a sensitive business operation, with the evidence for that classification, its discovery method, and a confidence/verification status. Classification combines multiple signals (route wording, HTTP method, related model's columns, DB::transaction() wrapping) — never a single signal like a method name. Inferred classification is capped at Confidence::Medium; only an explicit developer attribute reaches Confirmed.
  • Developer-declared attributes — #[WatcherSensitive], #[WatcherOwnership('user_id')], #[WatcherTenantIsolation('tenant_id')], #[WatcherStateTransition(...)] — always take precedence over inference. If a declared attribute disagrees with discovered evidence (e.g. #[WatcherOwnership('account_id')] but the model has no such column), that's recorded as a conflict and surfaced as its own finding, never silently resolved.
  • One security analyzer, fully implemented: ownership/IDOR. For a sensitive operation whose route binds a model with an ownership/tenant column, Watcher checks whether an authorize()/Gate/can() call (or a can: route middleware) was detected. Finding its absence is reported as "no ownership check was found" at Low/Medium confidence — never as a confirmed IDOR vulnerability, since confirming that would require testing it against real data. Findings land in the existing watcher_findings table (tool = business_logic), through the same fingerprint-based open/resolved/regressed lifecycle watcher:analyse uses.

Deliberately not yet implemented (so nothing here is overclaimed):

  • Workflow/state-machine mapping beyond detecting that a status-like column exists. #[WatcherStateTransition] is catalogued but not yet security-analyzed.
  • The other five analysis categories from the original design: payment/financial integrity, approval-workflow security, state-transition integrity, data integrity, and race conditions. Race conditions in particular need reproducible concurrent testing before any claim — a static scan alone must never assert one is exploitable.
  • Automatic security test generation from discovered rules.
  • Five of the six demo domains originally envisioned (wallet transfer, payments, student results, tenant records, approvals, document uploads) — only the ownership/invoice pair is built, in the existing workbench/ example app.

Known limitations specific to discovery:

  • Middleware applied via an old-style $this->middleware(...) call inside a controller constructor is invisible — discovery reads Route::middleware() (route/group-declared only) rather than Route::gatherMiddleware(), deliberately, since the latter instantiates the controller.
  • A relationship method is only recognised if its body contains a direct $this->hasMany(...)/belongsTo(...)/etc. call Watcher's parser can see — a relation built through a trait method or a dynamically-constructed call isn't detected.
  • Discovery instantiates each related Eloquent model with new $class() (no arguments) to read $fillable/getTable() — the one narrow exception to "never execute business methods," since that constructor runs no I/O or business logic (the same thing php artisan model:show does).

Limitations

Documented honestly rather than glossed over — read these before relying on the corresponding feature:

  • Attribute enforcement only covers HTTP requests through a controller action, dispatched through a pipeline that includes EnforceSecurityAttributes (the web group, by default). It does not cover closure-based routes, and it cannot do anything for a method called directly from other PHP code — console commands, queued jobs, Tinker, or another controller calling it in-process. PHP attributes carry no runtime behavior of their own; there is no way around this without bytecode-level proxying, which Watcher does not do. A test (EnforceSecurityAttributesTest::test_calling_a_guarded_method_directly_bypasses_enforcement) proves this gap exists rather than papering over it.
  • CSRF protection on the dashboard's status-update endpoint cannot be demonstrated in this package's own test suite — Laravel's VerifyCsrfToken deliberately disables itself whenever app()->runningUnitTests() is true. The middleware is present (confirmed by inspecting the resolved route's middleware at test time); it just isn't something a feature test can prove.
  • Psalm's SSRF/file taint sinks are only cleared by an explicit @psalm-taint-escape annotation, not by ad-hoc validation logic — even genuinely-safe allow-list checks will still be flagged unless the validating function carries that annotation. See tests/StaticAnalysis/vulnerable-samples/SafeExamples.php.
  • Incident correlation uses a per-subject cache lock (Cache::lock()) to close the read-then-write race where two concurrent requests for the same subject could each open a duplicate incident. This only provides real cross-process protection on a cache driver that supports it (redis, memcached, database — not the array driver). True multi-process concurrency isn't reproducible in this package's single-process test suite; the lock's behavior (and its fail-safe skip when contended) is tested, the race itself is not.
  • Role/permission-change and API-token-lifecycle events have no automatic hook — no universal Laravel event exists for these without assuming a specific package (Sanctum, Passport, a permissions package). Call Watcher::record(...) manually from your own code for these.
  • "Unusual request pattern" is not implemented. A credible version needs a statistical baseline this package doesn't build yet; shipping a heuristic that looks like detection without being grounded in one would be worse than not having it.
  • There is no AI-assisted analysis module, and business-logic discovery is not a substitute for one. watcher:discover (see Business Logic Intelligence) is deterministic pattern-matching over routes/controllers/models/schema — it has no semantic understanding of what your application is supposed to do beyond what a developer attribute or a column name tells it, and it implements exactly one security analyzer (ownership/IDOR). It deliberately doesn't depend on an external LLM for anything. If you want a business invariant checked that isn't covered, Watcher\Core\Contracts\DetectionRuleInterface (runtime) or a #[WatcherSensitive]/#[WatcherOwnership] attribute (discovery-time) are the extension points — encode it yourself rather than expecting Watcher to infer it.
  • watcher:verify's ZAP integration could not be tested against a real ZAP instance in this environment (no running instance was available). The client (Watcher\Laravel\Verification\ZapClient) is written against ZAP's documented REST API shape and tested with mocked HTTP responses matching that shape — the same level of verification as the rest of the external-tool integrations where the tool itself can't run here, stated plainly rather than implied to be proven.

Security policy

See SECURITY.md.

Testing

composer test

Not vendor/bin/phpunit directly, if you've used any vendor/bin/testbench workbench command (building the example app, serving it, seeding it) in this checkout — those write compiled caches into the shared Testbench skeleton that composer test clears first (package:purge-skeleton). Running phpunit directly against a skeleton contaminated this way can produce confusing, resource-exhausting failures unrelated to your change. See CONTRIBUTING.md for the full story.

Test suites: Unit, Feature, StaticAnalysis (shells out to the real Psalm/PHPStan binaries — slower), BusinessLogic, Performance (prints measured overhead to stderr; not a pass/fail gate). Run one with vendor/bin/phpunit --testsuite <name> after a composer test run has cleared the skeleton.

Contributing

See CONTRIBUTING.md.

Changelog

See CHANGELOG.md.

License

MIT. See LICENSE.