Dev.to Security 🔐 Cybersecurity 👁 0 📖 6 min read

I swapped trivy for an API call. Only 17% of my CVEs were in code that was actually running

My homelab security tool, warden, used to run trivy to find vulnerable packages. As of 10 October it doesn't. I pulled the scanner bit out, rewrote it around OSV.dev, and it's now its own small tool called sieve. This is

I swapped trivy for an API call. Only 17% of my CVEs were in code that was actually running

My homelab security tool, warden, used to run trivy to find vulnerable packages. As of 10 October it doesn't. I pulled the scanner bit out, rewrote it around OSV.dev, and it's now its own small tool called sieve. This is why, what it does differently, and the bug it found on the way.

What sieve replaced: trivy's binary, database and server vs package lists and OSV.dev

Why trivy had to go

Trivy is a good scanner. It was also the heaviest and riskiest thing in warden.

On my box it was a 161 MB binary that runs as root, a server process sat on the LAN with its own token, and a vulnerability database that had grown to 1.4 GB and needed downloading and keeping fresh.

Then trivy's own v0.69.4 release, and its v0.69.5 and v0.69.6 images, turned out to be malicious (CVE-2026-33634). The tool you run as root to find supply-chain problems became a supply-chain problem. I wasn't caught by it, but that was luck, not design.

So the question was: do I actually need any of that machinery?

What sieve does instead

Every Linux box already has a list of exactly what's installed. Debian has /var/lib/dpkg/status, Alpine has /lib/apk/db/installed, Python packages carry a METADATA file. Go binaries carry their build info. Rust binaries built with cargo-auditable carry their dependency list.

OSV.dev is the open database behind Google's osv-scanner. Debian, Ubuntu, Alpine, the GitHub advisory database, PyPA, the Go team and RustSec all publish straight into it.

sieve just joins those two things up:

  1. read the package lists that are already there
  2. send OSV one batch query with each package and its exact version
  3. add CISA's known-exploited list (KEV) and FIRST's EPSS scores
  4. print a short table, worst first

How sieve works: package lists, OSV.dev, KEV and EPSS, ranked table

No binary to download, no database to keep patched, nothing listening. The only dependency is requests. A full scan of a patched Debian 13 host took 15 seconds and peaked at 80 MB of memory.

Three things it does differently

It scans what's running, not the image. For containers, sieve finds every image with a running container and reads that container's real filesystem through /proc/<pid>/root. That mattered straight away. Trivy flagged two "actively exploited" alerts for a Python package in one of my containers. That container doesn't run that package any more: it runs a renamed fork. Trivy was reading the image. sieve reads what's actually there.

It ranks for fixing, not for scaring. A fully patched Debian box still shows nearly 2,000 findings, almost all "affected, no fix released yet". That list is useless on its own. sieve sorts it into tiers:

  1. in CISA KEV (someone is exploiting it right now), fix or not
  2. EPSS of 10% or more with a fix available
  3. critical with a fix available
  4. anything else with a fix
  5. no fix yet

Here it is against some deliberately old package lists. The two lines in red are the ones that matter. Everything else can wait for the normal update run.

sieve ranking old container images: two actively exploited CVEs first

It fails loudly when it should. Exit code 1 if anything in the output is known-exploited, 2 if part of the scan failed. So sieve all --fixable-only in a cron job or CI step only shouts about things you can actually fix today.

Did it miss anything trivy caught?

That was the bit I cared about. On my Proxmox node the new scanner found 680 of the 691 CVEs trivy found. Both agreed nothing there was fixable. The odd ones out were down to the two databases updating at different times, not to anything sieve can't see.

It wasn't free, though. Some traps I hit building it:

  • Whole advisories are huge. Ubuntu and Debian records list every affected version of every binary. Caching them as-is pushed one scan past 5 GB of memory. sieve cuts them down to what matching needs before caching.
  • Debian lists the fix in unstable. If you don't match your exact release, every curl on the box looks fixable. It isn't.
  • OSV's Alpine import lags. Alpine's own security database is merged in to cover the gap.

The bug the rewrite found

This one's a bit embarrassing. When I pulled the scanner out of warden and wrote proper tests for it, I found that for Debian 10 and 11, OSV only had the summary security notices (DSA/DLA), not one record per CVE. warden was skipping those notices as duplicates.

So an old buster or bullseye container could look clean while it had PwnKit (CVE-2021-4034) and Baron Samedit (CVE-2021-3156) in it. Both are on the known-exploited list.

sieve now expands a notice into its CVEs whenever nothing else already reported them, and the fix is going back into warden too. If you run old Debian images anywhere, that's worth checking whatever scanner you use.

Version 0.2: is it actually running?

That was version 0.1. A few people starred it on day one, so I spent a day on the things I'd want next.

Is the vulnerable code even loaded? Most scanners report a vulnerable library whether anything uses it or not. sieve now reads /proc/<pid>/maps, sees which libraries are actually loaded into running processes, and traces each one back to its package. On my Docker host with 11 images it found 6,771 findings. Only 1,162 of them, 17%, were in code that was running. Another 4,508 definitely weren't, and for 1,101 it couldn't tell (scripts, and languages it can't trace yet). The running ones go to the top. It also marks anything where an upgrade replaced the file but the old copy is still running, which means you need a restart, not another upgrade.

It's careful about what it can't see. Without root it only sees your own processes, so it tells you how many it read and never says "not loaded" when it doesn't know.

Known-malicious packages come first. OSV.dev also carries OpenSSF's list of known-malicious packages. sieve now puts those above everything, even actively exploited CVEs, and fails the run. I tested it with a lockfile that just names [email protected], the old event-stream backdoor (nothing installed), and it came straight back as malware.

It scans projects before you install them. sieve path reads package-lock.json, yarn.lock, pnpm-lock.yaml, requirements.txt, poetry.lock and uv.lock. On my own box, sieve host also found 221 globally installed npm packages that no scan had ever looked at.

sieve verify checks nothing's been tampered with. Debian, Alpine, RHEL and pip all keep a checksum for every file they install. sieve re-hashes the files and compares. 70,795 files on my Proxmox node took 34 seconds. It's a tripwire, not proof: anyone with root can rewrite the checksum lists too. It did find one real thing: in one of my container images, an unrelated package called jwt had overwritten three of PyJWT's files at build time.

Plus: sieve remote user@host scans another machine over SSH with nothing installed there, Rocky, Alma, RHEL and openSUSE are covered now, and there's SARIF output so results show up in GitHub's Security tab.

Two bugs came out of testing on real images. On a Red Hat style Docker host, container scans would have read the host's package database instead of the container's, because of how SQLite resolves /proc paths. And one bad openSUSE record in OSV made every Tumbleweed box look vulnerable to Shellshock, a bug from 2014. Both are handled now.

What it can't do

Being straight about the limits:

  • Forks, renamed packages and code copied into a project are invisible. If no package database says it's there, it can't be matched.
  • Kernel packages are skipped by default (containers use the host's kernel), and vendor kernels like Proxmox's aren't in any feed.
  • Scanning containers needs root, because the container filesystems under /proc are root-only.
  • OS packages are covered for Debian, Ubuntu, Alpine, AlmaLinux, Rocky, RHEL and openSUSE. Fedora, CentOS Stream and Amazon Linux have no OSV feed, so they're counted but not checked.
  • npm keeps no per-file checksums, so sieve verify can't check npm packages.
  • Package names and versions go to OSV.dev. Nothing else about the machine does.

Try it

pipx install sieve-scan          # on PyPI; the command is `sieve`
sieve host
sudo sieve containers
sudo sieve host --in-use-only
sieve path ~/my-project
sieve remote you@another-box

It's free and open source (AGPL-3.0): github.com/casareanderson/sieve. If you want the full thing with multiple machines, history, a console and patching, that's warden. sieve is just the scanner, on its own.

What do you run for vulnerability scanning at home, if anything? And has anyone else been bitten by a scanner reading the image instead of what's running?

🤖 Drafted with AI assistance from my own homelab notes, logs and repos, then reviewed and edited before publishing.

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.