# PHP supply chain: scan 6,000+ advisories every night

> Online audit of composer.json/lock for PHP: CVE with CVSS, EOL frameworks, abandoned packages. Dataset of 6,000+ advisories rebuilt nightly. No upload.

Free in-browser tool (no upload): https://www.mauriziofonte.it/tools/composer-security-audit

Paste a **composer.json** or **composer.lock** and get in thirty seconds a security-first audit of the manifest: **active CVEs** on installed versions (with CVSS vector and CWE IDs), **abandoned packages** with suggested replacement, **end-of-life frameworks** with security support end date, **PHP constraint** audit, detection of wildcard constraints, dev-branch dependencies, non-Packagist repositories, enabled Composer plugins. Dataset of 6,000+ advisories recompiled nightly from Packagist Security API, OSV.dev and endoflife.date. The analysis runs entirely in the browser: your manifest never leaves the page.

## How to use the audit

1. **Get composer.json or composer.lock** - Both files live at the project root, next to `vendor/`. **composer.json** declares dependencies and version constraints (e.g. `"laravel/framework": "^11.0"`): it is committed to git and visible on the repository (GitHub, GitLab, Bitbucket). **composer.lock** is generated by `composer install/update` and contains the exact resolved versions (e.g. `11.5.0`) plus integrity hashes: it is also committed to git to guarantee deterministic builds between dev and production. For an accurate audit prefer the `composer.lock`: the manifest declares what is allowed, the lock declares what was actually installed.
2. **Paste in the tool** - The tool auto-detects the type: a top-level `packages` array identifies the lock, a `require` object with string constraints identifies the json. JSON parsing is strict: syntax errors fail with explicit diagnostic. Input never leaves the browser: no network request carries your manifest, the analysis runs entirely in local JavaScript against a static dataset preloaded from the site.
3. **Read the report** - Two-tier output. **Statistics**: total package count, require / require-dev breakdown, PHP constraint, required extensions. **Severity-ordered findings** (critical, high, medium, low, info): for every issue you get the affected package, motivation, link to the public CVE record (cve.org) or to the GitHub Security Advisory (GHSA), CVSS vector when available, list of associated CWEs, vulnerable constraints and installed version, remediation hint. At the top: aggregated score 0-100 (green above 80, yellow 50-80, red below 50).
4. **Then run the real audit (CLI)** - Static triage in the browser gives you a snapshot today. To integrate into pipelines and block future regressions: `composer audit` from terminal (official command since Composer 2.4, September 2022) compares installed packages against the FriendsOfPHP CVE database. `composer require --dev roave/security-advisories:dev-latest` adds a meta-package that blocks installation of packages with known CVEs (zero runtime overhead, only conflict declarations). Best practice: `composer audit --abandoned=report` inside the test job of your CI/CD, exit code != 0 if active CVEs are found.

## Known vulnerabilities, EOL, abandoned: the three real PHP supply chain risks

**PHP applications don't die of old age, they die of skipped maintenance.** In every uncared-for Composer project three things happen silently: new CVEs get published against the actually-installed versions, authors stop maintaining libraries and mark packages as `abandoned`, major framework branches drop out of security support. After eighteen-twentyfour months of immobility, the application formally works but is exposed to vulnerabilities that have been known and patched in newer versions. This tool surfaces that state in thirty seconds from the manifest alone.

**What the audit actually checks.** Three independent sources feed the triage. (1) **Known vulnerabilities (CVE)**: the Packagist Security Advisory API aggregates FriendsOfPHP/security-advisories and the GitHub Advisory DB; OSV.dev (Open Source Vulnerabilities, maintained by Google) enriches each advisory with CVSS v3 vector and CWE IDs. Coverage: 6,000+ advisories on 1,000+ Composer packages. (2) **End-of-life (EOL)**: endoflife.date publishes the release cycles of the main frameworks with end-of-security-support dates. Monitored: PHP, Laravel (and `illuminate/*`), Symfony (and every `symfony/*`), Composer itself, Guzzle, Twig, CakePHP, Drupal, GrumPHP. (3) **Abandoned**: the `abandoned` flag from Packagist is read for the union of all packages mentioned in advisories and the top 2,000 popular packages, with a 7-day cache TTL. Result: 210+ packages flagged as actually abandoned in the dataset.

**Difference between auditing the lock and auditing the json.** The `composer.lock` contains exact resolved versions: the matcher does an exact lookup against the advisory's affected interval (e.g. `>=9.0.0,<9.5.6`). The `composer.json` contains only the declared constraint (e.g. `^9.0`): the matcher computes the intersection between the user constraint and the affected interval, flagging if any installable version is vulnerable. For rigorous audits use the lock. The json is useful to assess *whether the constraint would permit* downloading vulnerable versions on a fresh `composer install` tomorrow: triage flags it, but truth lives in production.

**The constraint matcher is precise.** It does not stop at major-version comparison: it implements caret `^X.Y.Z` with the major-zero exception (`^0.5.0` = `>=0.5.0 <0.6.0`, `^0.0.5` = `>=0.0.5 <0.0.6`), tilde `~X.Y`, wildcard `X.Y.*`, hyphen range `1.0 - 2.0`, operators `>=` `<=` `>` `<` `=`, OR `||` and AND comma-or-whitespace, stability suffixes `-dev` `-alpha` `-beta` `-RC` with lexicographic priority. Normalization is in DNF (disjunctive normal form) of intervals and constraint-vs-constraint intersection is pairwise overlap: same semantics as `composer/semver` PHP, in vanilla JS, with no dependencies.

**Operational privacy.** The `composer.lock` reveals the full structure of a project, its transitive dependencies, and any private packages with references to internal VCS repositories. Uploading it to a third-party service for preliminary analysis is not a good idea, even over TLS: every third party is a leak vector for your dependency map. Here no request leaves with manifest data. The dataset (16 advisory shards + 1 EOL file + 1 abandoned file) is downloaded by the browser only on first use and cached in `localStorage` for subsequent sessions (automatic invalidation via SHA-256 over the public files). Audit, parsing, matching: all local.

**When static triage is the right level.** Typical scenarios. (a) Preliminary review of a legacy project without filesystem access: the client pastes the manifest in an email and wants an assessment in thirty seconds. (b) Pre-engagement audit to estimate the cost of a possible remediation contract before quoting. (c) Initial screen before a serious cyber assessment: define the list of high/critical to deepen with exploitability assessment, mapping of applicable vectors, reachability of the codepaths. (d) Periodic self-check by the application owner who wants to know whether the maintainer is doing the work.

**Scope limit.** The tool flags published CVEs, known EOL, declared abandonment. It does not replace enterprise Software Composition Analysis (Snyk, Mend, Sonatype): no license check, no SBOM CycloneDX generation, no analysis of non-Composer code (system binaries, Node scripts, container base images). It does not replace `composer audit` CLI integrated into pipelines. It does not replace a manual assessment that evaluates real exploitability of vectors, presence of codepaths in installed versions, mitigations applied at hardening level. It is a triage and presents itself as one.

## Glossary

- **composer.json**: PHP project manifest file. Declares name, author, dependencies (`require`, `require-dev`), version constraints (e.g. `^11.0`), PSR-4 autoload. Committed to git, it is the authoritative document on what the project declares it wants to install.
- **composer.lock**: File auto-generated by `composer install/update`. Contains exact resolved versions of every package + content-hash + timestamp. Guarantees deterministic builds between dev and production. Committed to git (best practice). For precise security audits it is the authoritative source: the manifest says what is allowed, the lock says what is actually installed.
- **Composer version constraint**: Syntax to declare which versions of a dependency are acceptable. `^1.2` = SemVer compatible (`>=1.2 <2.0`, special-cased for major 0). `~1.2.3` = patch-level (`>=1.2.3 <1.3.0`). `1.2.*` = wildcard. `>=1.2,<1.5` = AND of operators. `^1.0 || ^2.0` = OR of ranges. `1.0 - 2.0` = hyphen range. Best practice: `^X.Y` for stable dependencies, `~X.Y.Z` for conservative dependencies where you only want patches.
- **CVE (Common Vulnerabilities and Exposures)**: Unique identifier of a public vulnerability in the form `CVE-YYYY-NNNN`. Assigned by the responsible CVE Numbering Authority (CNA). Authoritative database: [cve.org](https://www.cve.org). Every CVE relevant to the Composer ecosystem maps to Packagist Security and/or GitHub Advisory.
- **CVSS (Common Vulnerability Scoring System)**: Standard system for scoring CVE severity. Current version: 3.1 (4.0 in progressive adoption). Output: **vector string** (e.g. `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H`) and **base score** 0.0-10.0. Severity bands: 0.1-3.9 low, 4.0-6.9 medium, 7.0-8.9 high, 9.0-10.0 critical.
- **CWE (Common Weakness Enumeration)**: Taxonomy of software weakness classes. Identifiers in the form `CWE-NNN` (e.g. `CWE-79` Cross-Site Scripting, `CWE-89` SQL Injection, `CWE-502` Deserialization of Untrusted Data, `CWE-200` Information Disclosure). A CVE may have multiple CWEs associated. Useful for pattern matching during review and for team training.
- **GHSA (GitHub Security Advisory)**: GitHub-native security advisory identifier, format `GHSA-xxxx-xxxx-xxxx`. GitHub maintains a database parallel to CVE with often faster triage for open source ecosystems. Each GHSA can map to a published CVE (mapping exposed in the advisory's `cve` field).
- **PSA (Packagist Security Advisory)**: Packagist identifier in the format `PKSA-xxxx-xxxx-xxxx`. Packagist aggregates FriendsOfPHP/security-advisories and Composer-relevant GHSAs into a unified API endpoint.
- **EOL (End Of Life)**: Date beyond which a package or framework no longer receives security updates. Keeping EOL components in production is a risk: new vulnerabilities are not patched on the branch and the only path is the upgrade. In NIS2 scope (art. 21(2)(g)) this is explicitly flagged as 'inadequate cyber hygiene'.
- **LTS (Long Term Support)**: Branch of a framework or language that receives security updates for an extended period (typically 2-5 years) even after the next version is released. Examples: historical Laravel LTS (5.5, 6.x), Symfony LTS (4.4, 5.4, 6.4), PHP has no formal LTS but every minor receives security updates for about 2 years.
- **Abandoned**: State declared by the maintainer in `composer.json` with the `abandoned` field (boolean `true` or string with the suggested replacement name). Packagist exposes this flag in the package's public JSON. Not to be confused with deprecated: a deprecated package is still maintained but replaced by a newer version, an abandoned package is declared out of maintenance by its author.
- **FriendsOfPHP/security-advisories**: Collaborative open source database of security advisories for Composer packages. Updated daily, YAML format, GitHub repository. Historical basis of the `roave/security-advisories` meta-package and of the Packagist Security Advisory API.
- **OSV.dev**: Open Source Vulnerabilities, database maintained by Google. Unified OSV schema, bulk dump per ecosystem (Packagist, npm, PyPI, Go, Maven, etc.). For each vulnerability: aliasing between CVE/GHSA, CVSS v3 severity, CWE IDs, affected ranges per package version. Enrichment source for this tool's dataset.
- **endoflife.date**: Open source database of release cycles for languages, frameworks, runtimes and tools. Public JSON API at `/api/{product}.json`. For each cycle: release date, end of active support, end of security support, LTS flag, latest patch release. Source of this tool's EOL dataset.
- **roave/security-advisories**: Composer meta-package (no code, just conflict declarations) that blocks installation of packages with known CVEs. Usage pattern: `composer require --dev roave/security-advisories:dev-latest`. Zero runtime overhead, zero impact on build size: conflicts are evaluated only at the dependency manager solve time.
- **composer audit**: Official Composer command available since 2.4 (September 2022). Compares installed packages against the Packagist Security CVE database and produces text or JSON output. Exit code != 0 if active CVEs are present. To integrate in CI/CD test jobs to block PRs that introduce security regressions.
- **Renovate / Dependabot**: Automated bots (native GitHub Dependabot, multi-platform Renovate Bot) that monitor dependencies and open update PRs. For PHP-Composer: native support, recognize security advisories and prioritize security PRs separately from minor/patch updates.
- **Supply chain security**: ICT security discipline focused on third-party dependencies: open source libraries, package registries, mirrors, build runners, deploy artifacts. NIS2 scope (art. 21(2)(d)) explicitly includes ICT supply chain as a risk-management subject. Operational tools: SBOM (Software Bill of Materials), deploy signing, continuous CVE monitoring, update policies with SLAs.

## Frequently asked questions about Composer audit

### I'm the owner or project manager of a PHP application, not a developer. Do I really need this tool?

Yes, and you are in fact one of the targets. The tool's logic is written to be readable without deep PHP knowledge: for every finding you get severity (critical, high, medium, low, informational), affected package name, explicit motivation, link to the public CVE or GitHub Advisory. If the report returns more than three or four high or critical findings on production dependencies, you have a concrete argument to bring to the development team and ask how and when the application will be updated.

### Where do I actually find the composer.json or composer.lock of my application?

Both live at the project root, next to the `vendor/` folder. Three typical retrieval paths. (1) Git repository: ask for access to the repository (GitHub, GitLab, Bitbucket) and you find them at the root, they are committed text files. (2) Production server: with SSH access they live at `/var/www/<project-name>/composer.json` or similar. (3) Shared hosting: usually accessible via FTP/SFTP or control panel. In no case do these files contain sensitive application data (users, passwords, transactions): they only contain the library list and their versions.

### The report flags a package as EOL and my developer says 'it's not urgent'. How do I respond?

EOL means the maintainer DOES NOT release security updates on the branch in use. New vulnerabilities are not patched for that version. The date is public, with no subjective estimate involved. The correct answer from a competent developer is 'I'm planning the upgrade by date X' or 'the vector is not applicable to our scenario because of Y, and I document it in writing', not 'it's not urgent'. In NIS2 scope (art. 21(2)(g), enacted in Italy through Legislative Decree 138/2024) keeping EOL components in production is explicitly treated as a signal of inadequate cyber hygiene.

### Difference between auditing the lock and auditing the json?

The `composer.lock` contains the exact resolved versions: the tool knows you installed exactly `9.5.10` and can tell you whether that specific version is affected by a CVE. The `composer.json` contains only declared constraints (`^9.0`): the tool does not know the actual version, but it can tell you whether the constraint would permit installing vulnerable versions. For precise audits with immediate operational value use the lock; the json is useful to assess whether a fresh `composer install` tomorrow could pull affected versions.

### Does the tool flag specific CVEs per package?

Yes. The dataset fed by Packagist Security API + OSV.dev contains 6,000+ published advisories with CVE identifier when assigned, GitHub Security Advisory (GHSA), severity, CVSS v3 vector when enriched by OSV, associated CWE list, exact affected constraint. For every match: direct link to the CVE Record or GitHub Advisory page. The dataset is recompiled nightly to stay aligned with new publications.

### Is the version matching precise or just approximate?

Precise on the lock. The `ComposerSemver` module implements the same semantics as `composer/semver` PHP: caret, tilde, wildcard, hyphen range, OR/AND, stability suffix, major-zero exception. On the lock it does exact lookup (concrete version vs affected interval). On the json it computes intersection of intervals normalized in DNF: a finding is emitted when at least one installable version is covered both by the declared constraint and by the vulnerable interval. The module is validated by a 44-fixture test suite (manually runnable from console: `ComposerSemver.runFixtures()`).

### How many packages can I audit at once? Performance?

Performance is OK up to 1,000 packages in a single audit. The dominant cost is the advisory shard fetch (up to 16 files of ~100 KB each, gzipped to ~25 KB). With a warm cache the audit is instant (parsing + matching on a few hundred packages = milliseconds). For enterprise projects with 1,000+ deps: `composer audit` CLI is faster and more accurate (reads `vendor/composer/installed.json` directly). Our tool is optimized for typical projects between 30 and 300 deps, the 80% of consulting/SMB projects.

### What happens with a package not in the dataset?

It's counted in the general statistics but produces no finding. Unknown to the dataset does not mean dangerous: most Composer packages are niche or private and have never been the subject of a public advisory, published EOL or abandoned flag. For wider coverage on niche packages: `composer audit` CLI reads the full FriendsOfPHP CVE database, and Snyk/Dependabot integrated in the repository monitor continuously.

### The project's PHP constraint is `>=7.4`. What does the tool flag?

It's flagged as `php_eol`: the constraint includes PHP 7.4 (security support ended 2022-11), 8.0 (ended 2023-11), 8.1 (ended 2025-12). It means `composer install` would formally accept installation on a PHP 7.4 machine even if production reality is 8.x. Best practice: `>=8.2` minimum in the constraint, regardless of the current production version. Communicates intent + prevents accidental installs on legacy VPS + is a cyber hygiene signal for external audits.

### Symfony has many standalone components (`symfony/console`, `symfony/yaml`, etc.). Are they all covered?

Yes. The tool considers every `symfony/*` package with the same LTS policy as the main release (Symfony 5.4 LTS = `symfony/console:5.4` LTS, etc.). The mapping is via `symfony/` prefix in the `product_prefixes` of eol.json. Same pattern for `illuminate/*` mapped to the Laravel cycle. Rare exceptions (components with cycles slightly different from the main release) are not modeled to avoid false positives on marginal corner cases.

### Does the tool integrate any external PHP/JS library at runtime?

No. JSON parse is native, the constraint matcher is the `composer-semver.js` module written from scratch, dataset fetch uses the native `fetch` API, sharding uses native `crypto.subtle.digest('SHA-1')`. Zero dependencies, total ~13 KB of JavaScript (3 KB minified gzip for semver, 10 KB gzip for the GUI). The dataset itself is static, recompiled by the server-side PHP cron: the browser only downloads the shards needed for the manifest's packages, not the entire dataset.

### Difference between abandoned, deprecated and EOL?

**Abandoned**: the maintainer has explicitly marked the package as abandoned in `composer.json` (`abandoned` field read from Packagist). **Deprecated**: the package is still maintained but replaced by a newer version (e.g. `league/flysystem 1.x` deprecated in favor of `2.x`). **EOL**: the maintainer no longer releases security updates on the branch (e.g. Symfony 5.4 after November 2025). All three require migration planning; abandoned and EOL on production branches are the most urgent.

### Can I use the tool on a library's composer.json, not an application's?

Yes, it works the same. Libraries typically have fewer packages (10-30) and are more conservative on backwards compatibility. Typical library findings: too-permissive PHP constraint (`>=7.0` to support legacy downstream), version pinning of EOL test frameworks, dependencies on old abandoned `doctrine/cache`. Useful audit before publishing a new major release or closing the scope of a long-term version.

### How do I integrate the real audit into CI/CD?

GitLab CI: `composer audit --abandoned=report --format=json` in the test job, exit code != 0 fails the merge. GitHub Actions: same command, optionally with `tj-actions/composer-audit` action. Pre-commit hook: `composer audit` in `.husky/pre-commit` or `.git/hooks/pre-commit`. Best practice: also run the audit on release tags, not only on PRs, to catch CVEs published between merge and deploy. To block install of vulnerable packages: `composer require --dev roave/security-advisories:dev-latest`.

### Is the dataset updated in real time?

Almost: the cron recompiles it nightly at 03:00 UTC. Typical latency between a new CVE published on Packagist and visibility in the tool: less than 24 hours. For zero latency you need `composer audit` CLI integrated into the pipeline, which queries the Packagist API at execution time. The choice of nightly rebuild is a trade-off between freshness and network cost (the job makes ~2,800 Packagist calls at steady state).

### Privacy: is the manifest I paste stored or tracked?

No. The tool runs entirely in the browser. The only outgoing network request is the GET of the static dataset files, identical for every user, fully anonymous. The pasted `composer.json` or `composer.lock` is never sent in any network request, never saved in `localStorage`, never tracked in analytics. You can verify by opening the DevTools Network panel during the audit: zero outgoing requests with manifest data.

---
Tool by Maurizio Fonte, senior backend engineer. All tools: https://www.mauriziofonte.it/tools/
