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 a pip 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:

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.