CVE, SBOM & chaîne d'approvisionnementintermédiaire7 min de lecture

Rédiger une déclaration VEX (et pourquoi la date compte plus que le statut)

Un scanner signale CVE-2026-1234 dans libxml2. Vous savez que la fonction vulnérable n'est jamais appelée dans votre configuration. Une déclaration VEX est la façon de le consigner — lisible par machine, avec une raison, et avec la date à laquelle vous le saviez. Voici le format OpenVEX en vingt lignes, les quatre statuts et quand les utiliser, comment signer avec ssh-keygen, et comment les scanners utilisent la déclaration pour supprimer le bruit.

Sommaire
  1. Les quatre statuts
  2. Le format : OpenVEX
  3. Pourquoi la date compte plus que le statut
  4. Signer
  5. Faire utiliser la déclaration par les scanners
  6. Le processus (car le format est la partie facile)
  7. Pièges
  8. Ce qu'il vous manque encore
  9. Comment monsys fait
  10. FAQ

Un SBOM dit ce qu'il y a dedans. Un scanner dit quelles CVE vont avec. Ni l'un ni l'autre ne dit si ces CVE comptent dans votre produit — et c'est la question que posent votre client, votre auditeur et (à partir de 2027, sous le CRA) le régulateur. VEX, Vulnerability Exploitability eXchange, est le format de réponse : par produit, par vulnérabilité, une déclaration avec un statut, une raison et un horodatage. C'est petit, ennuyeux et bureaucratique, et c'est la seule chose qui ramène le flux de signalements de scanner à une liste qui est juste.

Les quatre statuts

StatutSignificationQuand
not_affectedLa vulnérabilité ne touche pas ce produitLe code vulnérable n'y est pas, n'est pas appelé, ou n'est pas atteignable. Exige une justification.
affectedLe produit est vulnérableVous le savez, et aucun correctif n'est (encore) déployé. Obligatoire : ce que l'utilisateur doit faire (action_statement).
fixedRésolu dans cette versionAprès la mise à niveau ou le correctif. Indiquez la version.
under_investigationPas encore évaluéEspace réservé honnête ; mieux que ne rien dire. Avec une date à laquelle vous saurez.

Les cinq valeurs de justification admises pour not_affected (selon la spécification CISA) :

  • component_not_present — le paquet est dans le SBOM mais le composant vulnérable (p. ex. un module optionnel) n'est pas compilé ni installé
  • vulnerable_code_not_present — la fonction vulnérable n'est pas dans votre build (p. ex. exclue à la compilation, ou une autre version de ce fichier)
  • vulnerable_code_not_in_execute_path — le code est là mais n'est jamais atteint dans votre usage
  • vulnerable_code_cannot_be_controlled_by_adversary — atteignable, mais l'entrée ne vient jamais d'un attaquant
  • inline_mitigations_already_exist — atteignable, mais une autre mesure (sandbox, pare-feu, seccomp) bloque l'exploitation

Choisissez la plus étroite qui soit vraie. inline_mitigations_already_exist est la plus faible (l'atténuation peut disparaître) ; component_not_present la plus forte.

Le format : OpenVEX

Il existe trois formats VEX (CSAF VEX, CycloneDX VEX, OpenVEX). OpenVEX est le plus petit et est lu par grype, trivy et osv-scanner. Un document, plusieurs déclarations :

mkdir -p /srv/vex && cat > /srv/vex/shop-2026.09.json <<'EOF'
{
  "@context": "https://openvex.dev/ns/v0.2.0",
  "@id": "https://example.be/vex/shop-2026.09.json",
  "author": "Example BV Security <security@example.be>",
  "role": "Product Security",
  "timestamp": "2026-09-16T09:00:00+02:00",
  "version": 1,
  "statements": [
    {
      "vulnerability": { "name": "CVE-2026-1234" },
      "timestamp": "2026-09-16T09:00:00+02:00",
      "products": [
        { "@id": "pkg:docker/example/shop@2026.09.1",
          "subcomponents": [ { "@id": "pkg:deb/ubuntu/libxml2@2.9.14+dfsg-1.3ubuntu3.4?distro=ubuntu-24.04" } ] }
      ],
      "status": "not_affected",
      "justification": "vulnerable_code_not_in_execute_path",
      "impact_statement": "libxml2 est utilisé exclusivement par php-xml pour parser nos propres fichiers de configuration statiques ; aucun XML fourni par des utilisateurs n'est parsé. Le code XInclude vulnérable n'est pas atteint."
    },
    {
      "vulnerability": { "name": "CVE-2026-5678" },
      "timestamp": "2026-09-16T09:00:00+02:00",
      "products": [ { "@id": "pkg:docker/example/shop@2026.09.1" } ],
      "status": "affected",
      "action_statement": "Définissez la variable d'environnement SHOP_DISABLE_EXPORT=1 jusqu'à la version 2026.09.2 (attendue le 2026-09-20).",
      "action_statement_timestamp": "2026-09-16T09:00:00+02:00"
    },
    {
      "vulnerability": { "name": "CVE-2026-0042" },
      "timestamp": "2026-09-16T09:00:00+02:00",
      "products": [ { "@id": "pkg:docker/example/shop@2026.09.1" } ],
      "status": "fixed"
    }
  ]
}
EOF
jq . /srv/vex/shop-2026.09.json >/dev/null && echo "JSON valide"

Les identifiants de produit sont des purls (package URLs) : pkg:docker/…, pkg:deb/…, pkg:npm/…. Utilisez les mêmes purls que dans votre SBOM, sinon un scanner ne peut pas relier la déclaration. Pour un produit interne sans registre : pkg:generic/example/shop@2026.09.1.

Pourquoi la date compte plus que le statut

Une déclaration VEX est une affirmation à un instant donné. not_affected le 16 septembre peut être faux le 3 octobre : une nouvelle version qui ajoute l'upload XML, une correction de la fiche CVE, un nouveau chemin d'exploitation. Donc :

  • Chaque déclaration a son propre timestamp — le moment où ce jugement a été rendu, pas le moment où vous avez enregistré le document.
  • Vous ne modifiez jamais une déclaration ; vous en ajoutez une nouvelle avec un timestamp plus récent et incrémentez version. Le lecteur prend la plus récente par (produit, vulnérabilité).
  • Un auditeur qui demande « que saviez-vous le 20 septembre ? » obtient une réponse du document lui-même. Sans dates, un fichier VEX est une opinion.

Concrètement : git log sur /srv/vex ne suffit pas comme preuve temporelle (une date de commit se règle). Le timestamp dans la déclaration plus une signature sur le fichier, si.

Signer

Qui peut modifier un fichier VEX peut transformer un affected en not_affected. Signez avec une clé distincte, comme pour les archives de preuves :

# Une seule fois
sudo install -d -m 0700 /etc/vex && sudo ssh-keygen -q -t ed25519 -N '' -C 'vex@example.be' -f /etc/vex/key
# Par document
sudo ssh-keygen -Y sign -f /etc/vex/key -n vex /srv/vex/shop-2026.09.json
# → /srv/vex/shop-2026.09.json.sig
# Vérifier (client, auditeur, CI) avec seulement la clé publique :
echo "vex@example.be $(cut -d' ' -f1,2 /etc/vex/key.pub)" > allowed_signers
ssh-keygen -Y verify -f allowed_signers -I vex@example.be -n vex -s shop-2026.09.json.sig < shop-2026.09.json

Publiez key.pub à une URL fixe (p. ex. https://example.be/.well-known/vex-signing-key.pub) pour que les clients puissent vérifier la signature sans vous appeler. Si vous utilisez Sigstore/cosign : cosign sign-blob fait la même chose avec un transparency log en plus.

Faire utiliser la déclaration par les scanners

Le but de VEX est que votre scanner n'affiche plus les signalements not_affected — et affiche bien les affected, avec votre action jointe.

# Trivy : scanner une image avec filtre VEX
trivy image --vex /srv/vex/shop-2026.09.json --show-suppressed ghcr.io/example/shop:2026.09.1
# Grype
grype ghcr.io/example/shop:2026.09.1 --vex /srv/vex/shop-2026.09.json
# osv-scanner (lockfiles) : le support OpenVEX varie selon la version ; vérifiez osv-scanner --help

--show-suppressed dans Trivy montre ce que VEX a filtré, avec la justification. C'est exactement l'aperçu que vous montrez en audit : pas « nous avons zéro vulnérabilité », mais « nous en avons évalué quarante, voici pourquoi pour chacune ».

Le processus (car le format est la partie facile)

  1. Déclencheur : un nouveau signalement de scanner sur un artefact de production (scan hebdomadaire, ou mise à jour KEV).
  2. Évaluation sous 5 jours ouvrés, par quelqu'un qui connaît le code : la fonction vulnérable est-elle atteignable ? Sinon : not_affected + justification + un paragraphe impact_statement. Si oui : affected + ce que l'utilisateur doit faire maintenant + la version corrigée prévue.
  3. Ajouter la déclaration, signer le document, publier (clients internes : un chemin ; externes : une URL).
  4. Réévaluer à chaque version du produit et à chaque modification de la fiche CVE. Fixez un rappel trimestriel pour toutes les déclarations not_affected de plus d'un an.
  5. Conserver : chaque version du document, avec signature, au moins tant que le produit est supporté. Pour le CRA : la période de support plus le temps pendant lequel le régulateur peut remonter.

Pièges

  • not_affected sans justification. Invalide selon la spécification, et sans valeur pour un auditeur. La justification est la déclaration.
  • inline_mitigations_already_exist comme réponse par défaut. Une règle WAF est une atténuation jusqu'à ce que quelqu'un la désactive. Ne l'utilisez que si l'atténuation fait partie du produit lui-même, et décrivez laquelle.
  • Des purls qui ne correspondent pas. pkg:deb/debian/libxml2 versus pkg:deb/ubuntu/libxml2, ou un ?distro= manquant : le scanner ne voit pas la déclaration et affiche le signalement quand même. Testez avec --show-suppressed.
  • Une déclaration pour toutes les versions. products pointe vers une version. Une nouvelle version est une nouvelle évaluation — même si ce n'est qu'une copie avec un nouveau purl et un nouvel horodatage.
  • VEX comme excuse. Un not_affected parce que corriger est difficile n'est pas une évaluation mais un report. La justification doit être défendable par quelqu'un avec le code sous les yeux.

Ce qu'il vous manque encore

  • La vue d'ensemble. Dix produits, douze versions par an, quarante CVE par version : ce sont des milliers de déclarations dans des fichiers JSON. « Quels jugements not_affected ont plus d'un an ? » est un jq sur un répertoire que quelqu'un doit tenir en ordre.
  • Le lien avec l'inventaire. La déclaration dit « produit X version Y est not_affected ». Si la version Y tourne encore quelque part, et où, c'est dans un autre système.
  • La preuve que le processus fonctionne. Un auditeur veut non seulement les déclarations, mais aussi : combien de temps entre signalement et évaluation, et qui a évalué.

Comment monsys fait

Dans monsys, vous enregistrez une déclaration VEX sur la CVE elle-même, dans le contexte de l'hôte ou de l'application où le scanner l'a trouvée : statut, justification, texte d'impact, et l'identité de qui a rendu le jugement. Le hub génère à partir de là un document OpenVEX par produit à côté du SBOM, signe les deux avec la clé du tenant et les inclut dans l'audit pack mensuel. Parce que le hub sait quelles versions tournent où, un jugement not_affected revient automatiquement en réévaluation quand le produit reçoit une nouvelle version ou que la fiche CVE change — et le délai entre signalement et jugement est une métrique du Trust Score.

FAQ

VEX est-il obligatoire ?

Pas encore légalement, mais le CRA (Annexe I §11-12) exige que les fabricants documentent les vulnérabilités de leurs produits et les communiquent aux utilisateurs, et les CISA 2026 Minimum Elements pour les SBOM renvoient explicitement à VEX comme format compagnon. Les clients qui relèvent eux-mêmes de NIS2 le demandent de plus en plus à leurs fournisseurs.

Quel format VEX choisir ?

OpenVEX pour la simplicité et le support des scanners (Trivy, Grype). CycloneDX VEX si votre SBOM est déjà en CycloneDX et que vous voulez tout dans un seul fichier. CSAF VEX si vos clients sont dans des secteurs industriels ou publics utilisant l'outillage CSAF. Le contenu (statut, justification, date) est le même dans les trois.

Combien de temps conserver les déclarations VEX ?

Au moins tant que le produit est supporté ; sous le CRA, toute la période de support plus la durée que le régulateur peut demander (comptez dix ans après la dernière version). Conservez chaque version, pas seulement la dernière.

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.