I built py-uv-audit because uv audit wasn't telling me enough
I shipped a small Python package this week. It's called py-uv-audit (https://pypi.org/project/py-uv-audit/), it works on uv-managed Python projects, and it tells you which of your dependencies have known vulnerabilities — plus, importantly, what to actually do about them.
Where the idea came from?
I was working on a website that used npm, and npm install casually dropped a "you have 7 vulnerabilities" message into the terminal. Standard npm behavior. And I thought — wait, why doesn't Python have this?
You install something with pip or uv and… nothing. No nudge that you just pulled in a package with three open CVEs.
Turns out it kind of does. uv audit exists and does a solid job. But after using it for a bit on a real project, I
had this list of questions it didn't answer:
- Which of my dependencies actually introduced this vulnerability?
- Is it something I added or did some library pull it in transitively?
- What's the safest upgrade path?
- Can I fix this without breaking my dependency tree?
- What's the exact command I should run?
The output told me what was broken but never really told me how to fix it without making things worse. So, I built the thing I wanted.
The deeper pattern I kept noticing
There's a thing happening that I think is going to get worse before it gets better: AI-generated code pulls in outdated packages.
Most AI models work on stale training data, so they suggest older versions of libraries, outdated APIs, and packages with patched-but-still-bundled CVEs. And because we're all shipping faster than ever, those vulnerable transitive deps slip into projects without anyone clocking them. The pace of code production is outrunning the pace of code review.
So a tool that makes dependency vulnerabilities cheap to triage and fix feels useful right now. Not in a year. Now.
What py-uv-audit actually does?
Point it at a uv-managed project (so anything with a pyproject.toml and a uv.lock) and it gives you something like
this:
```
=== VULNERABILITY REPORT ===
VULNERABLE: idna v3.11
Introduced via: aio-pika → aiormq → yarl → idna
- GHSA-65pc-fj4g-8rjx: Internationalized Domain Names in Applications (IDNA):
Specially crafted inputs to idna.encode() can bypass CVE-2024-3651 fix
Severity: MODERATE (CVSS_V4)
Fixed in: 3.15
Advisory: https://nvd.nist.gov/vuln/detail/CVE-2026-45409
...
--- 8 vulnerable package(s) found (63 total scanned) ---
And then this:
=== REMEDIATION SUGGESTIONS ===
Direct dependencies:
cryptography 46.0.3 → 46.0.7 [PATCH]
Command: uv add "cryptography>=46.0.7"
pyproject.toml: "cryptography>=46.0.7"
Fixes 3 vulnerabilities
Transitive dependencies:
idna 3.11 → 3.15 [MINOR] (via: aio-pika → aiormq → yarl → idna)
Option A: uv lock --upgrade-package idna
MINOR bump — yarl v1.22.0 supports this version
Option B: add "idna>=3.15" to pyproject.toml
if Option A causes a conflict, pin a version floor here
Option C: uv lock --upgrade-package aio-pika
last resort — upgrades aio-pika so it pulls in a compatible idna
```
The dependency chain is spelled out — `aio-pika → aiormq → yarl → idna` — so you can see at a glance how this thing got into your project.
Direct and transitive vulnerabilities are separated. Direct deps are usually a one-line fix. Transitive deps are where things get messy, and they deserve different treatment.
For every transitive vulnerability there are three remediation strategies, in order of preference:
1. try `uv lock --upgrade-package` *(least disruptive)*
2. pin a version floor in `pyproject.toml` *(when the lockfile won't budge)*
3. upgrade the tier-1 parent that pulled the package in *(last resort)*
The bump type is tagged — `[PATCH]`, `[MINOR]`, `[MAJOR]` — so you know whether you're applying a safe fix or signing up for a breaking change.
You get the exact `uv add ...` command to run. No need to look up syntax.
```
cryptography 46.0.3 has 3 known vulnerabilities:
- GHSA-m959-cc7f-wv43: ...
Fixed in: 46.0.6
- GHSA-p423-j2cm-9vmq: ...
Fixed in: 46.0.7
- GHSA-r6ph-v2qm-q3c2: ...
Fixed in: 46.0.5
```
Here is the difference side by side:
Useful! But it stops at "here's what's wrong." py-uv-audit tries to take the next step: "and here's how to fix it without breaking your lockfile."
To be clear — uv audit and uv itself are great. I love using uv. This is just the bit I wanted on top.
Why Rust?
Honest answer: I've wanted to build something in Rust for a long time and never had the right excuse.
uv is also written in Rust, and there was something satisfying about "tool that audits a Rust-built tool's lockfile, also written in Rust." Plus, for a CLI that needs to parse TOML, walk a dependency graph, batch-call an HTTP API, and produce colorized terminal output fast — Rust is genuinely good at that. Cold-start time is basically zero, the binary is ~4 MB, and it's snappy on big lockfiles.
How the distribution actually works?
This is the part I'm most happy about, technically. py-uv-audit is a precompiled Rust binary — but you install it through pip:
pip install py-uv-audit or uv add py-uv-audit
Same model as ruff (https://github.com/astral-sh/ruff) and uv itself. There's no Python code at runtime. The PyPI wheel is just a thin envelope around the Rust binary; pip extracts it into your venv's bin/ and that's it. Python is purely the installer and the distribution channel.
The build pipeline is a GitHub Actions matrix that compiles native binaries for Linux x86_64, Linux aarch64 (on the new ubuntu-24.04-arm native runners), macOS Apple Silicon, and Windows x86_64. Each binary gets wrapped into a py3-none-<platform> wheel by maturin (https://github.com/PyO3/maturin) (using bindings = "bin" mode), uploaded as a CI artifact, and on a v* tag everything publishes to PyPI via OIDC trusted publishing. No secrets, no manualupload.
For lookups, it hits the OSV.dev (https://osv.dev) public vulnerability database — Google's umbrella over GitHub Advisories, PyPI Safety DB, and friends. One batch POST per scan (/v1/querybatch is capped at 1000 packages), then a per-CVE fetch for severity and fix metadata. The "what's the safest upgrade?" suggestions are computed locally by reading each parent's version constraints from uv.lock and figuring out whether a patch/minor/major bump would satisfy them.
Why this one matters to me?
I've always wanted to do something in cybersecurity. Building a vulnerability scanner — even a small one — feels like a real first step in that direction, not a tutorial-shaped one. It scratches actual itches: real CVEs, real production lockfiles, real upgrade decisions developers have to make on Friday afternoons. And I picked up a lot building it: the OSV API, semver bump semantics, how uv.lock actually represents transitive deps, how maturin ships Rust binaries through PyPI, how to wire up cross-platform CI without setting anything on fire. Almost every commit taught me something.
Try it
pip install py-uv-audit
cd my-project
py-uv-audit audit --suggest
That's the whole interface.
- GitHub (https://github.com/shivakharbanda/py-uv-audit)
- PyPI (https://pypi.org/project/py-uv-audit/)
If you use uv, run py-uv-audit audit --suggest on a real project and tell me what's confusing, what's wrong, or what should be there but isn't. If you work in Python tooling or security, I'd love to hear what would make this more useful in your day-to-day.
pip install py-uv-audit or uv add py-uv-audit
cd my-project
py-uv-audit audit --suggest
That's the whole interface.
- GitHub (https://github.com/shivakharbanda/py-uv-audit)
- PyPI (https://pypi.org/project/py-uv-audit/)
If you use uv, run py-uv-audit audit --suggest on a real project and tell me what's confusing, what's wrong, or what should be there but isn't. If you work in Python tooling or security, I'd love to hear what would make this more useful in your day-to-day.
Comments
Post a Comment