Excubia documentation

Check a Laravel app for end of life, known vulnerabilities, abandoned packages and upgrade blockers, from the command line or in CI.

terminal
excubia.json
composer require --dev qiick/excubia-core
vendor/bin/excubia check
php artisan excubia:check

Getting started

Markdown

Introduction

Excubia is an open-source (MIT) health check for Laravel apps, part of the Qiick suite. The Composer package qiick/excubia-core reads your lockfiles and manifests and reports, in one place: PHP / Laravel / Node end of life, known vulnerabilities, abandoned and stale packages, blockers for the next Laravel major, and a suggested upgrade order. Run through Artisan, it also checks the posture of the booted app (debug mode, caches, migrations, failed jobs, scheduler, maintenance mode).

Optional, and off by default: push the report to your own self-hosted excubia-app, send runtime aggregates and masked exceptions to it, or serve an Oh Dear-compatible health endpoint. Nothing is sent to Qiick or any third party.

Status: 1.0.0-beta. The public contracts (CLI options, report JSON, check ids, exit codes, excubia.json, push payloads, health endpoint, SARIF ids, Action inputs) are frozen for 1.0.0-beta: docs/CONTRACTS.md. See docs/ROADMAP.md and docs/VERSIONING.md.

Requirements

RequirementNotes
PHP ^8.2To run Excubia. The scanned project may use any PHP version.
Composer 2composer.lock is required in the scanned project.
Laravel 12 or 13 (optional)Only for the Artisan commands and the opt-in features. Laravel 11 is not supported for the Artisan integration (D19); the standalone CLI still scans Laravel 11 projects and reports them as end of life.

Any Composer project can be scanned. Laravel-specific checks are skipped when laravel/framework is not locked.

Quick start

composer require --dev qiick/excubia-core
vendor/bin/excubia check

vendor/bin/excubia does not boot your app and never runs project code. In a Laravel app the package is auto-discovered, so the same checks plus the six posture checks run with:

php artisan excubia:check

Install without --dev (composer require qiick/excubia-core) when the deployed app runs Excubia itself: the scheduled push, runtime monitoring, exception capture, the health endpoint or posture checks in production. While those opt-ins are off, the package registers four Artisan commands and one every-minute scheduled heartbeat, and nothing else.

More examples:

vendor/bin/excubia check --format=json --output=report.json
vendor/bin/excubia check --format=markdown --output=report.md
vendor/bin/excubia check --fail-on=critical
vendor/bin/excubia check --only=php-eol,composer-advisories
vendor/bin/excubia check --skip=node-eol --no-dev
vendor/bin/excubia check ../other-project --offline
vendor/bin/excubia cache:clear
vendor/bin/excubia --version

Checks

IDNameData sourceFails or warns when (severity)
php-eolPHP end of lifeendoflife.dateSecurity support ended (critical); active support ended or security ends within 180 days (medium)
laravel-eolLaravel end of lifeendoflife.dateSecurity fixes ended (critical); bug fixes ended or security ends within 180 days (medium)
node-eolNode end of lifeendoflife.dateEOL (high); maintenance ends within 180 days (low). Skipped when no Node version is declared
composer-advisoriesKnown vulnerabilities (Composer)Packagist security advisoriesAn advisory affects the locked version (severity from the advisory)
npm-advisoriesKnown vulnerabilities (npm)OSV.devA vulnerability affects a version in package-lock.json (severity from OSV, default medium)
composer-abandonedAbandoned packagesPackagistAbandoned direct dependency (high); abandoned transitive dependency (medium)
composer-staleStale packagesPackagist, GitHubRepository archived (high); no release for more than 24 months (low)
laravel-next-blockerNext-major blockersPackagistThe latest release of a direct dependency does not allow the next Laravel major (medium), or only in a pre-release (low)
laravel-debug-modeDebug modeBooted appapp.debug on outside local / testing (high)
laravel-production-cachesProduction cachesBooted appConfig, route or event cache missing in production (low, one row per cache)
laravel-pending-migrationsPending migrationsMigratorMigrations have not run (medium)
laravel-failed-jobsFailed jobsFailed job providerFailed jobs exist (medium; high once the oldest is 24 hours old)
laravel-scheduler-heartbeatScheduler heartbeatDefault cache storeThe scheduler has not run for more than 10 minutes, or never (medium)
laravel-maintenance-modeMaintenance modeBooted appThe app is in maintenance mode (medium)
  • The six laravel-* posture checks after laravel-next-blocker need the booted app, so only php artisan excubia:check and excubia:push run them. vendor/bin/excubia reports them as skipped ("requires Artisan").
  • For laravel-scheduler-heartbeat, the service provider schedules php artisan excubia:heartbeat every minute. It stores the Unix time under the cache key excubia:scheduler-heartbeat in the default cache store. Keep the usual * * * * * php artisan schedule:run cron (or schedule:work), and use a shared cache store (database, Redis) when the scheduler and excubia:check run on different machines.
  • Every report ends with an ordered upgrade plan (upgrade-plan): security fixes, runtime EOL, blockers, Laravel major, replacements. It is info only, never changes the exit code and cannot be selected with --only / --skip.
  • Thresholds (180 days, 24 months, 10 minutes, 24 hours) are configurable in excubia.json. Full severity rules: docs/SPEC.md section 3.

Formats

--formatOutput
table (default)Human-readable, grouped by severity, coloured on a TTY (not with NO_COLOR or when writing to a file), summary line and upgrade plan
jsonVersioned report, schemaVersion 1, schema in docs/report-schema.json. Parse this, not the text formats
markdownFor pull requests, job summaries and AI agents
sarifSARIF 2.1.0 for GitHub code scanning, see CI guide

In the JSON report every skipped finding has a skipReason: data-source-failure, offline (--offline found no cached response) or not-applicable (the check does not apply or is disabled).

The JSON report also carries, as optional fields of schema version 1:

  • top level: generatedAt (UTC, Y-m-d\TH:i:s\Z), excubiaVersion (the value of --version) and project with php, laravel and node, each {version, cycle, securitySupportEndsOn, activeSupportEndsOn} or null when not declared (no name, path or hostname). The cycle and the Y-m-d dates come from the endoflife.date data the EOL checks already read, also when they pass; they are null when that check was skipped or did not run;
  • per finding: fingerprint (sha256 of checkId, subject and advisoryId joined by a NUL byte, the same value as the SARIF partialFingerprints, stable across runs), advisoryId on advisory findings, and details with check-specific values so you do not parse message:
checkIddetails keys
composer-advisoriespackage, lockedVersion, affectedVersions (Composer constraint), fixedVersions (list: the exclusive upper bound of each affected range)
npm-advisoriespackage, lockedVersion, fixedVersions (always empty for now: the OSV batch answer has ids only)
php-eol, laravel-eol, node-eolcycle, securitySupportEndsOn, activeSupportEndsOn (Y-m-d or null)
composer-abandonedpackage, lockedVersion, replacement (or null)
composer-stalepackage, lockedVersion
laravel-next-blockerpackage, lockedVersion, targetMajor (integer)
laravel-pending-migrationscount
laravel-failed-jobscount, oldestAgeHours (or null)
laravel-scheduler-heartbeatminutesSinceLastRun (null: never ran)
private packages row of the five checks that send namescount

laravel-debug-mode, laravel-production-caches, laravel-maintenance-mode and other skipped rows have no details. The exact rules are in docs/report-schema.json.

CLI

vendor/bin/excubia check [path] [options]
vendor/bin/excubia push [path] [--config=FILE] [--strict]
vendor/bin/excubia cache:clear [path] [--config=FILE]

path is the project directory to scan (default: the current directory).

OptionCommandsMeaning
--format=FORMATchecktable (default), json, markdown or sarif
--output=FILEcheckWrite the report to FILE instead of stdout. The only file Excubia writes in your project
--fail-on=SEVERITYcheckcritical, high (default), medium, low or none. A finding at or above it makes the exit code 1. Wins over failOn in excubia.json
--only=IDScheckComma-separated check ids to run. Unknown ids are an error
--skip=IDScheckComma-separated check ids to skip. With --only, applied to that selection
--no-devcheckIgnore require-dev / packages-dev. Same as "includeDev": false
--offlinecheckUse cached data-source responses only; checks without a cached response are skipped
--no-cachecheckDo not read or write the response cache
--config=FILEallConfig file (default: excubia.json in the project, if present). A missing explicit file is an error
--no-bannercheckHide the ASCII banner
--strictcheck, pushExit 3 when a data-source failure skipped a check; push then sends nothing. Same as "strict": true
-q, -v, -vvallQuiet, verbose, debug. From -v every HTTP request is listed on stderr
--versionPrint the Excubia version

The Artisan commands take the same options, without path (they always scan the app's base path):

Artisan commandOptionsWhat it does
php artisan excubia:checkas checkRuns every check, posture checks included
php artisan excubia:push--config, --strictRuns the checks and sends the report to your excubia-app (guide)
php artisan excubia:push-runtime--configSends the buffered runtime aggregates and exceptions (guide). Scheduled every minute when an opt-in is on
php artisan excubia:heartbeatStores the scheduler heartbeat. Scheduled every minute by the provider

Exit codes

Codecheckpush, push-runtime
0No finding at or above --fail-onSent, or nothing to send. Findings do not change it
1At least one finding at or above --fail-on—
2Usage or configuration error: missing or broken composer.lock, invalid excubia.json, unknown check id, bad option valueConfiguration error: missing URL or token, plain HTTP to a host other than localhost, invalid config
3Only in strict mode: a check was skipped because a data source failedThe app rejected the token, answered an error or could not be reached; in strict mode also a data-source failure (nothing is sent)

When several apply, 2 wins over 1, and 1 wins over 3, so CI sees real findings first.

Strict mode is --strict or "strict": true. A failed data source means the network was down with no cached response, or an API answered an error; the message on stderr names each check and data source. Without strict mode such checks are only marked skipped and the exit code ignores them. Deliberate skips never exit 3: --offline without a cached response, --only / --skip, Laravel checks on a non-Laravel project, a disabled check, or a row removed by an ignore rule. --strict can only turn strict mode on: it wins over "strict": false, and nothing on the command line turns "strict": true off.

Privacy

By default only package names and versions (and runtime versions) leave the machine, and only to the public data sources in the check table. No file contents, .env values, source code, paths or hostnames. Private packages (path, VCS, Satis, Private Packagist, file: or other-registry origins in the lockfile, plus privacy.skipPackages) are never sent and show as skipped. No telemetry. Each opt-in that sends data to your own excubia-app is listed field by field in the privacy guide.

Versioning and support

Excubia follows SemVer. Public contracts change only in a major; deprecated options keep working with a warning for at least one minor. In 0.x, minors may still break and only the latest minor gets fixes. Policy and supported versions: docs/VERSIONING.md. The list of public contracts, with their files, version fields and contract tests: docs/CONTRACTS.md; everything else (PHP classes, message wording, table and Markdown layout) is internal.

Guides