Guide · Python
Scan Python dependencies for vulnerabilities — self-hosted, from your lockfile
Here’s how to find known CVEs across your whole Python
dependency tree — direct and transitive — from the lockfile you already
commit, and fail the build on a severity you choose.
Open advisory data, self-hosted, with no pip install
in the job and nothing sent to a SaaS.
Updated 28 Aug 2026 · by the depproof team
First: which file are you scanning?
Python is the one ecosystem where this question has a different answer in
every repo. There is no single lockfile the way there is a
package-lock.json — there are six plausible files to point a
scanner at, they are not equivalent, and the one you pick decides what a
clean result is worth. PEP 751
standardised a lock format (pylock.toml) in March 2025
precisely because the ecosystem had spent years without one.
depproof reads all six. What changes between them is not whether the scan runs — it’s what the scan is able to claim:
| What you scan | What the result actually covers |
|---|---|
poetry.lock · pdm.lock · uv.lock | Every package you actually install, and which of your dependencies pulled it in. Direct and transitive are really distinguished, so “what do I bump to clear the most findings” has an answer. |
Pipfile.lock | Every package you actually install, exactly pinned. The format records no parentage, so each one is reported as a direct dependency — the findings are complete, the shape of the tree is not. |
requirements.txt — compiled | The same: a complete pinned set without parentage. Recognised by the header pip-compile writes at the top of the file it generates. |
requirements.txt — hand-written | Only what somebody asked for. The transitive packages are not in the file, so they cannot be in the answer — and the report says so, in place of a short clean list. |
pyproject.toml alone | Version constraints, not a resolved set. The weakest input of the six; commit the lockfile your tool already produces and scan that instead. |
The bottom two rows are the ones worth internalising. A hand-written
requirements.txt lists the packages a person typed; the
packages those pull in — where most of your code, and most of your CVEs,
actually live — are simply absent from the file. A scanner given that
file can return a short, clean, entirely honest report about the wrong
set of software. That failure looks exactly like good news, which is why
zero findings is not zero risk.
Poetry
and uv
both write the exact resolved versions to their lockfile and both tell you
to commit it. If you have one, scan it. If you only have a
requirements.txt, the
pip-compile
version of that file is a complete pinned set and the hand-written one is
not — same filename, different trustworthiness.
Option 1 · GitHub Action
Add this to a workflow. depproof finds your Python manifests, checks every
package in the tree for known vulnerabilities, and fails the job when it
finds one at or above your threshold — no pip install, no
virtualenv to build first.
# .github/workflows/vuln-scan.yml
name: vuln-scan
on: [pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: depproof/depproof-action@v1
with:
fail-on: high # critical | high | medium | low | none Option 2 · Docker container
The same scanner ships as one public image — run it in any CI, or locally while you’re deciding what to gate on. It exits non-zero when it finds a vulnerability at or above your threshold, so it drops straight into a pipeline step.
docker pull ghcr.io/depproof/depproof:v0
# scan one lockfile — or drop the filename to find every manifest in the repo
docker run --rm -v $PWD:/work -w /work ghcr.io/depproof/depproof:v0 \
scan poetry.lock --fail-on high --html --output-dir /work Make a partial scan fail instead of passing quietly
This is the setting to reach for if your repos are inconsistent about
lockfiles. require-fidelity fails the build when the
dependency tree could not be fully seen — regardless of how many
vulnerabilities were found:
# a scan that could not see the whole tree is a failure, not a pass
- uses: depproof/depproof-action@v1
with:
fail-on: high
require-fidelity: partial # resolved | partial | off -
resolved— every manifest has to come from a lockfile. The strictest bar, and the right one for a repo that already commits lockfiles everywhere. -
partial— named, specific gaps are tolerated; a tree that was never resolved at all is not. The usual starting point. -
off— the default. Coverage is reported in the run, but never fails the build.
It’s the one rule that can fail a build with an empty findings list, which is exactly the point: the case it exists for is a green check on a project nobody actually inspected.
Fail the build on the severity you choose
fail-on is the gate. Pick the bar that matches your risk
appetite:
critical— fail only on the most severe (the default).high— fail on high and critical (a common CI bar).medium/low— tighten the threshold further.none— report-only; never fails, findings still land in the artifacts.
Severity alone is a blunt instrument — plenty of vulnerabilities under
active attack are rated only medium, and a fail-on: high
gate merges them. If you’d rather gate on what is being exploited than on
what scores badly, see
severity is a bad prioritiser.
What you get
- Known CVEs and malicious versions across the whole tree — direct and transitive, each with its severity.
- A pass/fail exit code your CI can gate on, at the threshold you set.
- Every dependency’s license, classified — the same run answers the license question, so it isn’t a second tool and a second job.
- An HTML report you can read and a CycloneDX SBOM — the findings and the component inventory from one scan.
What this covers — and what it doesn’t
Being precise, because security claims are easy to overstate:
- Known vulnerabilities and known-malicious packages. Python advisories reach depproof through OSV.dev, which serves the community-owned PyPA Advisory Database — open, portable data in a published schema, not a proprietary feed you get locked into.
- It reads what you committed. depproof does not resolve
a lockfile-less project against PyPI on your behalf; if all you have is
a
pyproject.toml, generate the lockfile in CI and scan that. The trade is deliberate — the answer describes the software your build produces, not a resolution performed somewhere else at a different moment. pip-audit makes the opposite trade and resolves requirement files itself, which is functionally apip install. - It’s only as broad as OSV. depproof surfaces what OSV records for your dependencies. It doesn’t run its own behavioral malware analysis or typosquat heuristics, and it doesn’t do reachability analysis — whether a vulnerable function is actually called. Coverage tracks OSV’s.
Where this fits
Vulnerability scanning is one of three things depproof does in the same run — the others are license and SBOM. From here:
- Transitive dependencies — why the packages you never chose are the ones that carry the risk, and how version conflicts get settled.
- Scan npm dependencies and scan Maven & Gradle — the same gate for the rest of a polyglot repo, in one run.
- Self-hosted SBOM generation — the CycloneDX artifact for auditors and customers, on your own infrastructure.
- VEX and vulnerability waivers — what to do about a finding you can’t fix yet.
- The depproof hub — cross-repo blast-radius search: find a newly disclosed CVE everywhere in your estate at once.
depproof is free for open source and teams under $1M revenue — see pricing. The action README is the source of truth for exact inputs and the current release. depproof is not affiliated with the Python Software Foundation, the PyPA, Poetry, Pipenv, PDM or Astral; product names are used nominatively.