CVE's vinden in je Ubuntu- en Debian-packages met OSV.dev — zonder Nessus
Een script van veertig regels dat elk geïnstalleerd package tegen de gratis OSV.dev-API legt, backport-bewust, en de lijst verrijkt met EPSS-score en CISA KEV-status. Van 1 800 packages naar de vijf die je deze week moet patchen. Geen licentie, geen scanner-appliance.
Inhoud
Een vulnerability-scanner die nginx 1.24.0 ziet en 14 CVE's rapporteert, heeft meestal ongelijk: Ubuntu en Debian backporten fixes in de bestaande versie, en 1.24.0-2ubuntu7.3 bevat de patch die de scanner mist. Het gevolg is een rapport met honderden regels waarvan niemand weet welke echt zijn. Dit artikel doet het andersom: vertrek van wat dpkg weet, vraag OSV.dev — dat de Ubuntu Security Notices en de Debian Security Tracker als bron gebruikt — en verrijk alleen wat overblijft.
Stap 1: de inventaris zoals OSV hem wil
OSV kent Ubuntu- en Debian-packages onder de naam van het source package, met de ecosystem-string Ubuntu:24.04 of Debian:12. De binaire naam (libssl3t64) is niet wat je wilt; het source package (openssl) wel. dpkg-query kan dat direct leveren:
# Ubuntu 24.04 → "Ubuntu:24.04"; Debian 12 → "Debian:12"
. /etc/os-release
case "$ID" in
ubuntu) ECO="Ubuntu:$VERSION_ID" ;;
debian) ECO="Debian:$VERSION_ID" ;;
*) echo "unsupported: $ID"; exit 1 ;;
esac
# source package + versie, gededupliceerd (één source levert vaak 5 binaries)
dpkg-query -W -f='${source:Package} ${source:Version}\n' | sort -u > /tmp/pkgs.txt
wc -l /tmp/pkgs.txt # typisch 400-900 source packages op een server
Let op de versie: 1:2.4.7-1ubuntu5 — die 1: is de epoch en hoort erbij. OSV vergelijkt exact volgens de Debian-versieregels; laat hem staan.
Stap 2: OSV.dev bevragen in batches
De endpoint https://api.osv.dev/v1/querybatch neemt tot 1 000 queries per call, zonder API-key. Het antwoord bevat per query de lijst van OSV-id's (UBUNTU-CVE-…, USN-…, DSA-…, DLA-…) waarvoor jouw versie nog kwetsbaar is — een gefixte backport telt niet mee, want de USN zegt welke versie de fix bevat.
sudo apt install -y jq curl
# Bouw de JSON-body (max 1000 per batch; we splitsen)
split -l 900 /tmp/pkgs.txt /tmp/pkgbatch.
: > /tmp/osv-raw.jsonl
for f in /tmp/pkgbatch.*; do
jq -Rn --arg eco "$ECO" '
{queries: [inputs | split(" ") | {package: {name: .[0], ecosystem: $eco}, version: .[1]}]}' "$f" \
| curl -s -X POST https://api.osv.dev/v1/querybatch -H 'Content-Type: application/json' -d @- \
| jq -c --slurpfile q <(jq -Rn '[inputs | split(" ")]' "$f") '
.results | to_entries[] | select(.value.vulns != null)
| {pkg: $q[0][.key][0], version: $q[0][.key][1], vulns: [.value.vulns[].id]}' \
>> /tmp/osv-raw.jsonl
done
jq -r '"\(.pkg) \(.version) \(.vulns|length)"' /tmp/osv-raw.jsonl | sort -k3 -rn | head -20
Op een bijgewerkte Ubuntu 24.04-server is de lijst kort: een handvol packages waarvoor Ubuntu nog geen fix heeft uitgebracht (of die in universe zitten, waar geen security-support op is). Op een server die drie maanden niet is gepatcht, is hij lang. Beide zijn precies het antwoord dat je wilt.
Stap 3: van OSV-id naar CVE, severity en fix-versie
Een USN- of UBUNTU-CVE-id zegt je niets over ernst. Haal per kwetsbaarheid de details op — het CVE-nummer (uit de aliassen, of uit de id zelf bij UBUNTU-CVE-…), severity, en de versie waarin het gefixt is:
: > /tmp/osv-detail.jsonl
for id in $(jq -r '.vulns[]' /tmp/osv-raw.jsonl | sort -u); do
curl -s "https://api.osv.dev/v1/vulns/$id" | jq -c --arg eco "$ECO" '{
id,
cve: (((.aliases // []) + [.id | ltrimstr("UBUNTU-")]) | map(select(startswith("CVE-"))) | first),
severity: ((.database_specific.severity // .severity[0].score // "unknown") | tostring),
fixed: ([ .affected[] | select(.package.ecosystem == $eco) | .ranges[]?.events[]? | .fixed? // empty ] | first),
summary }' >> /tmp/osv-detail.jsonl
sleep 0.1 # beleefd blijven; OSV heeft geen harde rate-limit maar wel een ops-team
done
jq -r '[.id, (.cve // "-"), .severity, (.fixed // "no fix")] | @tsv' /tmp/osv-detail.jsonl | column -t
Het veld fixed is de sleutel: staat er een versie, dan is apt upgrade de oplossing. Staat er no fix, dan is het een open kwetsbaarheid waar je een mitigatie of een risico-acceptatie voor moet documenteren.
Stap 4: prioriteren met EPSS en KEV
Een lijst van 60 CVE's is nog steeds geen werkplan. Twee gratis bronnen maken er een van:
- EPSS (FIRST.org): de kans dat een CVE in de komende 30 dagen wordt misbruikt, 0–1. Boven 0,1 is uitzonderlijk.
- KEV (CISA Known Exploited Vulnerabilities): CVE's waarvan actief misbruik bevestigd is. Staat een CVE hierin, dan is de discussie voorbij.
# KEV: één JSON, dagelijks ververst
curl -s https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json \
| jq -r '.vulnerabilities[].cveID' | sort -u > /tmp/kev.txt
# EPSS: batch van max 100 CVE's per call
jq -r '.cve // empty' /tmp/osv-detail.jsonl | sort -u > /tmp/cves.txt
: > /tmp/epss.tsv
split -l 100 /tmp/cves.txt /tmp/cvebatch.
for f in /tmp/cvebatch.*; do
curl -s "https://api.first.org/data/v1/epss?cve=$(paste -sd, "$f")" \
| jq -r '.data[] | [.cve, .epss] | @tsv' >> /tmp/epss.tsv
done
# Samenvoegen: CVE, EPSS, KEV, package, fix
jq -r 'select(.cve) | [.cve, .id, (.fixed // "no fix")] | @tsv' /tmp/osv-detail.jsonl \
| while IFS=$'\t' read -r cve osv fix; do
epss=$(awk -v c="$cve" '$1==c{print $2}' /tmp/epss.tsv); epss=${epss:-0}
kev=$(grep -qx "$cve" /tmp/kev.txt && echo KEV || echo -)
printf '%s\t%s\t%s\t%s\t%s\n' "$cve" "$epss" "$kev" "$osv" "$fix"
done | sort -k3,3r -k2,2gr | column -t
De sortering is bewust: eerst alles in KEV, dan op dalende EPSS. De bovenste vijf regels zijn je week. De onderste veertig kun je met een gerust hart tot de volgende patchronde laten liggen — en dat is een beslissing die je in een audit kunt uitleggen.
Stap 5: automatiseren en bewaren
Zet stap 1–4 in één script, draai het wekelijks en bewaar de output met datum. Twee redenen: je wilt kunnen zien sinds wanneer een CVE open staat (voor de MTTR-vraag van een auditor), en je wilt weten of iets nieuws erbij kwam of alleen de lijst verschoof.
sudo install -d /var/lib/cve-scan
echo '15 6 * * 1 root /usr/local/sbin/cve-scan.sh > /var/lib/cve-scan/$(date +\%F).tsv 2>/var/lib/cve-scan/last-error.log' \
| sudo tee /etc/cron.d/cve-scan
# Wat is er nieuw sinds vorige week?
comm -13 <(cut -f1 /var/lib/cve-scan/2026-09-08.tsv | sort) <(cut -f1 /var/lib/cve-scan/2026-09-15.tsv | sort)
Valkuilen
universeheeft geen security-support op Ubuntu. Packages uituniverse(controleer metapt-cache policy <pkg>) krijgen geen USN's, dus OSV ziet "geen fix" terwijl upstream allang gepatcht is. Ubuntu Pro (gratis tot 5 machines) dektuniversevia ESM.- Debian's tracker kent
<unfixed>en<no-dsa>. Een CVE metno-dsais door Debian als laag risico beoordeeld en wordt pas in de volgende point-release gefixt. Niet negeren, wel anders wegen. - Meerdere versies van hetzelfde source package. Na een gedeeltelijke upgrade kun je
libssl3t64op de nieuwe enopensslop de oude versie hebben.sort -uop source+versie laat beide zien;apt list --upgradablevertelt waarom. - Snap- en pip-packages zie je niet.
dpkgweet niets vansnap listofpip list. Voor applicatie-dependencies (npm, pip, composer) is een aparte scan nodig met de ecosystem-stringsnpm,PyPI,Packagist. - De kernel is een verhaal apart.
linux-image-*staat in de lijst, maar de vraag "draai ik de gepatchte kernel" gaat overuname -r, niet overdpkg. Zie kernel-CVE's en backports.
Wat je hiermee nog niet hebt
- Fleet-zicht. Dertig servers zijn dertig
.tsv-bestanden. "Welke hosts hebben CVE-2026-1234 nog open?" is eengrep -lover ssh. - Context. Een critical CVE in
libxml2op een interne build-server weegt anders dan dezelfde CVE op de reverse proxy die op poort 443 naar buiten staat. Het script weet niet welke host internet-facing is. - Historie en bewijs. De tsv's geven je "sinds wanneer", maar niet in een vorm die een auditor accepteert (ondertekend, onveranderbaar, met de beslissing "geaccepteerd risico, want …" erbij).
Zo doet monsys het
De monsys-agent stuurt de package-inventaris (source package + versie) naar de hub; de OS-package-CVE-worker legt die tegen OSV.dev en verrijkt elke match met EPSS en KEV, precies zoals hierboven. Daarbovenop komt wat een script niet kan: een hop-afstand tot internet uit de netwerk-topologie (een CVE op een internet-facing host wordt hoger gerankt), een resolution-log dat vastlegt wanneer een CVE verdween zodat MTTR meetbaar is, en een ondertekend audit-pack per maand. Voor packages die je bewust niet patcht, leg je een VEX-statement vast — met datum.
FAQ
Is OSV.dev betrouwbaar voor Ubuntu en Debian?
Ja. OSV importeert de Ubuntu Security Notices en de Debian Security Tracker rechtstreeks, met de gefixte versies erbij. Het is dezelfde data die ubuntu-security-status en debsecan gebruiken, maar via één API voor beide distributies.
Waarom rapporteert mijn scanner meer CVE's dan dit script?
Omdat de meeste scanners de upstream-versie vergelijken (nginx 1.24.0) en niet weten dat 1.24.0-2ubuntu7.3 een backport van de fix bevat. Dat heet een false positive door ontbrekende backport-kennis; OSV heeft die kennis wel omdat de distributie ze zelf aanlevert.
Hoe vaak moet ik dit draaien?
Wekelijks is een goed minimum voor een servervloot; KEV wordt dagelijks bijgewerkt, dus bij een kritiek nieuws-item (een nieuwe OpenSSH- of kernel-CVE) draai je het ad hoc. Bewaar elke run — de historie is waardevoller dan de laatste stand.
Geschreven door het monsys-team — sysadmins die dit dagelijks doen.
Zelf gedaan? Laat monsys het bijhouden.
Alles uit deze how-to draait in monsys als doorlopende check, met historie, alerts en audit-bewijs. 5 servers gratis, EU-gehost in België, geïnstalleerd in 60 seconden.