Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Security Advisories Health Check Laravel Package

spatie/security-advisories-health-check

A Laravel Health check that reports known security advisories for your installed PHP packages, sourced from Packagist, GitHub and other sources. Supports configurable retries, result caching, and ignoring specific packages to reduce API calls.

View on GitHub
Deep Wiki
Context7

Getting Started

This package is a plugin for spatie/laravel-health: it adds one check that asks Packagist whether any of your installed PHP packages have known security advisories. It is not a replacement for composer audit — it is a runtime signal you can surface on your health endpoint and in your monitoring.

Minimal steps:

composer require spatie/laravel-health spatie/security-advisories-health-check

Laravel Health must be installed and configured first (migrations for the history table, and ideally the dashboard/endpoint). Then register the check in a service provider — AppServiceProvider::boot() is the safe place (registering from register() was only fixed in 1.3.1):

use Spatie\Health\Facades\Health;
use Spatie\SecurityAdvisoriesHealthCheck\SecurityAdvisoriesCheck;

public function boot(): void
{
    Health::checks([
        SecurityAdvisoriesCheck::new()
            ->retryTimes(3)
            ->cacheResultsForMinutes(60),
    ]);
}

First use case: run the check once by hand before wiring it into anything. php artisan health:check gives you the result inline; if it's green, deliberately add a package with a known advisory on a throwaway branch to see the failure output, or read the check's result metadata to understand the shape of what it reports. Where to look first:

  1. The Laravel Health docs page for this check (available-checks/security-advisories) — the README is thin, the real docs live there.
  2. Spatie\SecurityAdvisoriesHealthCheck\SecurityAdvisoriesCheck source — it's one small class; read the run() method so you know exactly what it queries and what it puts in meta.
  3. Your composer.lock — that's what determines the outcome, not your composer.json constraints.

Implementation Patterns

Baseline production configuration. The defaults make a network call on every invocation, which is wrong for anything hit by an uptime monitor. A sane starting point:

SecurityAdvisoriesCheck::new()
    ->retryTimes(3)
    ->cacheResultsForMinutes(60)
    ->ignoredPackages([
        'some/abandoned-package', // advisory exists, no patched release available
        'legacy/thing',           // advisory doesn't apply to how we use it
    ]);

The retryTimes() matters more than it looks: Packagist is a public API and a transient hiccup or rate-limit response would otherwise flip your check to failed and page someone at 3am. Caching is the second half of the same problem — with a 1-minute monitor and no cache you'd be making ~43k requests a day.

Pick the cache window from the polling frequency, not from gut feel. If Oh Dear / Better Uptime hits /health every minute, a 60-minute window means you're checking advisories hourly and still responding instantly to the monitor. Pair that with a separate daily scheduled job that clears/refreshes the cache if you want a guaranteed fresh read. Note the cache uses your default cache store, so if the driver is array (common in some test/queue setups) caching silently does nothing.

Scope the check by environment. Registering this in local and in feature tests creates noise and API traffic for no benefit. Gate it:

if (app()->environment('production', 'staging')) {
    Health::checks([SecurityAdvisoriesCheck::new()->cacheResultsForMinutes(60)]);
}

If you want it locally, do it explicitly via a config('app.health.security_advisories') flag rather than the environment name.

Complement, don't duplicate, composer audit. composer audit runs at build time against your lock file and fails the build — perfect for CI. This package runs against what's actually deployed and feeds your runtime health/alerting pipeline. The useful pattern is: CI blocks merges on new advisories, and this check catches the case where a deploy is running an old lock file, or where an advisory was published after your last deploy. That gap is precisely the window it exists to close.

Extend the result metadata rather than re-fetching. Since 1.3.3 the stored advisory record is slimmed to actionable fields plus the affected version range that matches your installed version. If you want, say, the advisory title or link in your dashboard, subclass and widen:

class VerboseSecurityCheck extends SecurityAdvisoriesCheck
{
    protected array $advisoryFieldsInMeta = [
        // add back fields omitted by the default slimming
        'title', 'link', 'cve', 'affectedVersions',
    ];
}

Do this deliberately — the slimming exists because full Packagist records bloated the health history table.

Route the result into whatever you already use. Laravel Health supports notifications (mail/Slack) on failing checks; that's usually enough. If you use the history table for a status dashboard, be aware that the security check's metadata is the largest of any check, so give the history table pruning some thought.

Gotchas and Tips

Packagist connection failures are ignored, on purpose (1.2.1). A network error or Packagist outage reports the check as OK, not failed. That's the right trade-off for avoiding false alarms but it means a silent false negative: if your host can't reach Packagist (egress firewall, DNS, proxy), you'll get green forever. Verify once in each environment that the check actually reaches out — temporarily require a package with a known advisory, or check logs for outbound requests. This is the single most important thing to validate before trusting the signal.

Private and VCS-only packages are invisible. Advisories come from Packagist (sourced from GitHub and others), keyed by package name. Anything installed from a private repository, a path repository, or a git URL won't be matched. Don't assume the green check means "all dependencies are clean" — it means "all Packagist-known dependencies are clean."

Dev dependencies are likely not covered. The check reflects what's installed. With composer install --no-dev in production, require-dev packages aren't present and therefore aren't checked. That's usually fine (they don't ship), but don't expect parity with a local composer audit run.

Composer's | and || constraint syntax. Version-range matching had a real bug here until 1.3.3, which could produce wrong matches for advisories using single-pipe OR constraints. Pin ^1.3.3 or later if you're relying on the affected-version output; on earlier versions treat the reported affected range with suspicion.

serializable_classes=false breaks caching on 1.3.1/1.3.2-era code. If you've configured Laravel's cache to disallow object serialization (a common hardening step), older versions failed to cache because they stored objects. 1.3.2 moved to plain arrays. If caching appears to no-op, check both your cache driver and this setting.

Registration timing. Changelog 1.3.1 specifically fixed registering the check in register(). If you're on an older version and the check just doesn't appear, that's the cause — move it to boot().

retryTimes() is not a timeout. A retried, slow Packagist call can make /health slow enough to trip your monitor's own response-time threshold. If your health endpoint is on a tight budget, either raise the cache window so most requests are cache hits, or run this check from a scheduled command that writes into the health history rather than from the synchronous HTTP endpoint.

Never expose /health publicly without protection. The result metadata enumerates your installed package versions and known vulnerabilities — a genuinely useful reconnaissance feed. Use the middleware/auth options Laravel Health provides, or keep the endpoint internal-only and let an external monitor reach it over a private network.

Watch the changelog before upgrading. This package is small and fast-moving; 1.3.x changed the metadata contract ($advisoryFieldsInMeta) and the cache representation. If you've written a dashboard that reads specific meta keys, an upgrade can silently blank those columns. Subclass rather than reaching into the raw result, and pin a minor version in composer.json.

Testing it. Don't let real HTTP calls into your test suite. Fake the Packagist endpoint with Http::fake() and assert on the check result, or test your subclass's metadata mapping in isolation. If you're asserting on the health endpoint in a feature test, remember the cache will leak between test cases unless you flush it — use the array driver and clear per test.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle