NIS2, ISO 27001 & bewijsgevorderd5 min lezen

ISO 27001 A.8.8 bewijzen met logs in plaats van met een Word-document: technische kwetsbaarheden beheren

Control A.8.8 vraagt dat je kwetsbaarheden tijdig identificeert, beoordeelt en aanpakt. De meeste organisaties bewijzen dat met een beleid en een screenshot van een scanner. Een auditor wil de keten zien: wanneer werd de CVE bekend, wanneer zag jij hem, wat besliste je, wanneer was hij dicht. Dit is hoe je die vier tijdstippen automatisch uit je servers haalt en er een metriek van maakt — MTTR per severity — die je elk kwartaal kunt tonen.

Inhoud
  1. De vier tijdstempels
  2. Stap 1: T1 en T3 uit je scan-historie
  3. Stap 2: T0 uit de bron
  4. Stap 3: T2 — de beslissing vastleggen
  5. Stap 4: de metriek — MTTR per severity
  6. Stap 5: het kwartaalbewijs
  7. Valkuilen
  8. Wat je hiermee nog niet hebt
  9. Zo doet monsys het
  10. FAQ

ISO 27001:2022, Annex A control 8.8, Management of technical vulnerabilities: "Information about technical vulnerabilities of information systems in use shall be obtained, the organization's exposure to such vulnerabilities shall be evaluated and appropriate measures shall be taken." Drie werkwoorden — verkrijgen, beoordelen, aanpakken — en een auditor toetst alle drie. Een beleid in Word bewijst dat je het wilt. Een scanner-screenshot bewijst dat je op één dag keek. Wat een auditor overtuigt, is een tabel: per kwetsbaarheid vier datums, en daaruit een doorlooptijd die je zelf hebt vastgesteld en die je haalt.

De vier tijdstempels

#TijdstipBronBewijst
T0Kwetsbaarheid gepubliceerdUSN/DSA-datum, OSV published— (extern)
T1Jij wist hetJe scan-historie: eerste tsv waarin de CVE voorkomtverkrijgen (A.8.8 "obtained")
T2Je beslisteBeslissingsregel: patchen / mitigeren / accepteren, met naambeoordelen ("evaluated")
T3Dichtdpkg.log-installatie van de fix, of eerste scan waarin de CVE weg isaanpakken ("measures taken")

Twee afgeleide getallen: T1 − T0 (hoe snel merk je het) en T3 − T1 (hoe snel los je het op, de MTTR). De eerste wil je onder de 7 dagen; de tweede is je eigen norm per severity — en de auditor toetst of je hem haalt, niet of hij streng is.

Stap 1: T1 en T3 uit je scan-historie

De wekelijkse scan uit CVE's vinden met OSV.dev bewaart per run een tsv in /var/lib/cve-scan/. Dat is je bron voor T1 (eerste voorkomen) en T3 (eerste afwezigheid na voorkomen):

sudo tee /usr/local/sbin/cve-lifecycle.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
# Leest alle scans in /var/lib/cve-scan/*.tsv (kolom 1 = CVE) en print per CVE: first_seen, last_seen, closed
D=/var/lib/cve-scan
for f in "$D"/*.tsv; do d=$(basename "$f" .tsv); cut -f1 "$f" | grep '^CVE-' | sed "s/$/\t$d/"; done \
| sort -k1,1 -k2,2 \
| awk -F'\t' '
  { if (!($1 in first)) first[$1]=$2; last[$1]=$2 }
  END {
    # laatste scan-datum bepaalt of een CVE nog open is
    for (c in last) printf "%s\t%s\t%s\n", c, first[c], last[c]
  }' > /tmp/lifecycle.tsv
latest=$(ls -1 "$D"/*.tsv | tail -1 | xargs -n1 basename | sed 's/.tsv//')
awk -F'\t' -v latest="$latest" '{ status = ($3==latest) ? "open" : "closed"; print $0"\t"status }' /tmp/lifecycle.tsv
EOF
sudo chmod 0755 /usr/local/sbin/cve-lifecycle.sh
sudo /usr/local/sbin/cve-lifecycle.sh | column -t | head
# CVE-2026-1234  2026-07-07  2026-08-04  closed   ← T1=07-07, T3≈08-04 (+ scan-interval)
# CVE-2026-5678  2026-08-11  2026-09-15  open

T3 uit scans heeft een onnauwkeurigheid van één scan-interval (wekelijks: tot 7 dagen). Preciezer is dpkg.log:

# Wanneer werd het package dat CVE-2026-1234 fixt geïnstalleerd?
pkg=openssl; fixver=3.0.13-0ubuntu3.5
grep -h " upgrade $pkg[: ]" /var/log/dpkg.log* | grep "$fixver" | awk '{print $1, $2}' | head -1
# 2026-08-02 03:14:07   ← T3 exact

Combineer beide: T3 = dpkg-datum als die bekend is, anders de eerste scan zonder de CVE.

Stap 2: T0 uit de bron

# Ubuntu USN/OSV: publicatiedatum per CVE
curl -s "https://api.osv.dev/v1/vulns/UBUNTU-CVE-2026-1234" | jq -r '.published[0:10]'
# Debian: security-tracker JSON heeft geen datum per CVE; gebruik de DSA-datum of NVD:
curl -s "https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=CVE-2026-1234" | jq -r '.vulnerabilities[0].cve.published[0:10]'

Bouw daar een lookup-tabel van (cve\tpublished) die je bij elke lifecycle-run aanvult; de NVD-API heeft een rate-limit van ~5 requests per 30 s zonder key, dus cache.

Stap 3: T2 — de beslissing vastleggen

Dit is de stap die geen tool voor je doet, en de stap waar A.8.8 om draait ("exposure evaluated"). Eén regel per CVE, in een bestand dat je bewaart:

sudo install -d /srv/inventory && sudo touch /srv/inventory/cve-decisions.tsv
# cve  datum  beslissing  wie  reden
printf 'CVE-2026-1234\t2026-07-08\tpatch\tj.peeters\tfix in -security, uitrol via unattended-upgrades\n' | sudo tee -a /srv/inventory/cve-decisions.tsv
printf 'CVE-2026-5678\t2026-08-12\taccept\tj.peeters\tlibxml2 XInclude niet bereikbaar; VEX not_affected; herbeoordelen bij release 2026.10\n' | sudo tee -a /srv/inventory/cve-decisions.tsv
printf 'CVE-2026-9012\t2026-08-12\tmitigate\tm.dubois\tgeen fix; WAF-regel 4471 blokkeert het pad; ticket OPS-812 voor upgrade\n' | sudo tee -a /srv/inventory/cve-decisions.tsv

Drie beslissingen zijn genoeg: patch, mitigate, accept. Een accept zonder reden is geen beslissing; een accept van een KEV-CVE is een rode vlag die je beter kunt uitleggen vóór de auditor ernaar vraagt. Voor accept hoort een VEX-statement bij het product.

Stap 4: de metriek — MTTR per severity

Voeg alles samen en bereken per kwartaal:

sudo tee /usr/local/sbin/cve-mttr.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
# Combineert lifecycle (T1/T3), published (T0), decisions (T2) en severity (bevroren op eerste detectie) tot één tabel + KPI's.
D=/var/lib/cve-scan; Q=${1:-$(date +%Y-Q$(( ($(date +%-m)-1)/3+1 )))}
/usr/local/sbin/cve-lifecycle.sh > /tmp/lc.tsv
# severity + KEV per CVE, bevroren op de EERSTE scan waarin hij voorkwam (kolom 6 = severity, kolom 3 = KEV in de OSV-how-to)
cat "$D"/*.tsv | awk -F'\t' '$1 ~ /^CVE-/ && !($1 in s) { s[$1]=($6?$6:"unknown"); k[$1]=$3 } END { for (c in s) print c"\t"s[c]"\t"k[c] }' | sort > /tmp/sev.tsv
join -t $'\t' <(sort /tmp/lc.tsv) <(sort /tmp/sev.tsv) 2>/dev/null \
| join -t $'\t' -a1 - <(sort /srv/inventory/cve-published.tsv 2>/dev/null) \
| join -t $'\t' -a1 - <(sort /srv/inventory/cve-decisions.tsv 2>/dev/null) \
> "/srv/inventory/cve-mttr-$Q.tsv"

awk -F'\t' -v q="$Q" '
  function days(a,b,  c1,c2){ gsub(/-/," ",a); gsub(/-/," ",b); c1=mktime(a" 0 0 0"); c2=mktime(b" 0 0 0"); return (c2-c1)/86400 }
  $4=="closed" { n[$5]++; s[$5]+=days($2,$3); if ($7!="") { d[$5]++; sd[$5]+=days($7,$2) } }
  $4=="open"   { o[$5]++ }
  END {
    print "kwartaal", q
    printf "%-10s %6s %8s %10s %8s\n", "severity", "closed", "MTTR(d)", "detect(d)", "open"
    for (sev in n) printf "%-10s %6d %8.1f %10s %8d\n", sev, n[sev], s[sev]/n[sev], (d[sev]? sprintf("%.1f", sd[sev]/d[sev]) : "-"), o[sev]
    for (sev in o) if (!(sev in n)) printf "%-10s %6d %8s %10s %8d\n", sev, 0, "-", "-", o[sev]
  }' "/srv/inventory/cve-mttr-$Q.tsv"
EOF
sudo chmod 0755 /usr/local/sbin/cve-mttr.sh
sudo /usr/local/sbin/cve-mttr.sh
# kwartaal 2026-Q3
# severity   closed  MTTR(d)  detect(d)     open
# CRITICAL        4      3.2        1.5        0
# HIGH           19      9.8        2.1        2
# MEDIUM         51     24.0        3.0       14

MTTR(d) is T3 − T1 gemiddeld; detect(d) is T1 − T0. Zet je eigen norm ernaast (bijvoorbeeld: CRITICAL ≤ 7 d, HIGH ≤ 30 d, MEDIUM ≤ 90 d) en het kwartaalverslag schrijft zichzelf: gehaald of niet, en zo niet, welke CVE's de uitschieters waren en waarom (uit cve-decisions.tsv).

Stap 5: het kwartaalbewijs

Q=$(date +%Y-Q$(( ($(date +%-m)-1)/3+1 )))
R=/srv/evidence/a88-$Q; sudo install -d "$R"
sudo /usr/local/sbin/cve-mttr.sh "$Q" | sudo tee "$R/kpi.txt"
sudo cp "/srv/inventory/cve-mttr-$Q.tsv" /srv/inventory/cve-decisions.tsv "$R/"
sudo cp /var/lib/cve-scan/*.tsv "$R/" 2>/dev/null          # de ruwe scans van het kwartaal
( cd "$R" && sudo sha256sum * > MANIFEST.sha256 )
sudo ssh-keygen -Y sign -f /etc/evidence/key -n a88 "$R/MANIFEST.sha256"

Wat de auditor krijgt: de KPI-tabel, de tabel per CVE met vier tijdstempels en de beslissing, de ruwe scans waaruit dat is afgeleid, en een handtekening die aantoont dat niets sindsdien is aangepast. Dat dekt "obtained" (scans), "evaluated" (decisions) en "measures taken" (T3 + dpkg.log) — de drie werkwoorden van A.8.8 — en levert meteen het bewijs voor NIS2 art. 21 §2 (e) op.

Valkuilen

  • Scan-gaten. Een week zonder scan (server uit, cron kapot) verschuift T1 en T3. Bewaar per scan ook een regel "scan uitgevoerd op … , N packages" — anders is afwezigheid van een CVE niet te onderscheiden van afwezigheid van een scan.
  • CVE's die verdwijnen zonder patch. Een package dat verwijderd wordt, laat zijn CVE's uit de scan verdwijnen: T3 zonder fix. Dat is legitiem (remove is een maatregel), maar noteer het als beslissing.
  • Severity die verandert. NVD past scores aan. Bevries de severity op T1 in je tabel, anders schuift een CVE tussen kwartalen van categorie.
  • Normen die niemand heeft goedgekeurd. "CRITICAL ≤ 7 dagen" moet in je beleid staan en door management zijn ondertekend; anders is de KPI een mening. Eén alinea in het beveiligingsbeleid volstaat.
  • Alleen OS-packages. A.8.8 gaat over alle "information systems in use": ook applicatie-dependencies, container-images, firmware. Begin met OS-packages, breid uit, en schrijf in de scope wat je nog niet dekt.

Wat je hiermee nog niet hebt

  • Fleet-breed. De scripts werken per host. Een CVE die op vijf hosts open stond en op vier gedicht is, heeft vijf lifecycles; de KPI over de vloot is een join die je zelf moet schrijven.
  • Continue T3. Wekelijkse scans geven T3 op een week nauwkeurig; dpkg.log is exact maar handwerk om te koppelen.
  • De beslissingen op één plek. cve-decisions.tsv per host of centraal? Centraal, met host-kolom — en dan is het een database geworden.

Zo doet monsys het

monsys houdt per CVE, per host en per applicatie de volledige lifecycle bij: eerste detectie, publicatiedatum uit de bron, de beslissing (met VEX-status en wie hem nam), en het moment waarop de CVE verdween — met een resolution-log dat niet wordt overschreven bij een rescan, zodat MTTR retroactief meetbaar blijft. De KPI's per severity staan in de Trust Score (patch_hygiene) en in het maandelijkse audit pack, met de control ISO 27001 A.8.8 als dekkingspercentage. Een kwartaal waarin de norm niet gehaald wordt, zie je vóór de review, niet erin.

FAQ

Welke MTTR-norm verwacht een auditor?

Geen vaste. ISO 27001 vraagt dat je zelf een norm definieert, passend bij je risico, en aantoont dat je hem haalt of afwijkingen verklaart. Gangbaar in de sector: kritiek ≤ 7–14 dagen, hoog ≤ 30, medium ≤ 90. KEV-CVE's: zo snel mogelijk, meestal ≤ 72 uur.

Is een scanner-rapport geen bewijs?

Het bewijst "obtained" op één dag. Het bewijst niet dat je beoordeeld hebt, niet dat je gehandeld hebt, en niet hoe lang dat duurde. De tabel met vier tijdstempels per CVE plus de beslissingsregels dekt alle drie.

Hoe lang bewaar ik dit?

Minstens drie jaar (twee audit-cycli plus marge); de ruwe scans mag je na een jaar comprimeren, de KPI-tabellen en beslissingen niet. Voor NIS2 geldt dezelfde termijn als voor ander technisch bewijs.

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.