Raised: $0
0% of monthly goal Help us cross the finish line!
Goal: $12,000
Raised: $0 Goal: $12,000
0% of monthly goal Help us cross the finish line!
Sponsor DDEV

If you find this add-on useful, please star it on GitHub — stars show appreciation and help maintainers know their work matters.

composer-cve-gate

Pre-install CVE supplement to Composer’s native config.policy. Blocks packages before install-from-lock can load them.

Status — pre-stable (1.x). API and exit codes are stable; defaults, detection heuristics, and recommendation logic may shift in minor versions while we iterate on real-world feedback.

The freshness-hold feature (gap §2 below) is time-boxed: when Composer ships minimum-release-age (a reserved name in their policy roadmap), that specific feature is retired. The install-from-lockfile advisory gap (§1 below) is the durable reason for this tool to exist, so the tool itself remains as long as that gap is open — and will be archived only if Composer closes it upstream too.

Composer 2.10+ ships config.policy.advisories.block (default true) for advisory blocking during composer update / require / remove, and config.policy.malware.block (default true) for malware blocking via the Aikido feed during composer install. composer-cve-gate fills three gaps that composer.policy doesn’t cover:

  1. Advisory blocking at composer install time — When a lockfile was clean at commit but a vulnerability is published for a locked version afterward, a subsequent composer install (the typical CI deploy) loads the vulnerable version with no block. composer audit can be opted in post-install but doesn’t prevent it.
  2. 3-day freshness hold — Defends against zero-hour publish attacks before security researchers can inspect and report advisories. Time-limited (until Composer ships minimum-release-age, a reserved name in their roadmap — we archive when that ships).
  3. Post-install IoC scanningsafe-scan walks vendor/ looking for known compromise indicators (C2 domains, exfil URLs, attacker-injected file paths) after a supply-chain incident surfaces in the news.

What it checks

The scanner queries multiple CVE databases and applies time-based filtering. Each signal covers different scope — see below for accuracy:

  1. OSV.dev — Google’s aggregated advisory feed.
    • Scope: top-level + transitive (batch queried)
    • Covers: Google’s data, which includes ecosystem-native disclosures
  2. GitHub Advisory Database — GitHub’s GHSA disclosures.
    • Scope: top-level only
    • Covers: composer ecosystem advisories, version-range filtered
    • Note: Composer’s native config.policy.advisories.block primarily pulls from GHSA, so this is partially redundant with stock Composer. We query it because our lock-from-cache use case (task #14, v1.3) will require it.
  3. NIST NVD — National Vulnerability Database.
    • Scope: top-level only (budgeted fallback on clean OSV transitive deps)
    • Covers: upstream CVE metadata, CPE-version matches that OSV may miss
    • Note: Slow queries (rate-limited); we budget the first N transitive packages to avoid timeouts.
  4. Packagist freshness hold — time-gate on publish date.
    • Scope: top-level only
    • Threshold: packages published < 3 days ago are held
    • Rationale: zero-hour malicious versions are typically flagged within 72 hours of publish. Override with --min-age 0 when needed.
    • Lifespan: temporary. When Composer ships minimum-release-age as a native policy, we will archive this tool.
  5. OSSF Malicious Packages — the OpenSSF ossf/malicious-packages registry (local snapshot).
    • Scope: top-level + transitive
    • Covers: known malware, confirmed by the OSSF community
    • Note: Separate from composer.policy.malware, which uses the Aikido feed. We include OSSF for breadth.

Why pre-install matters

Composer dependency code can execute on the next autoload bootstrap or when loading a composer-plugin type package — both happen during composer install itself, before composer audit gets to inspect anything. If a vulnerable (or malicious) version isn’t blocked at install time from the lockfile, the code runs before you have a chance to audit it.

Pre-install and install-time gating are the only points in the lifecycle where blocking is still useful. composer audit post-install is a useful backstop, but too late if the malicious code already executed.

Install

composer require sharkyger/composer-cve-gate --dev

That’s it — the plugin self-registers and both subcommands appear in composer list immediately. No config file, no per-project setup.

Requirements

Component Version Why
Composer ^2.0 Plugin uses the modern composer-plugin-api v2 hook
PHP ^8.2 Modern constructor promotion, readonly, enum
Python ≥ 3.11 Scanner uses datetime.UTC (Python 3.11+)

The bundled scanner (bin/dependency_security_check.py) is invoked as a subprocess — python3 must be on PATH. The scanner has zero third-party Python dependencies (only stdlib + the optional certifi bundle on macOS for SSL trust). If Python is missing at activation, the plugin fails loud immediately rather than disabling itself silently.

Usage

The plugin adds three commands: safe-install, safe-upgrade (aliased as safe-update), and safe-scan.

Install a new package, scanned first

composer safe-install monolog/monolog

The plugin resolves monolog/monolog plus its full transitive tree, queries every package against OSV / GHSA / NVD plus the freshness hold, and only proceeds with the actual install if everything is clean. Output on a clean scan:

safe-install: scanning monolog/monolog
[standard composer require output follows]

If something is blocked, you’ll see a structured report and nothing installs:

safe-install: scanning evil/pkg
BLOCKED: evil/[email protected] — status=vulnerable
  [CRITICAL] CVE-2026-XXXX — info-stealer in post-install script
safe-install: blocked 1 of 1 package(s). Nothing installed.

Exit code is 1. Your project is untouched — no download, no vendor/ write, no post-install scripts run.

Install a dev dependency

composer safe-install --dev phpstan/phpstan

--dev is forwarded to composer require, so the package lands in require-dev as expected.

Upgrade all dependencies

composer safe-upgrade

(Also available as composer safe-update — alias for discoverability.)

Scans every direct dependency from your composer.json, then delegates to composer update with no package args — composer resolves the full graph (including transitive-only updates).

Upgrade one package

composer safe-upgrade vendor/pkg

Scans then runs composer update vendor/pkg. Works the same with safe-update.

Install a brand-new release

The 3-day freshness hold blocks installs of packages published less than 72 hours ago — that’s the window where a compromised version is most often up on Packagist but not yet in any CVE database. If you know a particular fresh release is fine (e.g. a patch you’ve been waiting for from a maintainer you trust), pin to that version and disable the hold:

composer safe-install --min-age 0 vendor/just-released:1.2.3

Audit what’s already installed

composer safe-scan

Reads composer.lock to enumerate every installed dependency, runs the full pre-install scan against each, and additionally walks vendor/<package>/ looking for indicator-of-compromise strings or marker files from any known-malicious finding (C2 domains, exfil URLs, attacker-injected file paths). Output categorises packages as:

=== safe-scan report ===

INFECTED — 1 package(s):
  evil/[email protected]
    [url] https://evil.test/exfil  →  vendor/evil/pkg/src/payload.php

safe-scan — 12 clean, 0 suspicious, 1 infected (of 13 scanned).
Status Meaning
CLEAN No findings, no IoC matches.
SUSPICIOUS Vulnerability database hit, but no IoC strings on disk.
INFECTED IoC strings or marker files found inside the installed package.

Read-only — safe-scan never executes, modifies, or downloads anything. It’s the answer to “am I already infected?” after a supply-chain incident hits the news.

Reading exit codes

safe-install / safe-upgrade:

Exit code Meaning
0 Scan clean, install proceeded
10 At least one package blocked, nothing installed
1 Scanner errored (network, missing Python, etc.)

safe-scan:

Exit code Meaning
0 Clean
1 Infected (IoC matches found on disk)
2 Suspicious (vulnerability findings but no IoCs on disk)
3 Scanner error (lockfile missing, malformed, etc.)

When you see a BLOCKED line, the next step is to look up the CVE or advisory ID it cites and decide whether the issue actually applies to your usage. If it doesn’t, you have two paths:

Scope

composer-cve-gate is a supplement to config.policy, not a replacement.

It does not replace:

Temporary tool

When Composer ships minimum-release-age (a reserved name in their policy roadmap), the freshness-hold differentiator disappears and we will archive. We’re a stopgap for a known gap, not a permanent product. Maintain without long-term lock-in fear.

DDEV

If your project uses DDEV (TYPO3, Drupal, Laravel, Symfony, Magento, …), install the addon instead of the composer plugin directly. The addon runs the scanner inside the web container against the container’s PHP version — which is the version your application actually runs — rather than whatever PHP happens to be on your host.

ddev add-on get sharkyger/composer-cve-gate

That registers three custom commands and auto-installs the composer plugin into your project (if composer.json exists):

ddev safe-install monolog/monolog
ddev safe-upgrade
ddev safe-scan

Each one runs in the web container and applies the same 5-signal gate the plain-composer commands do. No host shim — your host PHP version is irrelevant.

Remove the addon with ddev add-on remove composer-cve-gate, which also removes the composer plugin from your project.

composer-cve-gate is part of the safe-install family:

Shipped (v1.0.0+):

In progress (v1.2+):

Roadmap (v1.3+):

All share the OSV + GHSA + NVD + freshness-hold pattern. Composer has a native plugin API, so we use it here. pip and npm will use prefixed binaries instead.

License

MIT. See LICENSE.

Security

Report vulnerabilities privately to [email protected]. See SECURITY.md. This repo does not accept public bug reports for security topics.

If you find this add-on useful, please star it on GitHub — stars show appreciation and help maintainers know their work matters.