Risk that has no CVE yet
Every other dependency scanner starts from a published advisory. This one starts from your call site and asks a different question: can attacker-controlled data reach a dangerous operation somewhere down the transitive chain — whether or not anyone has written it up?
What it returns
path P-8daff1f4-net_request confidence 0.85 no CVE required callsite app.py:14 requests.get(v2) through requests@2.32.3 -> urllib3@2.2.2 sink urllib3.connection.request (net_request) fix wrap the argument at the call site: vdb_safe_url(...) residual install-script-malware, background-exfiltration (not covered)
The fix is applied at your call site. VDB never asks you to patch, fork, or pin the dependency — you usually cannot fix someone else's package, and you can always fix your own boundary.
Your source code never leaves your machine
The file is abstracted locally into a call-graph representation. Only that representation and your lockfile are uploaded.
| Sent | Never sent |
|---|---|
| Call sites: module, API name, argument position | Function bodies, control flow |
| Whether each argument is tainted, and how | Identifiers — user_id becomes v3 |
Literal shapes — STR<len:9, [a-z0-9-]> | Literal values, comments, docstrings |
| Your lockfile — already public data | File paths, project name |
You do not have to take that on faith. --emit-ir
prints exactly what would be sent and makes no network call at all.
How a path is decided
- 1 Summaries, built once per package version. Each one records where an argument reaches a dangerous operation inside that package, and where it is handed to a specific argument of a specific downstream API. Built offline from the real registry artifact by parsing only — package code is never imported and never executed.
- 2 Composition over your resolved graph. Those summaries are chained across the exact versions your lockfile resolves to. A version range cannot be analyzed — the answer differs per version.
- 3 Six sink classes. File path operations, command execution, network requests, deserialization, regex evaluation, and global-state mutation.
Dynamic dispatch, native extensions, a failed signature check. Rather than dropping the path and under-reporting silently, the analysis assumes the argument passes through and prices that assumption into a confidence score you can see. A conservative guess you can inspect beats a silent miss.
Proving you closed it
Finding a path is half of it. After you apply the fix, re-analysis issues signed evidence bound to the hash of the dependency graph it was decided against — so an attestation cannot be quietly reused after your graph moves.
Partial defense is not closure. A network sink requires a scheme allowlist, a host allowlist, and internal-range blocking. Apply two of the three and you get told which one is missing, not a passing grade:
-> OPEN (sanitizer-incomplete:missing=block-internal-ranges)
A scheme check with the internal ranges left open is the exact shape of a working SSRF. An attestation that calls it closed would be worse than no attestation at all.
Turning a finding list into a triage order
A scan on a real project returns hundreds of advisories, nearly all of them about code the application never puts attacker-controlled data into. A queue nobody can work through is a queue everybody ends up ignoring. Point the same analysis at your source tree and it says which ones can be reached:
$ vdb harden ./src --manifest uv.lock --vex 36 advisory finding(s) in uv.lock not_affected 26 under_investigation 10 analyzed 41 file(s) · ecosystems: pypi
The output is an OpenVEX document, so any tool that reads the format can subtract the same set — and it gets a URL, because the result is only useful when a team looks at the same list.
Point it at the tree, not a file. A not_affected claim is only as wide as the code behind it: derive "nothing reaches urllib3" from one file while forty other modules import it and the document is wrong, in a format built to be trusted automatically. Below two analyzed files, every such claim is withheld.
It never says affected. Reaching a package is not reaching the function an advisory names, and stating otherwise would be a guess wearing the clothes of a finding. Reachability comes from static summaries, so a relation the analyzer missed is a false negative — the document says so on itself. Evidence for triage order, not proof of non-exploitability.
What it does not cover
This ships with every answer, not as a footnote. These are never marked defended, whatever fix you apply:
- — Install-script malware. Runs before your code ever calls the package.
- — Background exfiltration. Happens outside your input path, so a call-site boundary never sees it.
- — Unanalyzed dynamic loading. Reflection and dynamic imports cannot be resolved statically.
A boundary fix constrains what your argument can do. It does not make a dependency trustworthy.
Getting access
Python and PyPI today, over uv.lock,
poetry.lock,
Pipfile.lock,
requirements.txt, and CycloneDX.
The API is live for signed-in users; the local abstraction step ships
with the client.