Pour les RSSI + les équipes d'ingénierie · 2026-09-01

Que saviez-vous, et quand ? Pourquoi nos déclarations VEX portaient la mauvaise date

Une déclaration VEX qui affirme « not affected » sans justification n'est pas une conclusion, c'est une opinion horodatée. Et si cet horodatage correspond au moment de l'export, il ne prouve même pas ce que vous saviez. Nous avons trouvé ces deux problèmes dans notre propre implémentation. Voici ce que nous avons changé.

Un fil de discussion a circulé la semaine dernière sur LinkedIn, et il posait exactement la bonne question. Le contexte : dans dix jours, la première obligation du Cyber Resilience Act pour les fabricants entre en vigueur, et dans presque chaque conversation à ce sujet revient la même phrase, nous avons un SBOM.

L'observation qui a suivi est plus tranchante qu'il n'y paraît. Dans l'Annexe VII, la liste de ce que votre documentation technique doit contenir, le SBOM est le point huit sur huit. Les sept points qui le précèdent sont la description du produit, la conception et l'architecture, l'évaluation des risques au regard de chaque exigence essentielle, la justification de votre période de support, les normes appliquées, les rapports de test et la déclaration de conformité. Un point porte sur ce qu'il y a à l'intérieur. Les sept autres doivent démontrer ensemble si vous saviez ce que vous faisiez.

Sont ensuite venues quatre questions auxquelles vous devriez pouvoir répondre pour n'importe quel SBOM :

  1. De quel artefact s'agit-il ?
  2. À quel moment de la build a-t-il été généré ?
  3. Avec quels identifiants ?
  4. Qui a établi qu'il était correct ?

Si vous ne pouvez pas répondre à ces quatre questions, vous avez un fichier, pas un inventaire.

Nous exportons des SBOM et des documents VEX, nous avons donc confronté notre propre implémentation à ce critère. Deux choses n'ont pas tenu. Cet article porte sur ce que nous avons trouvé et sur ce que nous en avons fait.

D'abord une distinction que l'on perd trop souvent

Il existe deux types de SBOM qui portent tous deux le nom de SBOM, et la différence détermine ce que vous avez le droit d'en faire.

Un SBOM de build est généré dans votre pipeline, à partir de vos propres sources et fichiers de verrouillage. Il décrit ce que vous avez livré. C'est l'artefact que l'Annexe VII demande à un fabricant.

Un SBOM d'exécution est dérivé d'un système en fonctionnement. Il décrit ce qui est réellement installé aujourd'hui, y compris les paquets de la distribution et le noyau arrivés bien après votre build, et y compris ce que quelqu'un a installé à la main. C'est ce dont les équipes d'exploitation ont besoin.

Ils répondent à des questions différentes et se périment à des vitesses différentes. Un SBOM de build est définitif au moment où votre version part. Un SBOM d'exécution du mois dernier ne dit rien d'aujourd'hui.

Monsys génère le second type. Ce n'est pas une lacune, c'est la mission : nous regardons vers l'extérieur depuis l'hôte, et non vers l'intérieur depuis le serveur de build. Mais cela doit figurer dans le document, sinon un auditeur lira votre inventaire opérationnel comme de la documentation produit. Nous l'inscrivons donc dans le fichier plutôt que de le laisser à l'interprétation du lecteur :

"lifecycles": [{ "phase": "operations" }],
"properties": [
  { "name": "monsys:generation_context", "value": "after-build (runtime host inventory)" },
  { "name": "monsys:component_hashes", "value": "unknown (metadata-only, no artifact access)" }
]

Cette seconde ligne répond à la question trois, et elle est inconfortable. Depuis les 2026 Minimum Elements, la CISA demande les empreintes des composants. Nous lisons les métadonnées des paquets, pas les artefacts eux-mêmes, donc nous ne connaissons pas ces empreintes. Nous écrivons alors « inconnu ». Une empreinte inventée est pire qu'une empreinte manquante, car une empreinte manquante est honnête et une empreinte inventée est une erreur qui n'apparaît que lorsque quelqu'un tente de la vérifier.

Pourquoi le VEX est la partie la plus difficile

Un SBOM est une liste. Un document VEX est un jugement : pour chaque vulnérabilité, il établit si ce système est réellement affecté.

Dans ce même fil est venue la remarque qui nous a le plus marqués. Trop d'organisations utilisent « not affected » comme une simple affirmation plutôt que comme une conclusion d'ingénierie défendable. Une déclaration VEX bien documentée est liée à une vulnérabilité précise, un composant, une version de produit, un statut, une justification et idéalement les preuves qui la sous-tendent. À défaut, vous avez seulement réduit le bruit de votre tableau de bord sans améliorer la qualité de votre trace décisionnelle.

C'est exactement là que le VEX déraille, car la tentation est réelle. « Not affected » fait disparaître une case rouge. C'est la façon la moins coûteuse d'embellir un rapport, et personne ne distingue une évaluation raisonnée d'une supposition, jusqu'au moment où cela compte.

Notre valeur par défaut est donc inversée. Une CVE correspondante reste affected sauf preuve positive du contraire :

Source de preuveStatut résultant
OSV correspond à la version installée, rien d'autre n'est connuaffected, avec une action concrète
La distribution a rétroporté le correctif dans cette version du paquetfixed
Mitigation noyau active dans le noyau en cours d'exécutionnot_affected, avec inline_mitigations_already_exist
L'opérateur a explicitement accepté le risque, avec un motifnot_affected, ce motif servant d'impact statement
Statut noyau inconnu chez la distributionunder_investigation

Le silence ne figure pas dans ce tableau. L'absence de preuve n'est pas une justification, raison pour laquelle notre exportateur ne peut pas produire un not_affected sans que quelque chose de concret y soit rattaché.

Les deux choses que nous avions mal faites

Nous en étions là quand le fil est passé. Ce que nous avons trouvé ensuite était moins confortable.

Problème un : chaque déclaration portait l'heure de l'export.

La spécification OpenVEX attribue un timestamp à chaque déclaration. Nous y inscrivions le moment où vous cliquiez sur le bouton de téléchargement. Toutes les déclarations du document portaient donc la même heure, et cette heure était cet après-midi.

C'est du JSON techniquement valide et matériellement sans valeur. Cela prouve ce que vous affirmez maintenant, pas ce que vous saviez alors. Or c'est précisément là-dessus que porte une enquête ultérieure : que savait l'organisation, quand l'a-t-elle su, et ce qu'elle a fait ensuite était-il proportionné. Un document où tout s'est passé aujourd'hui ne peut pas répondre à cette question, par construction.

Problème deux : le fichier VEX autonome n'était pas signé.

Nos SBOM portent depuis juillet une signature Ed25519 sur la forme canonique du document. Nos Audit Packs sont signés. Le VEX contenu dans un Audit Pack relevait de cette signature. Mais le fichier VEX que vous téléchargez séparément et transférez par courriel, l'exemplaire qui circule le plus en pratique, n'en avait aucune. Une incohérence que personne n'avait signalée, et située exactement au mauvais endroit.

Ce que nous avons changé

L'horodatage porte désormais le sens qu'il doit porter. Pour chaque déclaration, c'est la date à laquelle la preuve derrière le statut est apparue, pas la date du document :

Statuttimestamp est désormais
affectedle moment de la première détection sur cet hôte
under_investigationle moment de la première détection
fixedle moment où le correctif a été constaté
not_affected via une exception acceptéele moment où l'opérateur a consigné cette décision

S'y ajoute un last_updated par déclaration, dès que la constatation sous-jacente a été reconfirmée. Cela sépare deux choses souvent confondues : quand l'avons-nous su pour la première fois, et quand l'avons-nous vérifié pour la dernière fois. Une constatation de mars revue ce matin n'a rien à voir avec une constatation de mars que personne n'a regardée depuis.

En CycloneDX, cela se traduit par analysis.firstIssued et analysis.lastUpdated. En OpenVEX, par timestamp et last_updated sur la déclaration elle-même, tandis que l'horodatage du document continue de faire ce qu'il doit faire, à savoir indiquer quand ce document a été produit.

Concrètement, avant et après :

{
  "vulnerability": { "name": "CVE-2026-21708" },
  "products": [{ "@id": "pkg:deb/ubuntu/libxml2@2.9.13+dfsg-1ubuntu0.4?distro=jammy" }],
  "status": "affected",
  "action_statement": "Upgrade libxml2 to 2.9.13+dfsg-1ubuntu0.5 or later.",
  "timestamp": "2026-04-11T02:14:56Z",
  "last_updated": "2026-09-01T04:03:12Z"
}

Auparavant, les deux champs contenaient l'heure de l'export. Désormais, le document indique que nous avons vu cette vulnérabilité pour la première fois sur cet hôte le 11 avril et qu'elle était encore présente ce matin. C'est une affirmation vérifiable. La version précédente ne l'était pas.

Et le fichier VEX autonome est maintenant signé, dans les deux formats. CycloneDX dispose d'un champ natif pour cela. OpenVEX n'en a pas, la signature réside donc dans son propre espace de noms, en précisant ce qui a été signé :

"monsys:signature": {
  "algorithm": "Ed25519",
  "keyId": "b5e1f5546d660fda",
  "canonical": "RFC8785-JCS",
  "excludes": ["monsys:signature"],
  "value": "…"
}

C'est la même clé que pour les SBOM et les Audit Packs, et la même canonicalisation (RFC 8785). Un seul vérificateur couvre les trois. La clé publique se trouve sur /api/v1/keys, et la vérification se fait sans nous :

curl -s "https://api.monsys.ai/api/v1/agents/$AGENT/vex?format=openvex" -b cookies.txt > vex.json
# extraire monsys:signature, canonicaliser le reste selon la RFC 8785,
# vérifier la signature Ed25519 contre la clé publiée sur /api/v1/keys

Nous l'avons reconstruit nous-mêmes dans un vérificateur autonome qui ne partage aucune de nos lignes de code, avec un test qui modifie un seul champ de statut et contrôle que la signature casse alors. Une signature que vous n'avez jamais vue échouer est une signature que vous n'avez pas testée.

Ce que cela signifie, et ce que cela ne signifie pas

Cela ne rend personne conforme au CRA. La conformité est un jugement sur l'ensemble de votre produit et de votre processus, et elle ne se règle pas par un fichier JSON.

Ce que cela fait, en revanche, c'est rendre les quatre questions ci-dessus vérifiables d'une manière qui survit à un fichier transféré. De quel artefact il s'agit y figure. À quel moment du cycle de vie il a été généré y figure. Quels identifiants ont été utilisés y figure, y compris ce que nous ignorons. Et qui a établi qu'il était correct n'est plus un nom dans un champ, mais une signature que vous pouvez vérifier sans nous.

Pour le VEX s'ajoute une cinquième question, qui est en réalité la plus importante : sur quelle base, et depuis quand. Un statut sans justification est une opinion. Un statut avec justification mais daté de cet après-midi est une opinion horodatée. Ce n'est que lorsque la date indique le moment où vous saviez que cela devient un compte rendu.

Nous n'avions pas cela en ordre jusqu'à la semaine dernière. C'est le cas maintenant.

Retour au blog