CVE's, SBOM & supply chaingevorderd6 min lezen

Een VEX-statement schrijven (en waarom de datum belangrijker is dan de status)

Een scanner meldt CVE-2026-1234 in libxml2. Jij weet dat de kwetsbare functie in jouw opstelling nooit wordt aangeroepen. Een VEX-statement is hoe je dat vastlegt — machine-leesbaar, met een reden, en met de datum waarop je het wist. Dit is het OpenVEX-formaat in twintig regels, de vier statussen en wanneer je ze gebruikt, hoe je ondertekent met ssh-keygen, en hoe scanners het statement gebruiken om ruis te onderdrukken.

Inhoud
  1. De vier statussen
  2. Het formaat: OpenVEX
  3. Waarom de datum belangrijker is dan de status
  4. Ondertekenen
  5. Scanners het statement laten gebruiken
  6. Het proces (want het formaat is het makkelijke deel)
  7. Valkuilen
  8. Wat je hiermee nog niet hebt
  9. Zo doet monsys het
  10. FAQ

Een SBOM zegt wat erin zit. Een scanner zegt welke CVE's daarbij horen. Geen van beide zegt of die CVE's er in jouw product toe doen — en dat is de vraag die je klant, je auditor en (vanaf 2027, onder de CRA) de toezichthouder stellen. VEX, Vulnerability Exploitability eXchange, is het antwoordformaat: per product, per kwetsbaarheid één uitspraak met een status, een reden en een tijdstip. Het is klein, saai en bureaucratisch, en het is het enige wat de stroom scanner-meldingen terugbrengt tot een lijst die klopt.

De vier statussen

StatusBetekenisWanneer
not_affectedDe kwetsbaarheid raakt dit product nietDe kwetsbare code zit er niet in, wordt niet aangeroepen, of is niet bereikbaar. Vereist een justification.
affectedHet product is kwetsbaarJe weet het, en er is (nog) geen fix uitgerold. Verplicht: wat de gebruiker moet doen (action_statement).
fixedOpgelost in deze versieNa de upgrade of patch. Vermeld de versie.
under_investigationNog niet beoordeeldEerlijk placeholder; beter dan niets zeggen. Met een datum waarop je het wél weet.

De vijf toegestane justification-waarden voor not_affected (uit de CISA-specificatie):

  • component_not_present — het package staat in de SBOM maar de kwetsbare component (bv. een optionele module) is niet meegecompileerd of geïnstalleerd
  • vulnerable_code_not_present — de kwetsbare functie zit niet in jouw build (bv. weggecompileerd, of een andere versie van dat bestand)
  • vulnerable_code_not_in_execute_path — de code zit erin maar wordt in jouw gebruik nooit bereikt
  • vulnerable_code_cannot_be_controlled_by_adversary — bereikbaar, maar de input komt nooit van een aanvaller
  • inline_mitigations_already_exist — bereikbaar, maar een andere maatregel (sandbox, firewall, seccomp) blokkeert exploitatie

Kies de smalste die waar is. inline_mitigations_already_exist is de zwakste (de mitigatie kan verdwijnen); component_not_present de sterkste.

Het formaat: OpenVEX

Er zijn drie VEX-formaten (CSAF VEX, CycloneDX VEX, OpenVEX). OpenVEX is het kleinste en wordt door grype, trivy en osv-scanner gelezen. Eén document, meerdere statements:

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 wordt uitsluitend gebruikt door php-xml voor het parsen van eigen, statische configuratiebestanden; XML-input van gebruikers wordt nergens geparsed. De kwetsbare XInclude-code wordt niet bereikt."
    },
    {
      "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": "Zet de omgevingsvariabele SHOP_DISABLE_EXPORT=1 tot versie 2026.09.2 (verwacht 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 "geldige JSON"

De product-identifiers zijn purls (package URLs): pkg:docker/…, pkg:deb/…, pkg:npm/…. Gebruik dezelfde purls als in je SBOM, anders kan een scanner het statement niet koppelen. Voor een intern product zonder registry: pkg:generic/example/shop@2026.09.1.

Waarom de datum belangrijker is dan de status

Een VEX-statement is een bewering op een moment. not_affected op 16 september kan op 3 oktober onwaar zijn: een nieuwe release die XML-upload toevoegt, een correctie van het CVE-record, een nieuw exploit-pad. Daarom:

  • Elk statement heeft een eigen timestamp — het moment waarop dat oordeel werd geveld, niet het moment waarop je het document opsloeg.
  • Je wijzigt een statement nooit; je voegt een nieuw toe met een latere timestamp en verhoogt version. De lezer neemt per (product, vulnerability) het nieuwste.
  • Een auditor die vraagt "wat wist u op 20 september?" krijgt een antwoord uit het document zelf. Zonder datums is een VEX-bestand een mening.

Concreet: git log op /srv/vex is niet genoeg als tijdbewijs (een commit-datum is instelbaar). De timestamp in het statement plus een handtekening over het bestand wel.

Ondertekenen

Wie een VEX-bestand kan aanpassen, kan een affected in een not_affected veranderen. Onderteken met een aparte sleutel, net als bij bewijs-archieven:

# Eenmalig
sudo install -d -m 0700 /etc/vex && sudo ssh-keygen -q -t ed25519 -N '' -C 'vex@example.be' -f /etc/vex/key
# Per 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
# Verifiëren (klant, auditor, CI) met alleen de publieke sleutel:
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

Publiceer key.pub op een vaste URL (bv. https://example.be/.well-known/vex-signing-key.pub) zodat klanten de handtekening kunnen controleren zonder je te bellen. Wie Sigstore/cosign gebruikt: cosign sign-blob doet hetzelfde met een transparency log erbij.

Scanners het statement laten gebruiken

Het doel van VEX is dat je scanner de not_affected-meldingen niet meer toont — en de affected wél, met jouw actie erbij.

# Trivy: image scannen met VEX-filter
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), OpenVEX-ondersteuning via --experimental-… varieert per versie; controleer osv-scanner --help

--show-suppressed in Trivy toont wat er door VEX is weggefilterd, met de justification. Dat is precies het overzicht dat je in een audit laat zien: niet "we hebben nul kwetsbaarheden", maar "we hebben er veertig beoordeeld, hier staat per stuk waarom".

Het proces (want het formaat is het makkelijke deel)

  1. Trigger: een nieuwe scanner-melding op een productie-artefact (wekelijkse scan, of KEV-update).
  2. Beoordeling binnen 5 werkdagen, door iemand die de code kent: is de kwetsbare functie bereikbaar? Zo niet: not_affected + justification + één alinea impact_statement. Zo wel: affected + wat de gebruiker nu moet doen + de geplande fix-versie.
  3. Statement toevoegen, document ondertekenen, publiceren (interne klanten: een pad; externe: een URL).
  4. Herbeoordelen bij elke release van het product en bij elke wijziging van het CVE-record. Zet een kwartaal-reminder voor alle not_affected-statements ouder dan een jaar.
  5. Bewaren: elke versie van het document, met handtekening, minstens zo lang als het product ondersteund wordt. Voor de CRA: de ondersteuningsperiode plus de tijd die de toezichthouder mag terugkijken.

Valkuilen

  • not_affected zonder justification. Ongeldig volgens de spec, en waardeloos voor een auditor. De justification is het statement.
  • inline_mitigations_already_exist als standaardantwoord. Een WAF-regel is een mitigatie tot iemand hem uitzet. Gebruik hem alleen als de mitigatie deel is van het product zelf, en beschrijf welke.
  • Purls die niet matchen. pkg:deb/debian/libxml2 versus pkg:deb/ubuntu/libxml2, of een ontbrekende ?distro=: de scanner ziet het statement niet en toont de melding gewoon. Test met --show-suppressed.
  • Eén statement voor alle versies. products wijst naar een versie. Een nieuwe release is een nieuwe beoordeling — al is het maar een kopie met nieuwe purl en nieuwe timestamp.
  • VEX als excuus. Een not_affected omdat het patchen lastig is, is geen beoordeling maar uitstel. De justification moet iemand met de code ernaast kunnen verdedigen.

Wat je hiermee nog niet hebt

  • Het overzicht. Tien producten, twaalf releases per jaar, veertig CVE's per release: dat zijn duizenden statements in JSON-bestanden. "Welke not_affected-oordelen zijn ouder dan een jaar?" is een jq over een map die iemand netjes moet houden.
  • De koppeling met de inventaris. Het statement zegt "product X versie Y is not_affected". Of versie Y nog ergens draait, en waar, staat in een ander systeem.
  • Het bewijs dat het proces werkt. Een auditor wil niet alleen de statements, maar ook: hoe lang zat er tussen melding en beoordeling, en wie beoordeelde.

Zo doet monsys het

In monsys leg je een VEX-statement vast bij de CVE zelf, in de context van de host of applicatie waar de scanner hem vond: status, justification, impact-tekst, en de identiteit van wie het oordeel velde. De hub genereert daaruit een OpenVEX-document per product naast de SBOM, ondertekent beide met de tenant-sleutel en neemt ze op in het maandelijkse audit pack. Omdat de hub weet welke versies waar draaien, verschijnt een not_affected-oordeel automatisch opnieuw ter beoordeling wanneer het product een nieuwe release krijgt of het CVE-record verandert — en de doorlooptijd tussen melding en oordeel is een metriek in de Trust Score.

FAQ

Is VEX verplicht?

Nog niet wettelijk, maar de CRA (Annex I §11-12) vereist dat fabrikanten kwetsbaarheden in hun producten documenteren en aan gebruikers communiceren, en de CISA 2026 Minimum Elements voor SBOM's verwijzen expliciet naar VEX als het bijbehorende formaat. Klanten die zelf onder NIS2 vallen, vragen het steeds vaker op bij leveranciers.

Welk VEX-formaat kies ik?

OpenVEX voor eenvoud en scanner-ondersteuning (Trivy, Grype). CycloneDX VEX als je SBOM al CycloneDX is en je alles in één bestand wilt. CSAF VEX als je klanten in de industriële of overheidssector zitten die CSAF-tooling gebruiken. De inhoud (status, justification, datum) is in alle drie hetzelfde.

Hoe lang moet ik VEX-statements bewaren?

Minstens zolang het product ondersteund wordt; onder de CRA de volledige ondersteuningsperiode plus de bewaartermijn die de toezichthouder mag opvragen (reken op tien jaar na de laatste release). Bewaar elke versie, niet alleen de laatste.

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.