Safeguard
Security Guides

Python Dependency Scanning: A Practical Guide

Your code is a small fraction of what ships. This is how to inventory, scan, and continuously monitor the Python dependency tree that makes up the rest.

Priya Mehta
Security Researcher
5 min read

Look at any Python service and count the lines you wrote against the lines you installed. The installed side wins by orders of magnitude, and every one of those installed lines runs with your application's privileges. Dependency scanning is how you find out whether the code you did not write is trying to hurt you. Done well, it is a routine part of CI. Done badly, it is a wall of alerts nobody reads.

Start with an accurate inventory

Scanning is only as good as its input, and the input is your resolved dependency tree, not your top-level requirements. The difference is the transitive graph: the packages your packages depend on, which is where most vulnerabilities actually live. Produce a fully resolved, pinned lockfile so the scanner sees exactly what runs:

pip-compile --generate-hashes requirements.in   # or: uv pip compile

If you use Poetry or uv, their lockfiles serve the same purpose. The goal is a deterministic, complete list, so today's scan and tomorrow's deploy describe the same set of packages.

Run a baseline scan

The standard-library-adjacent starting point is pip-audit, which checks your environment or a requirements file against the Python advisory database:

pip-audit -r requirements.txt

This gives you a first pass at known CVEs. It is a fine baseline, and for a small project it may be enough. Its limits show up at scale: it reports what is vulnerable but tells you little about whether the vulnerable code is reachable, how findings trend across many services, or which fix to prioritize when fifty items land at once.

Generate an SBOM as the durable record

A scan is a moment in time; a software bill of materials is a record you keep. Generate one in CycloneDX or SPDX format so any advisory published later can be checked against exactly what you shipped, without rebuilding:

cyclonedx-py environment --output-format json --outfile sbom.json

Storing and diffing SBOMs across releases is where SBOM Studio fits: it keeps a versioned history so that when a new CVE is disclosed, you can immediately answer which past and present builds contain the affected package rather than re-scanning everything by hand.

The problem after scanning: too many findings

The hard part of dependency scanning is not producing findings. It is that a real tree produces more findings than any team can fix at once, and treating them as a flat list burns trust. Two dimensions cut the noise:

Severity and exploitability. A critical CVE in a package you load on every request is not the same as a medium in a dev-only tool. Prioritize by severity weighted with real-world exploitability signals.

Reachability. Many vulnerabilities sit in code paths your application never calls. Reachability analysis traces whether the vulnerable function is actually invoked from your code, so you can defer the ones that are present but unreachable. Our software composition analysis applies this analysis so the list you act on reflects genuine exposure, not raw package counts.

Gate CI, but gate it sanely

Put scanning in the pipeline, but choose thresholds that keep the build meaningful. A common, workable policy: fail on new critical or high findings that are reachable and have a fix available; warn on the rest:

# Example CI step
- name: Dependency scan
  run: safeguard scan --fail-on critical,high --only-reachable --require-fix-available

Running the same check locally before pushing keeps developers out of a slow feedback loop; the Safeguard CLI runs the identical analysis on a laptop so a failing gate is never a surprise in CI.

Close the loop with remediation

A finding you cannot act on is just anxiety. The goal is a short path from "vulnerable" to "fixed": an accurate version bump, the changelog, and confidence the upgrade will not break you. Automated remediation drafts that pull request; a human still reviews and merges it.

Handle the vulnerabilities you cannot fix

Not every finding has a patch, and some patches would break you. A mature process needs a documented answer for these, not silence. When a vulnerability is present but not exploitable in your context, record that judgment as a VEX (Vulnerability Exploitability eXchange) statement so the finding is explicitly triaged rather than ignored, and so the next reviewer inherits your reasoning instead of re-litigating it. For issues that are exploitable but unpatched, apply a compensating control, restrict the input, disable the feature, add a network boundary, and track it with an owner and a review date. The failure mode to avoid is a backlog of thousands of untriaged findings, which trains everyone to ignore the scanner entirely.

Comparison at a glance

ApproachStrengthLimit
pip-auditFree, simple baselineNo reachability or cross-service view
Manual reviewDeep contextDoes not scale, drifts fast
Reachability-aware SCAPrioritizes real exposureRequires integration

How Safeguard helps

We are honest that a scan is only the beginning; the value is in what you do with it. Safeguard ingests your lockfile or SBOM, resolves the full transitive tree, and applies severity plus reachability so the finding list reflects real risk. It integrates through the CLI and CI, retains that history, and closes the loop by drafting fixes for review. If you are comparing reachability-based prioritization against incumbents, our comparison against Snyk lays out the differences, and our pricing is structured so a single service costs nothing to start.

Get started

Produce a pinned lockfile and run a baseline scan today, then layer on reachability and continuous monitoring. Create a free project at app.safeguard.sh/register and follow the Python dependency-scanning setup at docs.safeguard.sh.

Never miss an update

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