PHPackages                             sharkyger/composer-cve-gate - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Security](/categories/security)
4. /
5. sharkyger/composer-cve-gate

ActiveComposer-plugin[Security](/categories/security)

sharkyger/composer-cve-gate
===========================

Pre-install / pre-upgrade CVE gate for Composer. Blocks before post-install scripts run.

v1.1.4(1mo ago)236MITPHPPHP ^8.2CI passing

Since May 24Pushed 1mo agoCompare

[ Source](https://github.com/sharkyger/composer-cve-gate)[ Packagist](https://packagist.org/packages/sharkyger/composer-cve-gate)[ RSS](/packages/sharkyger-composer-cve-gate/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (10)Dependencies (6)Versions (43)Used By (0)

composer-cve-gate
=================

[](#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 (0.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.

> **Note on versioning.** v1.0.0 through v1.1.4 were a versioning mistake — v1.0 was declared before the project met the stability gate (v1.0 must be earned). The pre-stable line continues at **v0.2.x**. The v1.x tags remain published for archeology; future work happens on the v0.x line. If you have v1.x installed, run `composer require sharkyger/composer-cve-gate:^0.2` to migrate.

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 scanning** — `safe-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
--------------

[](#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 so the install-from-lock gate has GHSA coverage on the locked set.
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 &lt; 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`](https://github.com/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.

### Packages we skip (and why)

[](#packages-we-skip-and-why)

Two package shapes have no advisory data to query, so the gate skips them **before** any scan runs — silently in the install gate, with one informational line in `safe-scan`. Neither blocks the install:

- **Path-type packages** (`composer.json` repositories of `"type": "path"`, or any package whose lockfile entry has `dist.type: "path"`) — your project's own bespoke `clientname/site-package`, a Drupal custom module, an in-house Laravel package. Not on Packagist, not in any vulnerability database.
- **Composer dev-branch references** (`dev-main`, `dev-feature/x`, `1.x-dev`, etc.) — typically a private / in-development extension installed from a branch. Composer advisories are keyed to released versions and tags, not arbitrary branches.

If you want to keep an eye on what got skipped, `composer safe-scan`lists each skipped package with the reason in its report and counts them in the summary line as `N skipped`.

Why pre-install matters
-----------------------

[](#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.

Usage
-----

[](#usage)

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

### Install a new package, scanned first

[](#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/pkg@1.0.0 — 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

[](#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

[](#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

[](#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

[](#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

[](#audit-whats-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//` 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/pkg@1.0.0
    [url] https://evil.test/exfil  →  vendor/evil/pkg/src/payload.php

safe-scan — 12 clean, 0 suspicious, 1 infected (of 13 scanned).
```

StatusMeaning`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

[](#reading-exit-codes)

`safe-install` / `safe-upgrade`:

Exit codeMeaning`0`Scan clean, install proceeded`10`At least one package blocked, **nothing installed**`1`Scanner errored (network, missing Python, etc.)`safe-scan`:

Exit codeMeaning`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:

- Pin to a patched version explicitly: `composer safe-install vendor/pkg:^2.1.4`
- Disable the freshness hold for a one-off (only if the block came from `FRESH-HOLD`, not from a CVE): `composer safe-install --min-age 0 vendor/pkg`

Install
-------

[](#install)

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

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

### Requirements

[](#requirements)

ComponentVersionWhyComposer`^2.0`Plugin uses the modern `composer-plugin-api` v2 hookPHP`^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.

Configure the install-from-lock gate
------------------------------------

[](#configure-the-install-from-lock-gate)

The install-from-lock gate is **on by default in advisory mode** — plain `composer install` loads the lockfile, scans the locked set, warns on any findings, and proceeds. To fail the build on findings, switch to `block`mode via the root `composer.json`:

```
{
    "extra": {
        "composer-cve-gate": {
            "install-gate": "advisory",
            "install-gate-min-age": 3,
            "install-gate-cache-ttl": 21600
        }
    }
}
```

Any subset of these keys works — unspecified keys keep their defaults.

KeyTypeDefaultBehaviour`install-gate`string`advisory``advisory` warns and proceeds · `block` aborts the install with a non-zero exit **before any download or post-install script runs** · `off` is a silent permanent disable`install-gate-min-age`int (days)`3`Freshness hold applied to every locked package's publish date. `0` disables the hold (re-introduces the zero-hour-publish gap).`install-gate-cache-ttl`int (seconds)`21600` (6h)Per-package clean-verdict cache under `~/.cache/composer-cve-gate/install-gate/` (or `%USERPROFILE%\.cache\…` on Windows). `0` disables caching. Only clean verdicts are cached; flagged or errored verdicts are always re-scanned.Invalid or malformed values fall back to these defaults silently — the gate prefers a slightly-too-strict configuration over a silently disabled one. The flip side: a typo like `"install-gate": "blok"`silently degrades to advisory, so if block mode is critical, sanity-check your config by triggering a known finding once after editing it and confirming the gate's output names the mode you expect.

### `COMPOSER_CVE_GATE_DISABLE` (emergency bypass)

[](#composer_cve_gate_disable-emergency-bypass)

To skip the gate for a single command without editing `composer.json`, set `COMPOSER_CVE_GATE_DISABLE=1` in the environment:

```
COMPOSER_CVE_GATE_DISABLE=1 composer install
```

Any non-empty value other than the string `0` enables the bypass — so `1`, `true` and `yes` all work, **and so does the string `false`**, which is *not* falsy here. To re-enable the gate, unset the variable or set it to `0`. It is loud by design — a warning is printed to the build log so a disabled gate stays visible — and it works in all modes, including `block`. Use it as an emergency lever for a single command (e.g. an urgent hotfix deploy where you've already verified the finding); for a permanent silent disable in `composer.json`, use `install-gate: off`instead.

**Precedence:** `install-gate: off` short-circuits first (no scan, no log line). Otherwise, `COMPOSER_CVE_GATE_DISABLE` (when set to a non-`0`value) takes priority over the `install-gate` mode in `composer.json`.

Verify the install-from-lock gate
---------------------------------

[](#verify-the-install-from-lock-gate)

The core claim — blocking a flagged package **at `composer install` time, before any download or post-install script runs** — ships as an end-to-end test you can reproduce yourself in a throwaway container. The test builds a project whose `composer.lock` pins a flagged package (an inert, fixture-only stub — no real package or malware), runs a **real `composer install`**, and asserts the gate proceeds-with-a-warning in advisory mode and **aborts before the operation runs** in block mode (the package is never written to `vendor/`). The verdict is driven by a local advisory fixture, so it is deterministic and needs no live network lookup.

Any Linux base with PHP 8.2+, Python 3.11+, git and Composer 2 works. Start a disposable container (`--rm` auto-removes it on exit):

```
docker run --rm -it debian:trixie bash      # Debian / Ubuntu (apt)
# or:  docker run --rm -it almalinux:10 bash  # RHEL / AlmaLinux / UBI (dnf)
```

Then, inside it:

```
# 1) dependencies — Debian/Ubuntu (apt):
apt update && apt install -y php-cli php-mbstring php-xml php-zip git unzip python3 python3-venv python3-pip
#    RHEL/AlmaLinux/UBI instead (dnf):
#    dnf install -y php-cli php-mbstring php-xml git unzip python3 python3-pip

# 2) Composer — hash-verified official installer:
php -r "copy('https://getcomposer.org/installer','composer-setup.php');"
php -r "if (hash_file('sha384','composer-setup.php') === trim(file_get_contents('https://composer.github.io/installer.sig'))) { echo 'verified'.PHP_EOL; } else { unlink('composer-setup.php'); exit(1); }"
php composer-setup.php --install-dir=/usr/local/bin --filename=composer

# 3) run the proof:
git clone https://github.com/sharkyger/composer-cve-gate.git && cd composer-cve-gate
python3 -m venv .venv && . .venv/bin/activate
pip install pytest -r requirements.txt
pytest tests/integration/ -v        # -> 2 passed
```

`2 passed` confirms the gate fired on a real `composer install` on that platform. It has been reproduced this way across both major packaging families — Debian/Ubuntu (apt) and RHEL/AlmaLinux including unregistered UBI (dnf) — on PHP 8.3–8.5 and Python 3.12–3.14. The same end-to-end test runs in CI on every change.

> **Full step-by-step** — per-distro start commands, the dnf variant, and Docker start/stop/cleanup tips — is in [docs/verifying-in-docker.md](docs/verifying-in-docker.md).

Scope
-----

[](#scope)

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

It does not replace:

- **`composer audit`** — post-install lockfile scanning, included in every Composer project by default. Run it regularly.
- **`config.policy.advisories.block`** — native advisory blocking during `composer update` / `require` / `remove` (Composer 2.10+, default true). We run in parallel for the install-from-lock gap it doesn't cover.
- **`config.policy.malware.block`** — native malware blocking via Aikido during `composer install` (Composer 2.10+, default true). We provide additional OSSF ingestion.

### Temporary tool

[](#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
----

[](#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.

Related projects
----------------

[](#related-projects)

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

**Shipped:**

- `homebrew-safe-upgrade` — `brew safe-install` / `brew safe-upgrade`
- `claude-code-cve-gate` — Claude Code hook (intercepts AI installs)
- `mistral-code-cve-gate` — Mistral Code hook
- **This project** — `composer safe-install`, `composer safe-upgrade`, and the install-from-lock gate (plain `composer install` from a lockfile)

**Roadmap:**

- `pip-cve-gate` — `pip safe-install` / `pip safe-upgrade`
- `npm-cve-gate` — `npm safe-install` / `npm safe-upgrade`

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
-------

[](#license)

MIT. See [LICENSE](LICENSE).

Security
--------

[](#security)

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

###  Health Score

44

—

FairBetter than 90% of packages

Maintenance89

Actively maintained with recent releases

Popularity10

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity58

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

11

Last Release

55d ago

Major Versions

v0.1.0 → v1.0.02026-05-24

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/51028592?v=4)[Sharky](/maintainers/sharkyger)[@sharkyger](https://github.com/sharkyger)

---

Top Contributors

[![sharkyger](https://avatars.githubusercontent.com/u/51028592?v=4)](https://github.com/sharkyger "sharkyger (76 commits)")

---

Tags

composer-plugincveddev-getpackagistphpsecuritysupply-chain-securityvulnerability-scannersecuritycomposer-plugincvevulnerabilitysupply-chainpre-install

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan, Psalm

Type Coverage Yes

### Embed Badge

![Health badge](/badges/sharkyger-composer-cve-gate/health.svg)

```
[![Health](https://phpackages.com/badges/sharkyger-composer-cve-gate/health.svg)](https://phpackages.com/packages/sharkyger-composer-cve-gate)
```

###  Alternatives

[matomo/matomo

Matomo is the leading Free/Libre open analytics platform

21.7k38.9k](/packages/matomo-matomo)[oro/platform

Business Application Platform (BAP)

645143.5k116](/packages/oro-platform)[psecio/versionscan

A PHP version scanner for reporting possible vulnerabilities

25056.4k1](/packages/psecio-versionscan)[plan2net/typo3-update-check

A Composer plugin that checks for TYPO3 updates and provides detailed information about breaking changes and security updates

207.2k](/packages/plan2net-typo3-update-check)[dgtlss/warden

A Laravel package that proactively monitors your dependencies for security vulnerabilities by running automated composer audits and sending notifications via webhooks and email

9062.1k](/packages/dgtlss-warden)[mitnick/laravel-security

laravel-mitnick helps you secure your Laravel apps by setting various HTTP headers. it can help!

8111.8k1](/packages/mitnick-laravel-security)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
