NIS2, ISO 27001 & preuvesintermédiaire6 min de lecture

Prouver ISO 27001 A.8.8 avec des logs plutôt qu'un document Word : gérer les vulnérabilités techniques

Le contrôle A.8.8 exige d'identifier, d'évaluer et de traiter les vulnérabilités en temps utile. La plupart des organisations le prouvent avec une politique et une capture d'écran de scanner. Un auditeur veut voir la chaîne : quand la CVE est devenue connue, quand vous l'avez vue, ce que vous avez décidé, quand elle a été fermée. Voici comment extraire automatiquement ces quatre horodatages de vos serveurs et en faire une métrique — MTTR par sévérité — que vous pouvez montrer chaque trimestre.

Sommaire
  1. Les quatre horodatages
  2. Étape 1 : T1 et T3 depuis votre historique de scans
  3. Étape 2 : T0 depuis la source
  4. Étape 3 : T2 — consigner la décision
  5. Étape 4 : la métrique — MTTR par sévérité
  6. Étape 5 : la preuve trimestrielle
  7. Pièges
  8. Ce qu'il vous manque encore
  9. Comment monsys fait
  10. FAQ

ISO 27001:2022, annexe A contrôle 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. » Trois verbes — obtenir, évaluer, traiter — et un auditeur teste les trois. Une politique en Word prouve que vous le voulez. Une capture d'écran de scanner prouve que vous avez regardé un jour. Ce qui convainc un auditeur, c'est un tableau : quatre dates par vulnérabilité, et à partir de là un délai que vous avez fixé vous-même et que vous respectez.

Les quatre horodatages

#MomentSourceProuve
T0Vulnérabilité publiéedate USN/DSA, OSV published— (externe)
T1Vous saviezVotre historique de scans : premier tsv dans lequel la CVE apparaîtobtenir (A.8.8 « obtained »)
T2Vous avez décidéLigne de décision : corriger / atténuer / accepter, avec un nomévaluer (« evaluated »)
T3FerméeInstallation du correctif dans dpkg.log, ou premier scan sans la CVEtraiter (« measures taken »)

Deux chiffres dérivés : T1 − T0 (à quelle vitesse vous remarquez) et T3 − T1 (à quelle vitesse vous résolvez, le MTTR). Le premier, vous le voulez sous 7 jours ; le second est votre propre norme par sévérité — et l'auditeur vérifie si vous la respectez, pas si elle est stricte.

Étape 1 : T1 et T3 depuis votre historique de scans

Le scan hebdomadaire de trouver les CVE avec OSV.dev conserve un tsv par passage dans /var/lib/cve-scan/. C'est votre source pour T1 (première occurrence) et T3 (première absence après occurrence) :

sudo tee /usr/local/sbin/cve-lifecycle.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
# Lit tous les scans dans /var/lib/cve-scan/*.tsv (colonne 1 = CVE) et affiche par 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 {
    # la date du dernier scan détermine si une CVE est encore ouverte
    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 (+ intervalle de scan)
# CVE-2026-5678  2026-08-11  2026-09-15  open

T3 depuis les scans a une imprécision d'un intervalle de scan (hebdomadaire : jusqu'à 7 jours). dpkg.log est plus précis :

# Quand le paquet corrigeant CVE-2026-1234 a-t-il été installé ?
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

Combinez les deux : T3 = date dpkg si connue, sinon le premier scan sans la CVE.

Étape 2 : T0 depuis la source

# Ubuntu USN/OSV : date de publication par CVE
curl -s "https://api.osv.dev/v1/vulns/UBUNTU-CVE-2026-1234" | jq -r '.published[0:10]'
# Debian : le JSON du security tracker n'a pas de date par CVE ; utilisez la date DSA ou 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]'

Construisez-en une table de recherche (cve\tpublished) que vous complétez à chaque passage du lifecycle ; l'API NVD est limitée à ~5 requêtes par 30 s sans clé, donc mettez en cache.

Étape 3 : T2 — consigner la décision

C'est l'étape qu'aucun outil ne fait pour vous, et l'étape autour de laquelle tourne A.8.8 (« exposure evaluated »). Une ligne par CVE, dans un fichier que vous conservez :

sudo install -d /srv/inventory && sudo touch /srv/inventory/cve-decisions.tsv
# cve  date  décision  qui  raison
printf 'CVE-2026-1234\t2026-07-08\tpatch\tj.peeters\tcorrectif dans -security, déployé via unattended-upgrades\n' | sudo tee -a /srv/inventory/cve-decisions.tsv
printf 'CVE-2026-5678\t2026-08-12\taccept\tj.peeters\tlibxml2 XInclude non atteignable ; VEX not_affected ; réévaluer à la version 2026.10\n' | sudo tee -a /srv/inventory/cve-decisions.tsv
printf 'CVE-2026-9012\t2026-08-12\tmitigate\tm.dubois\tpas de correctif ; règle WAF 4471 bloque le chemin ; ticket OPS-812 pour mise à niveau\n' | sudo tee -a /srv/inventory/cve-decisions.tsv

Trois décisions suffisent : patch, mitigate, accept. Un accept sans raison n'est pas une décision ; un accept d'une CVE KEV est un signal d'alarme que vous feriez mieux d'expliquer avant que l'auditeur ne le demande. Un accept va avec une déclaration VEX sur le produit.

Étape 4 : la métrique — MTTR par sévérité

Rassemblez tout et calculez par trimestre :

sudo tee /usr/local/sbin/cve-mttr.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
# Combine lifecycle (T1/T3), published (T0), decisions (T2) et sévérité (figée à la première détection) en un tableau + KPI.
D=/var/lib/cve-scan; Q=${1:-$(date +%Y-Q$(( ($(date +%-m)-1)/3+1 )))}
/usr/local/sbin/cve-lifecycle.sh > /tmp/lc.tsv
# sévérité + KEV par CVE, figées au PREMIER scan où elle est apparue (colonne 6 = sévérité, colonne 3 = KEV dans le guide OSV)
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 "trimestre", 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
# trimestre 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) est T3 − T1 en moyenne ; detect(d) est T1 − T0. Mettez votre propre norme à côté (par exemple : CRITICAL ≤ 7 j, HIGH ≤ 30 j, MEDIUM ≤ 90 j) et le rapport trimestriel s'écrit tout seul : respectée ou non, et sinon, quelles CVE étaient les valeurs aberrantes et pourquoi (depuis cve-decisions.tsv).

Étape 5 : la preuve trimestrielle

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          # les scans bruts du trimestre
( cd "$R" && sudo sha256sum * > MANIFEST.sha256 )
sudo ssh-keygen -Y sign -f /etc/evidence/key -n a88 "$R/MANIFEST.sha256"

Ce que l'auditeur reçoit : le tableau des KPI, le tableau par CVE avec quatre horodatages et la décision, les scans bruts dont c'est dérivé, et une signature qui prouve que rien n'a été modifié depuis. Cela couvre « obtained » (scans), « evaluated » (décisions) et « measures taken » (T3 + dpkg.log) — les trois verbes d'A.8.8 — et sert en même temps de preuve pour NIS2 art. 21 §2 (e).

Pièges

  • Trous dans les scans. Une semaine sans scan (serveur éteint, cron cassé) décale T1 et T3. Conservez aussi par scan une ligne « scan exécuté le …, N paquets » — sinon l'absence d'une CVE ne se distingue pas de l'absence d'un scan.
  • CVE qui disparaissent sans correctif. Un paquet supprimé fait disparaître ses CVE du scan : T3 sans correctif. C'est légitime (remove est une mesure), mais consignez-le comme décision.
  • Sévérité qui change. La NVD ajuste les scores. Figez la sévérité à T1 dans votre tableau, sinon une CVE change de catégorie entre les trimestres.
  • Normes que personne n'a approuvées. « CRITICAL ≤ 7 jours » doit être dans votre politique et signé par la direction ; sinon le KPI est une opinion. Un paragraphe dans la politique de sécurité suffit.
  • Paquets OS uniquement. A.8.8 concerne tous les « information systems in use » : dépendances applicatives, images de conteneurs, firmware aussi. Commencez par les paquets OS, étendez, et écrivez dans le périmètre ce que vous ne couvrez pas encore.

Ce qu'il vous manque encore

  • À l'échelle de la flotte. Les scripts fonctionnent par hôte. Une CVE ouverte sur cinq hôtes et fermée sur quatre a cinq cycles de vie ; le KPI sur la flotte est une jointure que vous devez écrire vous-même.
  • T3 en continu. Les scans hebdomadaires donnent T3 à la semaine près ; dpkg.log est exact mais manuel à relier.
  • Les décisions en un seul endroit. cve-decisions.tsv par hôte ou central ? Central, avec une colonne hôte — et c'est alors devenu une base de données.

Comment monsys fait

monsys conserve par CVE, par hôte et par application le cycle de vie complet : première détection, date de publication depuis la source, la décision (avec statut VEX et qui l'a prise), et le moment où la CVE a disparu — avec un journal de résolution qui n'est pas écrasé au rescan, de sorte que le MTTR reste mesurable rétroactivement. Les KPI par sévérité sont dans le Trust Score (patch_hygiene) et dans l'audit pack mensuel, avec le contrôle ISO 27001 A.8.8 comme pourcentage de couverture. Un trimestre où la norme n'est pas respectée, vous le voyez avant la revue, pas pendant.

FAQ

Quelle norme de MTTR un auditeur attend-il ?

Aucune fixe. ISO 27001 exige que vous définissiez vous-même une norme, adaptée à votre risque, et que vous démontriez que vous la respectez ou expliquiez les écarts. Courant dans le secteur : critique ≤ 7–14 jours, élevé ≤ 30, moyen ≤ 90. CVE KEV : le plus vite possible, généralement ≤ 72 heures.

Un rapport de scanner n'est-il pas une preuve ?

Il prouve « obtained » un jour donné. Il ne prouve pas que vous avez évalué, ni que vous avez agi, ni combien de temps cela a pris. Le tableau avec quatre horodatages par CVE plus les lignes de décision couvre les trois.

Combien de temps conserver cela ?

Au moins trois ans (deux cycles d'audit plus marge) ; les scans bruts peuvent être compressés après un an, les tableaux de KPI et les décisions non. Pour NIS2, la même durée s'applique que pour les autres preuves techniques.

Rédigé par l'équipe monsys — des sysadmins qui font cela tous les jours.

Fait à la main ? Laissez monsys s'en charger en continu.

Tout ce que contient ce guide tourne dans monsys comme contrôle permanent, avec historique, alertes et preuves d'audit. 5 serveurs gratuits, hébergés en Belgique, installés en 60 secondes.