Search by

porro / filament-connector

Porro

Site health, suspicious activity and IP blocking for Filament 3–5, in your panel: security advisories, end-of-life PHP and Laravel, Filament and Laravel updates and the scheduler, plus probes, failed logins and form floods, blocked in the app, at Cloudflare or in nginx. Free; connect Porro to add up


README

Site health for Filament, free in your panel: health, activity and blocking, beside the real Porro Site Health page

A Porro Site Health page for Filament 3, 4 and 5 panels. It checks the app for the things that quietly go wrong between projects (security advisories in your Composer packages, PHP and Laravel versions running out of security support, Filament, Livewire and Laravel updates, a scheduler that never runs) and tells you what to do about each.

It's free and works on its own. If you use Porro to look after your sites, connecting it adds what can only be seen from outside: uptime, SSL certificates, domain expiry and the server's operating system.

The Porro Site Health page in a Filament panel on a site that isn't connected to Porro: a migration not run, the scheduler not running and three failed jobs, with the commands that fix them

Install

composer require porro/filament-connector

Add the plugin to the panel you want it in, usually your admin panel:

use Porro\Filament\PorroPlugin;

public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->plugin(PorroPlugin::make());
}

That adds a Porro group to the navigation with Site Health and Blocking, plus a lazy-loaded dashboard widget. Run php artisan migrate for the blocklist and activity tables. No assets to publish or build.

The Porro Site Health dashboard widget

Options

PorroPlugin::make()
    ->widget(false)                 // no dashboard widget
    ->navigationGroup('Settings')   // instead of "Porro": the pages become "Porro Site Health" and "Porro Blocking"
    ->navigationSort(90)
    ->authorize(fn ($user) => $user->can('view site health'))
    ->authorizeManage(fn ($user) => $user->can('block ips'));   // who can block and safelist

Who can see it

In order:

  1. Your authorize() callback, if you give one.
  2. A viewPorroSiteHealth gate, if your app defines one: Gate::define('viewPorroSiteHealth', fn (User $user) => $user->is_admin);
  3. Admins, when your users have the usual markers: an isAdmin() or isSuperAdmin() method, an is_admin attribute, or a spatie/laravel-permission super_admin or admin role.
  4. Otherwise, nobody. If your users have none of those markers, the page and widget stay hidden until you say who can see them, with a gate in a service provider's boot():
use Illuminate\Support\Facades\Gate;

Gate::define('viewPorroSiteHealth', fn (User $user) => in_array($user->email, ['ops@example.com']));

or with authorize() on the plugin:

PorroPlugin::make()->authorize(fn (User $user) => $user->hasVerifiedEmail() && str_ends_with($user->email, '@youragency.com'));

To let everyone who can use the panel see it, use ->authorize(fn () => true). Only do that on a panel for your own team, never a customer-facing one.

The pages, the widget and Check again all check this on the server.

Who can block and safelist

Whoever can see the pages, unless you narrow it. In order:

  1. Your authorizeManage() callback.
  2. Otherwise, a managePorroBlocklist gate.

Everyone else sees the lists but no buttons, and the server refuses their changes too.

Suspicious activity and blocking

Free, and on the app itself. IP addresses aren't sent to Porro, and only leave the server in the cases listed under Where IP addresses go: Cloudflare and reverse-DNS lookups on an IP's details page, the AbuseIPDB button, and blocking at Cloudflare. The rules are the Laravel connector's, so its README has every setting.

Site Health in a Filament panel: the suspicious activity summary and the IPs worth a look, each with Block 7 days, Block permanently and Safelist buttons

  • What's watched: probes for files like /.env, /.git and wp-login.php, failed logins (from Laravel's Failed auth event, which the panel's login fires; token guards and custom guards that don't fire it aren't counted), bursts of 404s and form floods. Counts are kept for 48 hours in Porro's own porro_cache table, so a deploy's cache clear doesn't wipe them, and usernames only as hashes.
  • Suspicious requests are batched: each one appends a line to storage/porro/activity.buf on its server, and the lines are recorded every minute by the scheduler, now and then at the end of a suspicious request, and whenever an activity page opens. Flags can lag by up to a minute; blocking is immediate. Where storage can't be written, or on Lambda (Vapor), each request is recorded straight away. PORRO_ACTIVITY_BUFFER=false turns batching off.
  • Site Health lists the IPs worth a look, worst first, with badges for Tor exits, Spamhaus DROP, hosting networks and research scanners, and Block 7 days, Block permanently and Safelist buttons. Each one asks first, in a confirmation that says what will happen and for how long.
  • Exposed files: if /.env or /.git ever answers, that's raised as Act now.
  • IP details: click an IP for its network (from Cloudflare's API when PORRO_CLOUDFLARE_TOKEN is set, which sends Cloudflare the IP), a reverse-DNS hostname checked both ways, what it did here over 48 hours, and a plain note when something claiming to be Googlebot or Bingbot isn't. With PORRO_ABUSEIPDB_KEY, a button checks AbuseIPDB; it sends the IP to AbuseIPDB, so it only runs when clicked.

IP details in a Filament panel: an address pretending to be Googlebot, caught by its hostname, with buttons to block it or its range

  • The Blocking page: where blocks are enforced, the newest of the blocklist and safelist (each searchable on its own page, 50 at a time), and what's always protected.
  • Where blocks are enforced:
    • In the app: blocked IPs get a plain 403 before the app runs. On by default; PORRO_BLOCK_APP=false turns it off.
    • At Cloudflare's edge: open Blocking → Set up Cloudflare and follow three steps. Create a token with Zone WAF Rules: Edit and Zone: Read on the site's domain, plus Account Rule Lists: Edit on the account. Set it as PORRO_CLOUDFLARE_TOKEN, then press Connect (or run php artisan porro:cloudflare connect). It adds one rule next to your existing ones, and only production syncs. Blocked IPs and ranges go into the account's porro_blocklist list with their notes. The list belongs to the whole Cloudflare account, so every site you connect on that account shares its blocks.
    • In nginx: PORRO_BLOCK_NGINX=true, then Blocking → Set up nginx walks through three steps with this site's real paths and shows how far each server has got.
  • Always protected: Cloudflare, Laravel Forge and its health checks, Google's and Bing's published crawler ranges, and private addresses: a block that covers any of them is refused when you try to add it, and the app never turns them away. Open each on the Blocking page to see its ranges and why. Not on Forge? PORRO_PROTECT_FORGE=false. Happy to block a crawler? PORRO_PROTECT_CRAWLERS=false. Blocks of your own IP or very wide ranges are refused too.

The Blocking page in a Filament panel: blocking in the app, at Cloudflare and in nginx, above the blocklist

  • Production only: activity is recorded in production only (PORRO_ACTIVITY=true to record elsewhere). Blocks apply everywhere.
  • Country: comes from Cloudflare when the site is behind it. Otherwise it comes from DB-IP's free country file, downloaded monthly to storage/porro.

What it checks

On the app itself, with no account:

Check How
Security advisories in composer.lock Packagist's security advisories API, the one composer audit uses. Only package names are sent; versions are matched on your server. High and critical advisories show as Act now.
PHP and Laravel support Past, or within 90 days of, the end of security support, from endoflife.date. A copy ships with the package for when it can't be reached.
Updates Newer Filament, Livewire and Laravel releases on the major versions you run, from Packagist
Scheduler Whether schedule:run is running, from a heartbeat the package schedules every 15 minutes, and whether the crontab has the line. php artisan porro:cron adds it.
Debug mode, migrations, failed jobs APP_DEBUG on in production, migrations not run, failed jobs in the last 24 hours
Backups and health checks Stale backups from spatie/laravel-backup and failing spatie/laravel-health checks, if you use them

Results are cached for five minutes, and the public data behind them for between six hours and a day. Check again runs everything now.

Connect Porro (optional)

Porro watches every site you look after from outside: uptime from several regions, SSL and domain expiry, the server's OS and security updates, with alerts by email or Slack. It's a separate service with its own account; see porro.dev for plans. Nothing in this package needs it, and nothing is sent to Porro until you connect. Add the site in Porro, then:

php artisan porro:connect <site key>

The page then shows the site's open findings in Porro alongside the checks above, each linking to Porro.

Porro Site Health connected to Porro, in dark mode: an end-of-life server, an expiring certificate and domain, and a slow region, above the checks run on the site

Connecting also sends Porro a signed report every 15 minutes on Laravel's scheduler: Filament, Livewire, plugin and package versions, each panel's id, path, whether it has a login and (from Filament 4) whether multi-factor authentication is required, plus the health facts above. Never content, users or settings values. Run php artisan porro:status --json to see exactly what would be sent, and see the Laravel connector for config and commands.

porro:connect offers to add the scheduler's cron line; php artisan porro:cron adds it on its own and, once the site is connected, sends a report straight away. See Scheduling reports to add it on Forge, Ploi, Laravel Cloud and other hosts.

Scheduling reports

Laravel's scheduler needs to run php artisan schedule:run from the project folder every minute. Reports, the scheduler check and activity batching all run on it.

php artisan porro:cron adds the line to the server's crontab for you, or tells you it's already there. Once the site is connected it also sends a report straight away, so Porro stops saying cron is missing without waiting for the first scheduled one. If it can't reach the crontab, it prints the line to add by hand, using one of the guides below.

Zero-downtime deploys (Forge, Envoyer, Ploi, Deployer): always point cron at the current folder, e.g. /home/forge/example.com/current, never a releases/… folder. A release folder runs old code after the next deploy and stops working once it's cleaned up, with no error you'd see. php artisan porro:cron uses current for you.

Laravel Forge

Open the site and turn on Laravel scheduler in its Laravel settings. That's all: Forge adds the every-minute schedule:run job for the site, from the right folder.

To add it by hand instead, go to the server's Scheduler and add a New scheduled job:

  • Command: php /home/forge/example.com/current/artisan schedule:run. Leave out /current if the site doesn't use zero-downtime deploys. On an isolated site, use the site's own user in the path.
  • User: forge, or the site's isolated user. Never root.
  • Frequency: every minute.

If you add sites through Porro's one-click install on Forge, it turns on the Laravel scheduler for you when the Forge API token has the optional server:create-schedulers scope.

Ploi

Open the server's Cronjobs and add one:

  • Command: php /home/ploi/example.com/artisan schedule:run, with /current before /artisan on zero-downtime sites.
  • User: ploi, or the site's own user.
  • Frequency: every minute.
Laravel Cloud

There's no crontab. Open the environment, edit the App cluster and turn on Scheduler. Cloud then runs schedule:run every minute. Set PORRO_KEY as an environment variable rather than running php artisan porro:connect there.

Laravel Vapor

Vapor runs the scheduler for you unless vapor.yml sets scheduler: false. Leave it on, and set PORRO_KEY as an environment variable.

Envoyer

Envoyer deploys but doesn't run cron. Add the job on the server itself (Forge's Scheduler, or crontab -e), pointing at Envoyer's current folder: php /home/forge/example.com/current/artisan schedule:run.

cPanel or Plesk
  • cPanel: Advanced → Cron Jobs. Choose Once Per Minute and use cd /home/user/example.com && php artisan schedule:run > /dev/null 2>&1.
  • Plesk: Websites & Domains → Scheduled Tasks → Add Task. Choose Run a command, Cron style * * * * *, and the same command.

If php isn't found, or runs an older PHP than the site, use the full path your host lists for the site's PHP version.

Any other server (crontab)

Run crontab -e as the user that owns the site (not root) and add:

* * * * * cd /path/to/site && php artisan schedule:run > /dev/null 2>&1

Use the current path on zero-downtime deploys.

Requirements

  • Filament 3.2, 4 or 5, Laravel 10–13 (Laravel 10 with Filament 3), PHP 8.2+
  • Outbound HTTPS to packagist.org, repo.packagist.org and endoflife.date (and app.porro.dev once connected). Without it, the checks that need them say so and the rest still run.

Demo

Dog-ear Books is a demo app with the plugin set up and things for it to find, a stand-in Porro for the connected view, and the end-to-end checks and screenshot script behind these images. docs/images/filament-thumbnail.png is the plugin directory's 2560 × 1440 thumbnail.

External requests

To When What's sent
packagist.org Advisory check, at most every 6 hours or when composer.lock changes Package names from composer.lock
repo.packagist.org Update check, at most twice a day per package Nothing beyond the request
endoflife.date Support dates, at most once a day Nothing beyond the request
app.porro.dev Only once connected: reports every 15 minutes, findings when the page is open (cached 5 minutes) Signed with the site key
www.cloudflare.com, forge.laravel.com Their published IP ranges, once a day Nothing beyond the request
developers.google.com, www.bing.com Google's and Bing's published crawler ranges, once a day Nothing beyond the request
check.torproject.org, www.spamhaus.org The Tor exit and Spamhaus DROP lists, when an IP's details page opens (cached an hour and a day) Nothing beyond the request
api.cloudflare.com Only with PORRO_CLOUDFLARE_TOKEN: syncing blocks, and opening an IP's details page (cached a day per IP) Blocked IPs and ranges with their notes, and the IP being looked at
api.abuseipdb.com Only with PORRO_ABUSEIPDB_KEY, when someone clicks the reputation button The IP being checked
download.db-ip.com The free country file, once a month, where activity is recorded Nothing beyond the request

Support

Questions and bugs: GitHub issues. Security issues: please email security@porro.dev rather than opening an issue. We reply within one working day.

Developing

composer install
composer test

License

MIT. See CHANGELOG.md for what changed.