Metadata-Version: 2.4
Name: pip-cve-gate
Version: 0.3.3
Summary: Pre-install CVE gate for pip — blocks vulnerable and freshly published packages before install
Author-email: Sharky <51028592+sharkyger@users.noreply.github.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/sharkyger/pip-cve-gate
Project-URL: Source, https://github.com/sharkyger/pip-cve-gate
Project-URL: Issues, https://github.com/sharkyger/pip-cve-gate/issues
Project-URL: Changelog, https://github.com/sharkyger/pip-cve-gate/blob/main/CHANGELOG.md
Project-URL: Funding, https://github.com/sponsors/sharkyger
Keywords: pip,security,supply-chain,cve,vulnerability
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Build Tools
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.31
Requires-Dist: packaging>=23
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: pytest-cov>=5; extra == "dev"
Requires-Dist: pytest-httpserver>=1.0; extra == "dev"
Requires-Dist: responses>=0.25; extra == "dev"
Requires-Dist: ruff>=0.5; extra == "dev"
Requires-Dist: mypy>=1.10; extra == "dev"
Requires-Dist: types-requests>=2.31; extra == "dev"
Requires-Dist: bandit[toml]>=1.7; extra == "dev"
Requires-Dist: pip-audit>=2.7; extra == "dev"
Requires-Dist: pre-commit>=3.7; extra == "dev"
Requires-Dist: detect-secrets>=1.5; extra == "dev"
Requires-Dist: build>=1.2; extra == "dev"
Dynamic: license-file

# pip-cve-gate

**Pre-install CVE gate for pip.** Blocks vulnerable and freshly published packages *before* any code runs on your machine.

```
safe-pip install flask requests django
# [pip-cve-gate] Scanning 3 package(s)…
# [pip-cve-gate] Resolved 27 package(s) (incl. transitive deps)
# [pip-cve-gate] All clear — delegating to pip
```

If a package is blocked:

```
safe-pip install somelib
# [pip-cve-gate] BLOCKED — install aborted
#   [CVE] 'somelib==1.2.3' has known vulnerabilities: GHSA-xxxx-yyyy-zzzz
#   [FRESH_HOLD] 'dep==0.0.1' was published 1d ago (hold: 3d). Use --skip-fresh-hold to override.
```

Exit code `0` = clean, `1` = blocked, `2` = error.

---

## Why

Post-install tools (pip-audit, safety) run *after* pip has already downloaded and potentially executed install scripts. By then it's too late for zero-hour supply chain attacks.

pip has no native plugin hook for pre-install scanning. pip-cve-gate fills that gap with a wrapper that resolves the full dependency tree, scans every package against three independent feeds, and only delegates to real pip when everything is clean.

The closest prior art — [pipask](https://github.com/feynmanix/pipask) — checks PyPI advisories but lacks freshness hold and OSSF malicious package coverage. pip-cve-gate covers all three.

---

## What it checks

| Signal | Source | Fail behaviour |
|--------|--------|----------------|
| Known CVEs / advisories | [osv-scanner](https://github.com/google/osv-scanner) (the OSV database engine) | Block |
| OSSF malicious packages | [ossf/malicious-packages](https://github.com/ossf/malicious-packages) | Block |
| Freshness hold (default 3d) | PyPI upload timestamp | Block (overridable) |

**Borrow the engine, own the policy.** CVE lookup is a commodity, so the gate
delegates it to Google's [`osv-scanner`](https://github.com/google/osv-scanner)
(install: `brew install osv-scanner`, or see their releases) rather than
hand-rolling OSV queries. pip-cve-gate keeps the policy layer — dependency
resolution, freshness hold, OSSF check, and the fail-closed semantics below.

The gate hands osv-scanner the already-resolved closure and scans it with
`--no-resolve`, so osv-scanner checks *exactly* the versions the gate resolved
instead of re-resolving the manifest itself. `--no-resolve` is an **osv-scanner
2.x** flag (the version installed by `brew install osv-scanner`); on an older
1.x binary the scan exits non-zero and the gate **fails closed** (pass
`--allow-unknown` to override), so install the 2.x line.

### Fail-closed by default

If a feed **cannot produce a verdict** — osv-scanner missing, a network error,
the OSSF index unreachable — the gate emits an `UNVERIFIED` block and **refuses
the install**. "Can't verify" means "don't allow." The only way past an
unverifiable scan is the explicit opt-out:

```bash
safe-pip install flask --allow-unknown   # install despite an unverifiable feed (fail-open opt-in)
```

(A *truncated* OSSF index — common on GitHub without a token — is the one softer
case: it warns and continues on the partial set rather than blocking everything.)

### Known limitations

- **Version resolution is not pip-compatible.** For unversioned specs (`safe-pip install flask`), the gate scans the **latest** release on PyPI, not the version pip would actually pick under your existing constraints. Pin or use a requirements file with explicit specifiers (`flask==3.0.0` or `flask>=3,<4`) when you need the scan to match what pip will install.
- **Environment markers are evaluated in the current interpreter.** A dep gated by `sys_platform == "win32"` scanned on Linux will be skipped, just as pip would skip it.
- **PyPI JSON API has a 60 req/min rate limit.** Caching dedups transitive lookups, but very large dep graphs may still hit it. Set `GITHUB_TOKEN` to lift the OSSF feed's 60 req/hour limit.
- **Editable (`-e`), URL, and VCS installs are not scannable.** They have no PyPI metadata to resolve, so they are forwarded to pip unscanned — each one is flagged with a stderr warning so the gap is visible.

---

## Usage

`safe-pip` is a drop-in replacement for `pip install`:

```bash
safe-pip install flask
safe-pip install "django>=4.2" "celery==5.3.6"
safe-pip install -r requirements.txt
safe-pip install flask --skip-fresh-hold   # bypass freshness hold only
safe-pip install flask --allow-unknown     # install even if a feed can't be verified
```

Non-install subcommands pass through to real pip unchanged:

```bash
safe-pip list
safe-pip show flask
safe-pip uninstall flask
```

---

## Install

```bash
pip install pip-cve-gate
```

**Homebrew** (macOS / Linux):

```bash
brew install sharkyger/tap/pip-cve-gate
```

> **Engine requirement (v0.3.0+).** The CVE check is powered by the external
> [`osv-scanner`](https://github.com/google/osv-scanner) binary. The Homebrew
> formula installs it automatically (`depends_on "osv-scanner"`); with
> `pip install` you must add it yourself (`brew install osv-scanner` or your
> platform's equivalent). If it is absent the gate **fails closed** — pass
> `--allow-unknown` to install anyway.

Or run directly from the repo without installing:

```bash
git clone https://github.com/sharkyger/pip-cve-gate
cd pip-cve-gate
python bin/safe-pip install flask
```

---

## Configuration

| Variable | Default | Description |
|----------|---------|-------------|
| `PIP_CVE_GATE_FRESH_HOLD_DAYS` | `3` | Days a new release must age before install (max 365) |
| `PIP_CVE_GATE_TIMEOUT` | `10` | HTTP timeout in seconds (min 1, max 3600) |
| `PIP_CVE_GATE_MAX_DEPTH` | `5` | Max transitive dependency depth (min 1, max 50) |
| `PIP_CVE_GATE_PIP_BIN` | `pip` | Path to real pip binary |
| `PIP_CVE_GATE_PIP_TIMEOUT` | _(unset)_ | Optional pip subprocess timeout in seconds; unset = no timeout |
| `PIP_CVE_GATE_OSV_SCANNER_BIN` | `osv-scanner` | Path/name of the osv-scanner binary (the CVE engine) |
| `PIP_CVE_GATE_OSV_SCANNER_TIMEOUT` | `120` | osv-scanner subprocess timeout in seconds (min 5, max 3600) |
| `GITHUB_TOKEN` | _(unset)_ | Raises OSSF feed rate limit from 60 req/h to 5000 req/h |

---

## Cross-platform support

CI runs the full happy + fail-closed + arg-parse fix smoke loop on each release inside fresh containers for:

| Distro | Install path |
|--------|--------------|
| Ubuntu (latest) | `apt-get install python3 python3-pip` |
| Debian (stable-slim) | `apt-get install python3 python3-pip` |
| AlmaLinux 9 | `dnf install python3 python3-pip` |
| RHEL UBI 9 | `dnf install python3 python3-pip` |
| macOS (Homebrew tap) | system `python3` + Homebrew formula |

Tested on Python 3.9 – 3.12. Python runtime dependencies: `requests`, `packaging`.
The CVE engine is the external [`osv-scanner`](https://github.com/google/osv-scanner)
binary (`brew install osv-scanner`); if it is absent the gate fails closed (pass
`--allow-unknown` to install anyway).

If you hit a distro-specific issue, open a PR with a fresh entry in `scripts/ci-cross-distro.sh` — the smoke loop is the source of truth.

---

## Part of the safe-install fleet

pip-cve-gate is part of a pre-install CVE gate fleet for different package ecosystems:

| Ecosystem | Tool |
|-----------|------|
| Homebrew | [homebrew-safe-upgrade](https://github.com/sharkyger/homebrew-safe-upgrade) |
| Composer (PHP) | [composer-cve-gate](https://github.com/sharkyger/composer-cve-gate) |
| pip (Python) | **pip-cve-gate** ← you are here |

---

## Support

If `pip-cve-gate` saves you time, consider [sponsoring the work](https://github.com/sponsors/sharkyger). Sponsorship funds maintenance of the pre-install CVE gate fleet across ecosystems.

---

## Contributing

Bugs / feature requests: open an issue or PR. Security issues: see [SECURITY.md](SECURITY.md).

Local development:

```bash
git clone https://github.com/sharkyger/pip-cve-gate
cd pip-cve-gate
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pre-commit install
pytest -v
ruff check src/ tests/
```

---

## License

MIT — see [LICENSE](LICENSE).
