Safeguard
Guides

How to Audit Python Dependencies with pip-audit (and What It Misses)

pip-audit checks your Python dependencies against the PyPA advisory database in one command. Here is how to run it well in CI, and the four gaps it leaves open.

Tomas Lindgren
Platform Engineer
6 min read

pip-audit is the PyPA-maintained scanner that checks your Python dependencies against the Python Packaging Advisory Database and OSV.dev, and a single command — pip-audit -r requirements.txt — will tell you which of your pinned packages carry known CVEs. What it will not tell you is whether a package is malicious, whether your transitive dependencies have drifted since the last lock, or whether the vulnerable code path is ever executed. This guide covers how to run pip-audit properly, how to gate CI on it without the team learning to ignore the job, and the gaps you still have to close some other way.

Install it isolated from the project

pip-audit has its own dependency tree — pip-api, cachecontrol, cyclonedx-python-lib, and friends. If you install it into the same virtualenv you are auditing, you can silently upgrade or downgrade packages in the environment you are trying to measure. Keep it out of the project:

pipx install pip-audit
pip-audit --version   # pip-audit 2.7.2 at time of writing

In CI, installing it into the job's throwaway environment with python -m pip install pip-audit==2.7.2 is fine, because nothing persists. Pin the tool version either way. An unpinned scanner that updates itself mid-week is a great way to get a red pipeline that nobody can reproduce locally.

Know which of the two scan modes you are running

Run bare, pip-audit audits whatever is importable in the current environment. Run with -r, it resolves the requirements file in a temporary isolated environment and audits the result:

# Audit the active virtualenv
pip-audit

# Audit a requirements file (resolves in a temp env)
pip-audit -r requirements.txt

# Skip resolution entirely if the file is fully pinned
pip-audit -r requirements.txt --no-deps

The distinction matters. The environment scan reflects what actually runs; the requirements scan reflects what you declared. If those two disagree — and in older projects they usually do — that disagreement is itself a finding.

Output rows carry PYSEC identifiers with CVE and GHSA aliases. Add --desc for advisory text, and -f json or -f cyclonedx-json when a machine is the consumer:

pip-audit -r requirements.txt --desc -f json -o audit.json

Pin everything, or the results are fiction

Auditing a requirements file full of requests>=2.28 tells you about the version the resolver picked today, not the one running in production since March. Compile a full lock first:

pip-compile --generate-hashes -o requirements.txt requirements.in
pip-audit -r requirements.txt --require-hashes

--require-hashes gives you a second property for free: the audit fails if any pinned artifact's hash does not match PyPI, which catches a class of registry tampering that a plain version check never would. If you are on Poetry, poetry export -f requirements.txt --output requirements.txt gets you the same fully-pinned input.

Gate CI without becoming ignorable

A minimal GitHub Actions step:

- name: Audit Python dependencies
  run: |
    python -m pip install pip-audit==2.7.2
    pip-audit -r requirements.txt --strict --desc

pip-audit exits non-zero when it finds anything, so this blocks the merge by default. Two flags keep it honest. --strict fails the run if any dependency could not be audited (private packages, yanked versions) instead of silently skipping it. And --ignore-vuln PYSEC-2023-192 handles accepted risk — but leave a comment with an expiry date next to every ignore, or your allowlist becomes a graveyard.

One pattern that works: block PRs only on newly introduced advisories, and handle the pre-existing backlog in a separate scheduled job. A gate that fails on day one for 40 inherited findings gets deleted by day five.

The --fix flag: useful, occasionally reckless

pip-audit -r requirements.txt --fix --dry-run

--fix rewrites specifiers in place to the nearest fixed release. It checks that the new set resolves; it does not check that your code still works, and a jump from Django 3.2 to 4.2 will resolve just fine while breaking half your middleware. Treat --fix as a pull-request generator, never as a deploy step. Run it with --dry-run first, always behind your test suite.

What pip-audit misses

This is the part vendors gloss over. Four gaps, in rough order of how much they should worry you:

GapWhy pip-audit misses itWhat covers it
Malicious packagesTyposquats live for days before any advisory exists; pip-audit only matches published advisoriesRegistry-level analysis, install-script review
Bundled native codePillow ships its own libwebp; the CVE-2023-4863 exposure lived in a .so, not in Python package metadata, until advisories caught upContainer scanning, binary analysis
System packagesDebian's openssl and zlib never appear in pip listOS-level scanners against the image
ReachabilityNo call graph — an advisory in a module you never import scores the same as one in your request pathReachability-aware SCA

The last one is where teams drown. A mid-size Django monolith routinely surfaces 60-plus advisories, of which a handful are actually reachable. Reachability-aware SCA exists precisely to make that cut for you, and it is the main axis on which commercial tools differ — the Snyk comparison goes through how Safeguard and others handle it. If you live in Rust as well, the same known-vuln-versus-policy split shows up in cargo-audit vs cargo-deny.

None of this makes pip-audit optional. It is free, fast, PyPA-maintained, and catches the embarrassing stuff. It is a floor, not a ceiling.

Frequently asked questions

Does pip-audit work with Poetry or Pipenv projects?

Yes, indirectly. Export a pinned requirements file first (poetry export -f requirements.txt or pipenv requirements), then audit that. Alternatively, install the project into a virtualenv and run bare pip-audit against the environment.

How is pip-audit different from Safety?

Safety queries its own commercial database and its free tier has narrowed over the years; pip-audit is maintained under the PyPA umbrella and reads the open PyPI Advisory Database via OSV. For most teams the coverage is comparable and pip-audit's licensing is simpler.

Can pip-audit generate an SBOM?

It can emit CycloneDX with -f cyclonedx-json, which is handy for handing results to other tooling. For a production SBOM pipeline with component provenance you will want a dedicated generator rather than a scanner side-effect.

Should a pip-audit failure block merges?

Block on newly introduced advisories at high or critical severity; report everything else. A gate the team can pass by fixing their change stays enabled. A gate that punishes them for two years of inherited debt gets muted within a sprint.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.