CVE's, SBOM & supply chaingevorderd5 min lezen

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
  1. Stap 1: de inventaris zoals OSV hem wil
  2. Stap 2: OSV.dev bevragen in batches
  3. Stap 3: van OSV-id naar CVE, severity en fix-versie
  4. Stap 4: prioriteren met EPSS en KEV
  5. Stap 5: automatiseren en bewaren
  6. Valkuilen
  7. Wat je hiermee nog niet hebt
  8. Zo doet monsys het
  9. FAQ

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

  • universe heeft geen security-support op Ubuntu. Packages uit universe (controleer met apt-cache policy <pkg>) krijgen geen USN's, dus OSV ziet "geen fix" terwijl upstream allang gepatcht is. Ubuntu Pro (gratis tot 5 machines) dekt universe via ESM.
  • Debian's tracker kent <unfixed> en <no-dsa>. Een CVE met no-dsa is 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 libssl3t64 op de nieuwe en openssl op de oude versie hebben. sort -u op source+versie laat beide zien; apt list --upgradable vertelt waarom.
  • Snap- en pip-packages zie je niet. dpkg weet niets van snap list of pip list. Voor applicatie-dependencies (npm, pip, composer) is een aparte scan nodig met de ecosystem-strings npm, PyPI, Packagist.
  • De kernel is een verhaal apart. linux-image-* staat in de lijst, maar de vraag "draai ik de gepatchte kernel" gaat over uname -r, niet over dpkg. 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 een grep -l over ssh.
  • Context. Een critical CVE in libxml2 op 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.