Voor CISO's + engineering-teams · 2026-09-01

Wat wist je, en wanneer? Waarom onze VEX-statements de verkeerde datum droegen

Een VEX-statement dat 'not affected' zegt zonder onderbouwing is geen conclusie, het is een mening met een tijdstempel. En als die tijdstempel het exportmoment is, bewijst hij niet eens wat je wist. Wij vonden allebei die problemen in onze eigen implementatie. Dit is wat we veranderd hebben.

Op LinkedIn liep vorige week een draad die precies de juiste vraag stelde. De aanleiding: over tien dagen wordt de eerste verplichting uit de Cyber Resilience Act voor fabrikanten van kracht, en in bijna elk gesprek daarover valt dezelfde zin, we hebben een SBOM.

De observatie die daarop volgde is scherper dan ze op het eerste gezicht lijkt. In Annex VII, de lijst van wat je technische documentatie moet bevatten, is de SBOM punt acht van acht. De zeven punten ervoor zijn de productbeschrijving, het ontwerp en de architectuur, de risicobeoordeling per essentiële eis, de onderbouwing van je ondersteuningsperiode, de toegepaste normen, de testrapporten en de conformiteitsverklaring. Eén punt gaat over wat erin zit. De andere zeven moeten samen aantonen of je wist wat je aan het doen was.

Daarop volgden vier vragen die je over elke SBOM zou moeten kunnen beantwoorden:

  1. Over welk artefact gaat dit?
  2. Op welk punt in de build is het gegenereerd?
  3. Met welke identifiers?
  4. Wie heeft vastgesteld dat het klopte?

Kun je die vier niet beantwoorden, dan heb je een bestand, geen inventaris.

Wij exporteren SBOM's en VEX-documenten, dus we hebben onze eigen implementatie langs die lat gelegd. Twee dingen bleken niet in orde. Dit stuk gaat over wat we vonden en wat we eraan gedaan hebben.

Eerst een onderscheid dat te vaak wegvalt

Er bestaan twee soorten SBOM's die allebei SBOM heten, en het verschil bepaalt waar je hem voor mag gebruiken.

Een build-time SBOM wordt in je pijplijn gegenereerd, uit je eigen bronnen en lockfiles. Die beschrijft wat je hebt uitgeleverd. Dat is het artefact dat Annex VII van een fabrikant vraagt.

Een runtime SBOM wordt afgeleid van een draaiend systeem. Die beschrijft wat er vandaag daadwerkelijk geïnstalleerd staat, inclusief de distributiepakketten en de kernel die er lang na jouw build bij zijn gekomen, en inclusief de pakketten die iemand er met de hand op heeft gezet. Dat is wat operations nodig heeft.

Ze beantwoorden verschillende vragen en ze verouderen op verschillende snelheden. Een build-SBOM is definitief op het moment dat je release de deur uit gaat. Een runtime-SBOM van vorige maand zegt niets over vandaag.

Monsys genereert het tweede type. Dat is geen tekortkoming, het is de opdracht: wij kijken vanaf de host naar buiten, niet vanaf de buildserver naar binnen. Maar het moet wel in het document staan, anders leest een auditor jouw operationele inventaris als productdocumentatie. Daarom zetten we het expliciet in het bestand in plaats van het aan de lezer over te laten:

"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)" }
]

Die tweede regel is het antwoord op vraag drie, en hij is ongemakkelijk. CISA vraagt sinds de 2026 Minimum Elements om component-hashes. Wij lezen pakketmetadata en niet de artefacten zelf, dus die hashes kennen we niet. Dan zetten we "onbekend" neer. Een verzonnen hash is erger dan een ontbrekende hash, want een ontbrekende hash is eerlijk en een verzonnen hash is een fout die pas opvalt als iemand hem probeert te controleren.

Waarom VEX het moeilijkere deel is

Een SBOM is een lijst. Een VEX-document is een oordeel: per kwetsbaarheid stelt het vast of dit systeem daadwerkelijk geraakt wordt.

In diezelfde draad stond de opmerking die ons het meeste bijbleef. Te veel organisaties gebruiken "not affected" als een bewering in plaats van als een verdedigbare technische conclusie. Een goed VEX-statement is gekoppeld aan een specifieke kwetsbaarheid, een component, een productversie, een status, een onderbouwing en idealiter het bewijs dat eronder ligt. Doe je dat niet, dan heb je alleen je dashboard stiller gemaakt zonder de kwaliteit van je besluitvorming te verbeteren.

Dat is precies waar het bij VEX misgaat, want de verleiding is groot. "Not affected" laat een rood vakje verdwijnen. Het is de goedkoopste manier om een rapport er beter uit te laten zien, en niemand merkt het verschil tussen een onderbouwde beoordeling en een gok, tot het moment waarop het uitmaakt.

Onze standaard is daarom omgekeerd. Een gematchte CVE blijft affected tenzij er positief bewijs is dat het anders ligt:

Bron van bewijsResulterende status
OSV matcht de geïnstalleerde versie, verder niets bekendaffected, met een concrete actie
Distributie heeft de fix teruggeport naar deze pakketversiefixed
Kernelmitigatie actief in de draaiende kernelnot_affected, met inline_mitigations_already_exist
Operator heeft het risico expliciet aanvaard, met redennot_affected, met die reden als impact statement
Kernelstatus onbekend bij de distributieunder_investigation

Stilte staat niet in die tabel. Het ontbreken van bewijs is geen onderbouwing, en daarom kan onze exporter geen not_affected produceren zonder dat er iets concreets aan hangt.

De twee dingen die bij ons niet klopten

Zo ver waren we al toen de draad langskwam. Wat we vervolgens vonden, was minder comfortabel.

Probleem één: elk statement droeg het exportmoment.

De OpenVEX-specificatie geeft elk statement een timestamp. Wij vulden daar het moment in waarop je op de downloadknop drukte. Elk statement in het document had dus dezelfde tijd, en die tijd was vanmiddag.

Dat is technisch valide JSON en inhoudelijk waardeloos. Het bewijst wat je nu beweert, niet wat je toen wist. En dat laatste is precies waar het in een onderzoek achteraf om draait: wat wist de organisatie, wanneer wist ze het, en was wat ze daarna deed proportioneel. Een document waarin alles vandaag is gebeurd, kan die vraag per definitie niet beantwoorden.

Probleem twee: het losse VEX-bestand was niet ondertekend.

Onze SBOM's dragen sinds juli een Ed25519-handtekening over de gecanonicaliseerde vorm van het document. Onze Audit Packs zijn ondertekend. De VEX in een Audit Pack viel onder die handtekening. Maar het VEX-bestand dat je los downloadt en doormailt, het exemplaar dat in de praktijk het vaakst wordt rondgestuurd, had er geen. Een inconsistentie die niemand had gemeld en die precies op de verkeerde plek zat.

Wat we veranderd hebben

De tijdstempel draagt nu de betekenis die hij hoort te dragen. Per statement is het de datum waarop het bewijs achter de status is ontstaan, niet de datum van het document:

Statustimestamp is nu
affectedhet moment van eerste detectie op deze host
under_investigationhet moment van eerste detectie
fixedhet moment waarop de fix is vastgesteld
not_affected via een aanvaarde uitzonderinghet moment waarop de operator die beslissing vastlegde

Daarnaast krijgt elk statement een last_updated zodra de onderliggende bevinding opnieuw is bevestigd. Dat scheidt twee dingen die vaak op één hoop gaan: wanneer wisten we dit voor het eerst, en wanneer hebben we het voor het laatst gecontroleerd. Een bevinding uit maart die deze ochtend nog is teruggezien, ziet er heel anders uit dan een bevinding uit maart waar sindsdien niemand meer naar gekeken heeft.

In CycloneDX komt dat terecht in analysis.firstIssued en analysis.lastUpdated. In OpenVEX in timestamp en last_updated op het statement zelf, terwijl de tijdstempel van het document blijft doen wat hij hoort te doen, namelijk zeggen wanneer dit document is gemaakt.

Concreet, voor en na:

{
  "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"
}

Voorheen stond op beide velden de exporttijd. Nu staat er dat we deze kwetsbaarheid op 11 april voor het eerst op deze host zagen en dat ze vanochtend nog aanwezig was. Dat is een controleerbare bewering. De vorige versie was dat niet.

En het losse VEX-bestand is nu ondertekend, in beide formaten. CycloneDX heeft daar een eigen veld voor. OpenVEX heeft dat niet, dus daar hangt de handtekening in een eigen namespace, met vermelding van wat er precies ondertekend is:

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

Het is dezelfde sleutel als voor de SBOM's en de Audit Packs, en dezelfde canonicalisatie (RFC 8785). Eén verifier dekt alle drie. De publieke sleutel staat op /api/v1/keys, en verifiëren kan zonder ons:

curl -s "https://api.monsys.ai/api/v1/agents/$AGENT/vex?format=openvex" -b cookies.txt > vex.json
# haal monsys:signature eruit, canonicaliseer de rest volgens RFC 8785,
# controleer de Ed25519-handtekening tegen de sleutel op /api/v1/keys

We hebben dat zelf nagebouwd in een losstaande verifier die niets van onze code deelt, inclusief een test die één statusveld wijzigt en controleert dat de handtekening dan breekt. Een handtekening die je nooit hebt zien falen, heb je niet getest.

Wat dit wel en niet betekent

Dit maakt niemand CRA-conform. Conformiteit is een oordeel over je hele product en je hele proces, en dat wordt niet geveld door een JSON-bestand.

Wat het wel doet, is de vier vragen van hierboven beantwoordbaar maken op een manier die een doorgestuurd bestand overleeft. Over welk artefact dit gaat, staat erin. Op welk punt in de levenscyclus het is gegenereerd, staat erin. Welke identifiers gebruikt zijn, staat erin, inclusief wat we niet weten. En wie heeft vastgesteld dat het klopte, is niet langer een naam in een veld maar een handtekening die je zonder ons kunt controleren.

Voor VEX komt daar de vijfde vraag bij, die eigenlijk de belangrijkste is: op basis waarvan, en sinds wanneer. Een status zonder onderbouwing is een mening. Een status met onderbouwing maar met de datum van vanmiddag is een mening met een tijdstempel. Pas als de datum zegt wanneer je het wist, wordt het een verslag.

Wij hadden dat tot vorige week niet op orde. Nu wel.

Terug naar blog