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
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
| # | Tijdstip | Bron | Bewijst |
|---|---|---|---|
| T0 | Kwetsbaarheid gepubliceerd | USN/DSA-datum, OSV published | — (extern) |
| T1 | Jij wist het | Je scan-historie: eerste tsv waarin de CVE voorkomt | verkrijgen (A.8.8 "obtained") |
| T2 | Je besliste | Beslissingsregel: patchen / mitigeren / accepteren, met naam | beoordelen ("evaluated") |
| T3 | Dicht | dpkg.log-installatie van de fix, of eerste scan waarin de CVE weg is | aanpakken ("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 (
removeis 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.logis exact maar handwerk om te koppelen. - De beslissingen op één plek.
cve-decisions.tsvper 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.