Loading...
Skip to main content
Ecosystem Guide

What pip-audit Misses: EPSS-Aware Vulnerability Prioritization for Python

pip-audit finds vulnerabilities in your Python dependencies but ranks them by CVSS severity, not exploit probability. Three concrete gaps cause missed priorities, false urgency, and blind spots where advisories carry no CVE at all.

Sudhir P.
Last updated
8 min read

A data engineering team ran pip-audit on their production requirements file, saw four vulnerabilities, and spent the afternoon patching the two marked "Critical" — both CVSS 9.8. Neither had been exploited in the wild. The third finding, CVSS 6.5 in urllib3, sat in the backlog. That one had an active public exploit and a 0.15 EPSS score.

pip-audit is a solid tool. It's maintained by Google and the Python Packaging Authority, covers OSV and PyPA advisory data, and integrates cleanly into CI. The gap isn't detection — pip-audit finds the CVEs. The gap is prioritization: it ranks by CVSS severity, which is a measure of theoretical impact, not likelihood of real-world exploitation.

This piece shows the three things pip-audit misses, demonstrates the gap with a reproducible fixture, and shows how to close each one.

What We'll Cover

What pip-audit Actually Does

When you run pip-audit, the tool:

  1. Resolves your installed packages or parses a requirements file
  2. Queries the OSV database and PyPA advisory feed for matching CVEs
  3. Returns findings sorted by package name, with CVSS severity where available

The output is accurate. If pip-audit says Pillow 9.0.0 has a vulnerability, it does. The problem is the triage layer: every finding looks equally urgent unless you know its CVSS score, and CVSS scores are a poor proxy for real-world risk.

pip-audit does not provide:

  • EPSS scores — the probability this CVE will be exploited within 30 days
  • CISA KEV status — whether this CVE is being actively exploited right now
  • CVE-less advisory awareness — whether a real PyPI advisory exists that carries no CVE identifier, and so never appears in a CVE-keyed scan

The Evidence: Same Requirements, Different Priority Order

This is the fixture used to generate the output below — committed to tests/fixtures/dependencies/python/vulnerable/requirements.txt for reproducibility:

Pillow==9.0.0        # CVE-2022-22817 CVSS 9.8  EPSS 0.02
urllib3==1.26.4      # CVE-2021-33503 CVSS 6.5  EPSS 0.15
cryptography==36.0.0 # CVE-2023-49083 CVSS 5.1  EPSS 0.04
Django==3.2.12       # CVE-2022-28347 CVSS 9.8  EPSS 0.01
requests==2.27.1     # clean

Running pip-audit against this fixture:

$ pip-audit -r requirements.txt
Found 4 known vulnerabilities in 4 packages
Name          Version  ID                   Fix Versions
------------- -------- -------------------- ------------
Pillow        9.0.0    GHSA-8vj2-vxx3-667w  9.0.1
urllib3       1.26.4   CVE-2021-33503       1.26.5
cryptography  36.0.0   CVE-2023-49083       41.0.6
Django        3.2.12   CVE-2022-28347       3.2.13

pip-audit finds all four. It doesn't rank them. Most developers — and most CI pipelines — look at CVSS and patch the highest score first. That ranking is:

  1. Pillow — CVSS 9.8 🔴 Critical
  2. Django — CVSS 9.8 🔴 Critical
  3. urllib3 — CVSS 6.5 🟡 Moderate
  4. cryptography — CVSS 5.1 🟡 Moderate

After adding EPSS and CISA KEV data, the actual priority order is:

  1. urllib3 — EPSS 0.15 (highest exploit probability of the four)
  2. cryptography — EPSS 0.04
  3. Pillow — EPSS 0.02
  4. Django — EPSS 0.01

The two "Critical" findings (Pillow and Django) are at the bottom. urllib3, flagged Moderate by pip-audit, needs patching first.

Miss #1: No Exploitation Probability

CVSS scores measure what a vulnerability could do if exploited. A CVSS 9.8 means: if an attacker successfully exploits this, the impact is severe. It says nothing about how likely exploitation is.

EPSS (Exploit Prediction Scoring System) measures the probability that a CVE will be exploited in the next 30 days, based on threat intelligence, proof-of-concept availability, and historical exploitation patterns. An EPSS of 0.15 means there's a 15% chance of exploitation this month — roughly 7x higher than 0.02.

For the fixture above:

PackageCVSSSeverityEPSSSprint priority
Pillow 9.0.09.8Critical0.02Patch next cycle
Django 3.2.129.8Critical0.01Patch next cycle
urllib3 1.26.46.5Moderate0.15Patch this week
cryptography 36.0.05.1Moderate0.04Patch this sprint

pip-audit cannot produce this table. It sees CVSS 9.8 and 6.5 — that's the full picture. You have to layer EPSS separately, or use a scanner that includes it.

Practical rule: At most about 6% of CVEs are ever exploited, and stricter datasets put it nearer 1–2%. CVSS does not predict which ones. EPSS does.

Miss #2: The Advisories With No CVE

pip-audit queries the PyPA advisory database and OSV. Both are well-maintained. The intuitive worry is that advisories trail NVD disclosure, leaving a window where pip-audit says "clean" and NVD already knows better.

We measured it, and that worry is mostly wrong. Across every CVE NVD published in 2025, joined against OSV's full PyPI export, the median gap between NVD publication and the matching PyPI advisory is 0.00 days — same day. The 90th percentile is 0.16 days, under four hours. And in 28.6% of matched cases the advisory landed before the CVE record (n=738). Coordinated disclosure means the ecosystem often knows first.

So the latency blind spot is largely a myth. The real gap is coverage, not speed: 5.6% of PyPI vulnerability advisories carry no CVE alias at all (excluding the malware/typosquat reports, which can never have one). Those are findings that will never show up in a CVE-keyed workflow no matter how long you wait.

What this means in practice:

NVD publishes CVE-YYYY-XXXXX affecting your-package==X.Y.Z
         │
         │  ← measured gap: median 0.00 days, p90 0.16 days (PyPI, n=738)
         │     and 28.6% of the time this arrow points the other way
         │
         ▼
PyPA advisory published
         │
         ▼
pip-audit now flags the finding

During the lag window, any scanner that checks registry advisories — pip-audit, safety, or even GeekWala — will report clean. This isn't a pip-audit-specific bug; it's an ecosystem-level race condition. The mitigations are NVD-direct monitoring, subscribing to PyPA security advisories, and keeping your advisory scanner current.

Miss #3: No CISA KEV Flag

The CISA Known Exploited Vulnerabilities catalog is a curated list of CVEs confirmed to be actively exploited in the wild. CISA mandates federal agencies patch KEV entries within 2–15 days. Private organizations treat KEV as the clearest available signal that a vulnerability has crossed from theoretical to operational.

pip-audit does not indicate KEV status. A CVE on the KEV catalog looks identical to a theoretical one in pip-audit's output — same ID, same fix version. Your team has to look it up separately.

None of the four packages in the fixture are on KEV. But if urllib3 1.26.4 were on KEV, that finding would require emergency patching — regardless of its CVSS 6.5 Moderate label. pip-audit's output would not change.

Decision tree with full signals:

Is the finding on CISA KEV?
├── Yes → Patch within 24 hours. No exceptions.
└── No → Check EPSS score
         ├── EPSS > 0.3  → Patch this sprint
         ├── EPSS 0.1–0.3 → Schedule for next update cycle
         └── EPSS < 0.1  → Log and monitor

pip-audit gets you to the "Is it a CVE?" question. The decision tree above requires EPSS and KEV data that pip-audit doesn't provide.

A Practical Python Security Workflow

The recommended stack combines pip-audit's strengths (fast, zero-config, PyPA-native) with signals it lacks:

In CI (every build):

# Fast check — catches advisories at merge time
pip-audit -r requirements.txt --format=json --output=audit.json

Weekly or on-demand (cross-ecosystem with EPSS + KEV):

Run the same requirements.txt through a scanner that includes EPSS and CISA KEV enrichment. GeekWala's PyPI scanner accepts requirements files directly and returns findings sorted by exploitation probability rather than CVSS.

On new CVE disclosure:

Subscribing to PyPA security announcements is still worth doing, but not because of a latency gap — the measured gap is essentially zero. It's worth doing because a meaningful share of advisories never acquire a CVE at all, and a CVE-keyed tool cannot see those by construction.

pip-audit in CI is table stakes. The EPSS layer is what separates a noise-generating scanner from an actionable one.

Is pip-audit still worth using?

Yes. pip-audit is fast, maintained, and has no false positives from CPE matching (unlike OWASP Dependency-Check). It integrates cleanly into GitHub Actions with a single action. The limitation is prioritization, not detection.

Use pip-audit in CI for immediate merge-time feedback. Use EPSS-enriched scanning for triage decisions. They're complementary, not competing. See the full feature-by-feature pip-audit alternative comparison for the complete breakdown, including ecosystem coverage and pricing.

Does pip-audit cover transitive dependencies?

pip-audit scans your full installed environment when invoked without a requirements file (pip-audit alone), which includes transitive packages. With -r requirements.txt, it resolves transitives using pip's resolver. The PyPA advisory database covers direct and indirect dependencies equally — if urllib3 is a transitive dependency of requests, pip-audit catches it.

The limitation isn't coverage; it's still prioritization. A transitive dependency with EPSS 0.15 gets the same Moderate label as a direct one with EPSS 0.01.

What's the difference between pip-audit and safety?

Both scan Python dependencies for known CVEs. pip-audit uses OSV and PyPA advisory data — both open and free. safety (by PyUP) historically used a separate commercial database with earlier advisory publication; some advisories appeared in safety's database before OSV/PyPA. The gap has narrowed as PyPA advisory coverage improved. Neither tool provides EPSS or KEV enrichment.

How often should I run pip-audit?

Run it on every CI build that installs dependencies (including PRs and scheduled builds against your main branch). Vulnerabilities are disclosed continuously — a package clean yesterday may have a new CVE today. The nightly or scheduled scan pattern — even just a cron job running pip-audit against your production lockfile — catches disclosures between deploys.

Can pip-audit replace a dedicated SCA tool?

For small Python-only projects: pip-audit handles detection adequately. The gap is prioritization — you'll still need EPSS context for triage. For multi-ecosystem projects (Python + npm, Python + Go, etc.), a cross-ecosystem scanner with a unified dashboard becomes essential. Switching between pip-audit, npm audit, and go vulnerability checks on every triage session is operationally expensive.


See the EPSS signal pip-audit can't show you.

Scan your requirements.txt with EPSS + CISA KEV enrichment → — paste your requirements file or upload a poetry.lock or Pipfile.lock. Results in under 60 seconds, no account required.