Tell me only about the published vulnerabilities that affect the versions I actually run — and only the ones I haven't already seen.
A raw CVE feed is unusable: hundreds of entries a day, almost none of which concern you. Filtering by product is not enough either — an advisory about Django 6.0 is noise if you run 4.2.
Two things make a feed something you keep reading:
- Version matching that is actually correct. Harder than it looks, see below.
- The delta. Ninety-nine advisories is a report you close. Three new ones since yesterday is something you act on.
pip install -r requirements.txt # no dependencies beyond the stdlibWrite down what you run:
# inventory.txt
PyPI:django==4.2.0
PyPI:requests==2.30.0
npm:lodash==4.17.20
python -m stack_radar inventory.txt3 components checked, 99 advisories, 99 new
NEW CVE-2024-42005 django@4.2.0 fixed in 5.0.8, 4.2.15
Django SQL injection vulnerability
NEW CVE-2021-23337 lodash@4.17.20 fixed in 4.18.0
lodash vulnerable to Code Injection via `_.template` imports key names
...
Run it again tomorrow, and only what appeared since is reported:
python -m stack_radar inventory.txt --new-only3 components checked, 99 advisories, 0 new
Exit codes: 0 nothing to report · 2 findings · 1 error. So cron or CI
acts on it directly:
python -m stack_radar inventory.txt --new-only || mail -s "new CVEs" me@example.com| Option | |
|---|---|
--new-only |
Report only what appeared since the last scan |
--no-remember |
Do not record this scan — everything stays new |
--state |
Where seen advisories live (default ~/.stack-radar/seen.json) |
Ecosystems are whatever OSV supports: PyPI, npm, Go, crates.io, Maven, NuGet, Packagist, RubyGems, Debian, Alpine…
Compare versions as strings and "4.10" sorts before "4.9" — you miss the
advisory that hits you. Ignore pre-release markers and 6.0a1 looks newer than
6.0 — you alert on a range you were never in.
Then there are two traps that catch code which handles both of those correctly.
GHSA-2mcm-79hx-8fxw carries three affected blocks, one per maintained Django
branch:
introduced 6.0a1 fixed 6.0.2
introduced 5.2a1 fixed 5.2.11
introduced 4.2a1 fixed 4.2.28
Read affected[0] — the obvious thing to write — and Django 4.2.0 reports as
safe, because 4.2.0 is not in [6.0a1, 6.0.2). It is very much affected; the
answer was in the third block.
GHSA-6c3j-c64m-qhgq shows up in a Django query, because Django ships jQuery in
its admin. It carries a jQuery range of [1.1.4, 3.4.0). Compare a Django
version against it without checking which package the block describes, and you
raise an alert about software the range never mentioned.
This one was not anticipated. It was found by cross-checking (below).
OSV filters by version server-side. Query it twice — once with a version, once without — and you get a labelled dataset for free: every advisory returned for the version is a positive, every other one a negative.
Running our own matcher across four packages, 652 records:
| First run | 651 agree, 1 disagree |
| The disagreement | the jQuery-inside-Django case above — a real bug |
| After the fix | 652 agree, 0 disagree |
That single disagreement is the entire justification for the cross-check. It was not a case anybody would have thought to write a test for, and it produced a false alert with no visible symptom.
Every advisory OSV returns is re-checked locally before being reported. OSV filters correctly — but a feed you act on should not depend on a remote filter you cannot inspect.
pytest -q # 35 tests- It does not discover your inventory. You write it down. Auto-detection guesses, and a security feed built on guesses reports the wrong versions.
- It does not recompute CVSS. Ordering is derived from the vector string (network reachable, no privileges, no interaction, high impact) as a triage aid. Read the advisory before deciding anything.
- It does not cover what OSV does not cover. Commercial software and appliances are largely absent. It is strong on open-source dependencies.
- It is not a scanner. It reads a list you maintain; it does not look at your machines.
MIT — see LICENSE.