Vous avez scanné votre image avec Grype. Résultat : 12 vulnérabilités. Après
analyse, vous réalisez que CVE-2025-12084 concerne xml.dom.minidom, mais votre
API ne parse jamais de XML. C'est un faux positif.
Le problème : demain, vous rescannerez. Cette CVE réapparaîtra. Vous re-analyserez. Vous re-conclurez "faux positif". Et ainsi de suite, à chaque build, à chaque sprint, à chaque release.
La solution : vexctl. Cet outil vous permet de documenter une fois que cette CVE n'est pas exploitable dans votre contexte. Au prochain scan, Grype lira votre document VEX et filtrera automatiquement ce faux positif.
Ce que fait vexctl
Section intitulée « Ce que fait vexctl »vexctl est l'outil en ligne de commande officiel du projet OpenVEX. Il génère des documents VEX valides en quelques commandes, sans risque d'erreur de syntaxe JSON.
Le workflow complet :
- Scanner : Grype détecte 12 CVE sur votre image
- Analyser : vous triez par sévérité et vérifiez le contexte de chaque CVE
- Documenter : vexctl crée un fichier
.vex.jsonpour les faux positifs - Corriger : vous mettez à jour les vraies vulnérabilités
- Rescanner :
grype --vexfiltre les faux positifs documentés → 11 CVE
Anatomie d'un document VEX
Section intitulée « Anatomie d'un document VEX »Avant d'utiliser vexctl, comprenez ce qu'il génère :
Un document VEX contient :
- Métadonnées : auteur, date, version, identifiant unique
- Statements : une liste de déclarations, chacune associant une CVE à un produit avec un statut d'exploitabilité
Chaque statement précise :
- vulnerability : l'identifiant CVE ou GHSA
- products : le ou les produits concernés (au format purl)
- status :
not_affected,affected,fixed, ouunder_investigation - justification : le code standardisé expliquant pourquoi (si
not_affected) - impact_statement : l'explication humaine détaillée
Prérequis
Section intitulée « Prérequis »Connaissances :
- Comprendre ce qu'est un VEX et à quoi il sert
- Avoir déjà scanné une image avec Grype ou Trivy
- Connaître les bases des CVE
Outils à installer :
# Installer Go (version 1.21+)# Ubuntu/Debian
sudo snap install go --classic
# macOSbrew install go
# Installer vexctlgo install github.com/openvex/vexctl@v0.4.1
# Vérifier l'installationvexctl version# vexctl version v0.4.1# Installer Go depuis https://go.dev/dl/
# Installer vexctlgo install github.com/openvex/vexctl@v0.4.1
# Vérifier l'installationvexctl versiondocker pull ghcr.io/openvex/vexctl:v0.4.1
# Alias pour simplifier l'usagealias vexctl='docker run --rm -v $PWD:/work -w /work ghcr.io/openvex/vexctl:v0.4.1'
# Vérifiervexctl versionCommandes principales
Section intitulée « Commandes principales »vexctl propose deux commandes essentielles : create pour générer des
statements VEX, et merge pour les combiner.
create : créer un document VEX
Section intitulée « create : créer un document VEX »La commande create génère un nouveau document VEX. C'est la commande que vous
utiliserez 90% du temps.
Syntaxe de base :
vexctl create \ --author="<email ou nom>" \ --product="<purl du produit>" \ --vuln="<CVE ou GHSA>" \ --status="<not_affected|affected|fixed|under_investigation>" \ --justification="<code justification>" \ --impact-statement="<explication humaine>" \ > fichier.vex.jsonParamètres obligatoires :
| Paramètre | Description | Exemple |
|---|---|---|
--author | Auteur du VEX (email ou organisation) | security@mon-org.com |
--product | Produit concerné au format purl | pkg:oci/mon-app@sha256:abc123... |
--vuln | Identifiant de la vulnérabilité | CVE-2024-1234 ou GHSA-xxxx-yyyy-zzzz |
--status | Statut d'exploitabilité | not_affected, affected, fixed, under_investigation |
Paramètres optionnels :
| Paramètre | Description | Quand l'utiliser |
|---|---|---|
--justification | Code justification standardisé | Pour status=not_affected (voir tableau ci-dessous) |
--impact-statement | Explication en langage naturel | Toujours recommandé (pour les humains) |
--action-statement | Action prise ou planifiée | Pour status=fixed ou affected |
Les 4 statuts possibles
Section intitulée « Les 4 statuts possibles »Chaque CVE que vous documentez doit avoir un statut. Choisissez celui qui correspond à votre situation :
not_affected
La CVE ne vous concerne pas. Le composant vulnérable est présent mais non exploitable dans votre contexte.
Exemple : CVE sur xml.dom.minidom mais votre API ne parse que du JSON.
Requiert : --justification obligatoire
affected
La CVE vous concerne. Le composant est utilisé de manière vulnérable. Action corrective nécessaire.
Exemple : CVE sur python-jose que vous utilisez pour l'auth JWT.
Requiert : --action-statement recommandé
fixed
La CVE a été corrigée. Mise à jour appliquée ou patch déployé.
Exemple : Upgrade de starlette 0.41.3 vers 0.49.1.
Requiert : --action-statement recommandé
under_investigation
Analyse en cours. Vous n'avez pas encore déterminé l'impact.
Exemple : CVE publiée ce matin, l'équipe sécu évalue.
Temporaire : à mettre à jour rapidement
Justifications standardisées
Section intitulée « Justifications standardisées »Utilisez ces codes avec --justification uniquement pour status=not_affected.
Ces codes sont standardisés par la spécification VEX et compris par tous les
outils compatibles :
| Code | Signification | Exemple concret |
|---|---|---|
component_not_present | Le composant vulnérable n'est pas installé | "CVE sur PostgreSQL mais nous utilisons MySQL" |
vulnerable_code_not_present | Le code vulnérable a été supprimé (patch custom) | "Nginx recompilé sans module DAV vulnérable" |
vulnerable_code_not_in_execute_path | Le code existe mais n'est jamais appelé | "libxml2 présent mais API ne parse jamais de XML" |
vulnerable_code_cannot_be_controlled_by_adversary | L'attaquant ne peut pas atteindre le code | "Endpoint admin bloqué par NetworkPolicy" |
inline_mitigations_already_exist | Des mitigations (WAF, sandbox) neutralisent l'exploit | "WAF ModSecurity bloque les injections SQL" |
Exemples pratiques
Section intitulée « Exemples pratiques »Voici les 4 cas d'usage les plus fréquents, avec les commandes complètes.
1. CVE non exploitable (faux positif)
# xml.dom.minidom présent dans Python 3.12 mais jamais utilisévexctl create \ --author="security@stephane-robert.info" \ --product="pkg:oci/devops-status-api@sha256:0c4d2ee81f66cc0a4fc155b6cf233a1e435a6c2102e0b5a613ecd70ac3c55f4c" \ --vuln="CVE-2025-12084" \ --status="not_affected" \ --justification="vulnerable_code_not_in_execute_path" \ --impact-statement="Notre API FastAPI ne parse jamais de XML, uniquement du JSON. Le module xml.dom.minidom est présent dans Python 3.12 mais les fonctions vulnérables ne sont jamais appelées." \ > devops-status-api.vex.json2. CVE corrigée
# python-jose upgradé vers version corrigéevexctl create \ --author="security@stephane-robert.info" \ --product="pkg:oci/devops-status-api@sha256:0c4d2ee81f66cc0a4fc155b6cf233a1e435a6c2102e0b5a613ecd70ac3c55f4c" \ --vuln="GHSA-6c5p-j8vq-pqhj" \ --status="fixed" \ --action-statement="Mise à jour python-jose 3.3.0 → 3.4.0 dans requirements.txt (commit abc1234)" \ > devops-status-api.vex.json3. CVE en cours d'analyse
# CVE récente, équipe sécu évalue l'impactvexctl create \ --author="security@stephane-robert.info" \ --product="pkg:oci/devops-status-api@sha256:0c4d2ee81f66cc0a4fc155b6cf233a1e435a6c2102e0b5a613ecd70ac3c55f4c" \ --vuln="GHSA-9hjg-9r4m-mvj7" \ --status="under_investigation" \ --impact-statement="CVE publiée le 19/12/2024, équipe analyse si notre usage de requests (API internes uniquement) est concerné. Résultats attendus 23/12 17h." \ > devops-status-api.vex.json4. CVE exploitable (nécessite action)
# CVE critique confirmée, upgrade planifiévexctl create \ --author="security@stephane-robert.info" \ --product="pkg:oci/devops-status-api@sha256:0c4d2ee81f66cc0a4fc155b6cf233a1e435a6c2102e0b5a613ecd70ac3c55f4c" \ --vuln="GHSA-6c5p-j8vq-pqhj" \ --status="affected" \ --action-statement="python-jose utilisé pour JWT auth. Upgrade vers 3.4.0+ requis sous 48h (ticket JIRA SEC-1234)." \ > devops-status-api.vex.jsonmerge : fusionner plusieurs statements
Section intitulée « merge : fusionner plusieurs statements »La commande merge combine plusieurs documents VEX en un seul. Utile pour
ajouter des statements à un VEX existant sans l'écraser.
Ajouter un statement à un VEX existant :
# Créer un nouveau statementvexctl create \ --product="pkg:oci/devops-status-api@sha256:0c4d2ee8..." \ --vuln="CVE-2025-13836" \ --status="not_affected" \ --justification="vulnerable_code_not_in_execute_path" \ --impact-statement="CVE sur zipimport mais application ne charge jamais de .zip" \ > new-statement.vex.json
# Fusionner avec le VEX existantvexctl merge devops-status-api.vex.json new-statement.vex.json > merged.vex.json
# Remplacer le VEX originalmv merged.vex.json devops-status-api.vex.json
# Nettoyerrm new-statement.vex.jsonFusionner plusieurs VEX :
# Fusionner VEX vendor + VEX customvexctl merge \ vendor-alpine.vex.json \ vendor-python.vex.json \ custom-app.vex.json \ > complete.vex.jsonComportement en cas de doublon : si un statement existe pour la même CVE
dans plusieurs VEX, merge conserve tous les statements. Le dernier fourni
en ligne de commande a priorité lors de l'évaluation par les scanners.
Workflows courants
Section intitulée « Workflows courants »Les trois séquences ci-dessous répondent à trois situations distinctes : traiter un scan ponctuel, industrialiser des faux positifs qui reviennent à chaque build, et gérer plusieurs versions d'image en parallèle. Elles partagent une contrainte que rien ne rattrape ensuite : le VEX est lié à un digest précis. Un statement écrit pour une image ne s'applique pas à la suivante, même si le tag n'a pas changé.
Workflow 1 : Traiter les faux positifs d'un scan Grype
Section intitulée « Workflow 1 : Traiter les faux positifs d'un scan Grype »L'étape 3 est celle qui prend du temps et que personne ne peut automatiser : le
statut not_affected engage votre responsabilité et doit être défendable en
audit. Les commandes qui l'entourent ne font que consigner une décision
d'analyse, elles ne la produisent pas.
-
Scanner et exporter les CVE en JSON
Fenêtre de terminal # Générer SBOMsyft mon-app:test -o cyclonedx-json > mon-app.sbom.json# Scanner avec Grype (format JSON)grype sbom:mon-app.sbom.json -o json > vulns.json -
Extraire la liste des CVE uniques
Fenêtre de terminal jq -r '.matches[].vulnerability.id' vulns.json | sort -u > cve-list.txt# Afficher la listecat cve-list.txt# CVE-2024-1234# CVE-2024-5678# GHSA-xxxx-yyyy-zzzz -
Analyser chaque CVE manuellement
Pour chaque CVE, vérifier :
- La description NVD :
curl https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=CVE-2024-1234 - Votre code source : le composant est-il utilisé ?
- Votre architecture : l'exploit est-il possible ?
- La description NVD :
-
Créer un statement VEX pour chaque faux positif
Fenêtre de terminal # Récupérer le digest de l'imageDIGEST=$(docker inspect mon-app:test | jq -r '.[0].Id')# Créer le premier statementvexctl create \--author="security@mon-org.com" \--product="pkg:oci/mon-app@${DIGEST}" \--vuln="CVE-2024-1234" \--status="not_affected" \--justification="vulnerable_code_not_in_execute_path" \--impact-statement="libxml2 non utilisé" \> mon-app.vex.json -
Ajouter les autres CVE au même VEX
Fenêtre de terminal # Pour chaque faux positif supplémentairevexctl create \--product="pkg:oci/mon-app@${DIGEST}" \--vuln="CVE-2024-5678" \--status="not_affected" \--justification="component_not_present" \--impact-statement="PostgreSQL non installé, nous utilisons MySQL" \| vexctl merge mon-app.vex.json - > temp.vex.jsonmv temp.vex.json mon-app.vex.json -
Rescanner avec le VEX
Fenêtre de terminal # Scanner avec VEX pour filtrer les faux positifsgrype sbom:mon-app.sbom.json --vex mon-app.vex.json# Comparer le nombre de CVE avant/aprèsecho "Sans VEX: $(jq '.matches | length' vulns.json)"grype sbom:mon-app.sbom.json --vex mon-app.vex.json -o json > vulns-filtered.jsonecho "Avec VEX: $(jq '.matches | length' vulns-filtered.json)"
Workflow 2 : VEX automatique pour CVE récurrentes
Section intitulée « Workflow 2 : VEX automatique pour CVE récurrentes »Si certains faux positifs reviennent à chaque build (dépendances de l'image de base Alpine, Python, etc.), automatisez leur création :
#!/bin/bashDIGEST=$(docker inspect mon-app:test | jq -r '.[0].Id')PRODUCT="pkg:oci/mon-app@${DIGEST}"
# Liste des CVE connues comme faux positifsKNOWN_FP=( "CVE-2024-1234:libxml2 jamais utilisé" "CVE-2024-5678:Module PostgreSQL absent" "GHSA-xxxx-yyyy-zzzz:Endpoint admin bloqué par NetworkPolicy")
# Créer le premier statementIFS=':' read -r CVE REASON <<< "${KNOWN_FP[0]}"vexctl create \ --author="security@mon-org.com" \ --product="$PRODUCT" \ --vuln="$CVE" \ --status="not_affected" \ --justification="vulnerable_code_not_in_execute_path" \ --impact-statement="$REASON" \ > mon-app.vex.json
# Ajouter les autresfor i in "${KNOWN_FP[@]:1}"; do IFS=':' read -r CVE REASON <<< "$i" vexctl create \ --product="$PRODUCT" \ --vuln="$CVE" \ --status="not_affected" \ --justification="vulnerable_code_not_in_execute_path" \ --impact-statement="$REASON" \ | vexctl merge mon-app.vex.json - > temp.vex.json mv temp.vex.json mon-app.vex.jsondone
echo "✓ VEX créé avec ${#KNOWN_FP[@]} statements"Workflow 3 : VEX pour plusieurs versions d'image
Section intitulée « Workflow 3 : VEX pour plusieurs versions d'image »Chaque version d'image doit avoir son propre VEX (les dépendances changent entre versions) :
#!/bin/bashVERSIONS=("1.0.0" "1.1.0" "1.2.0")
for VERSION in "${VERSIONS[@]}"; do echo "Traitement de mon-app:${VERSION}..."
# Récupérer le digest DIGEST=$(docker inspect mon-app:${VERSION} | jq -r '.[0].Id')
# Scanner syft mon-app:${VERSION} -o cyclonedx-json > mon-app-${VERSION}.sbom.json grype sbom:mon-app-${VERSION}.sbom.json -o json > vulns-${VERSION}.json
# Créer VEX (exemple simplifié) vexctl create \ --author="security@mon-org.com" \ --product="pkg:oci/mon-app@${DIGEST}" \ --vuln="CVE-2024-1234" \ --status="not_affected" \ --justification="vulnerable_code_not_in_execute_path" \ --impact-statement="libxml2 non utilisé dans version ${VERSION}" \ > mon-app-${VERSION}.vex.json
# Rescanner avec VEX grype sbom:mon-app-${VERSION}.sbom.json --vex mon-app-${VERSION}.vex.jsondoneDistribuer les VEX avec Cosign
Section intitulée « Distribuer les VEX avec Cosign »Une fois vos VEX créés avec vexctl, distribuez-les de façon sécurisée.
Attacher le VEX à l'image OCI
Section intitulée « Attacher le VEX à l'image OCI »cosign attach dépose le document dans la registry à côté de l'image, ce qui
règle la question de la distribution mais n'apporte aucune garantie
d'authenticité : n'importe qui disposant d'un accès en écriture peut y placer
un autre VEX. La signature traitée juste après est ce qui rend l'attachement
digne de confiance.
# Générer SBOM + VEXsyft mon-app:1.0.0 -o cyclonedx-json > sbom.jsonvexctl create [...] > app.vex.json
# Attacher SBOM et VEX à l'image dans la registrycosign attach sbom --sbom sbom.json mon-app:1.0.0cosign attach artifact --artifact app.vex.json mon-app:1.0.0
# Vérifier les artefacts attachéscosign tree mon-app:1.0.0# mon-app:1.0.0# ├── sha256:abc123... (SBOM)# └── sha256:def456... (VEX)Signer le VEX (recommandé en production)
Section intitulée « Signer le VEX (recommandé en production) »Un VEX signé devient une attestation : il lie le document à l'identité qui
l'a produit, vérifiable par un tiers. Sans cela, un attaquant capable d'écrire
dans la registry pourrait publier un VEX déclarant not_affected sur une
vulnérabilité réellement exploitable et faire disparaître l'alerte chez tous vos
consommateurs.
# Signature keyless avec OIDC GitHub Actionscosign attest \ --predicate app.vex.json \ --type https://openvex.dev/ns/v0.2.0 \ mon-app:1.0.0
# Vérifier la signaturecosign verify-attestation \ --type https://openvex.dev/ns/v0.2.0 \ mon-app:1.0.0Récupérer le VEX depuis la registry
Section intitulée « Récupérer le VEX depuis la registry »L'attestation encapsule le document dans une enveloppe encodée en base64, d'où
la chaîne de décodage avant de retrouver le JSON exploitable. Notez que
download attestation ne vérifie pas la signature : utilisez
verify-attestation si le VEX vient d'un tiers.
# Télécharger le VEX attaché à une imagecosign download attestation mon-app:1.0.0 \ | jq -r '.payload' \ | base64 -d \ | jq -r '.predicate' \ > downloaded.vex.json
# Utiliser le VEX téléchargégrype mon-app:1.0.0 --vex downloaded.vex.jsonIntégration CI/CD
Section intitulée « Intégration CI/CD »Automatiser le VEX déplace un risque : la justification cesse d'être relue à
chaque scan pour devenir une ligne de configuration qui s'applique
silencieusement, build après build. Deux garde-fous s'imposent donc dans le
pipeline. D'abord le scan doit tourner avant l'application du VEX, pour
conserver la trace du nombre réel de CVE. Ensuite le blocage sur les
sévérités Critical et High doit s'appuyer sur le résultat filtré, ce qui rend
tout not_affected abusif directement responsable d'une image livrée
vulnérable.
GitHub Actions
Section intitulée « GitHub Actions »Ce workflow retient les deux comptages, avant et après VEX, ce qui permet de détecter une dérive : un écart qui grandit à chaque release signale des justifications accumulées sans réanalyse.
name: Build + SBOM + VEX
on: push: branches: [main]
jobs: security-scan: runs-on: ubuntu-24.04 permissions: contents: read packages: write id-token: write
steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Build image run: docker build -t ghcr.io/${{ github.repository }}:${{ github.sha }} .
- name: Install tools run: | # Syft (binaire vérifié par SHA256) SYFT_VERSION=1.38.2 sbase="https://github.com/anchore/syft/releases/download/v${SYFT_VERSION}" curl -sSfLO "${sbase}/syft_${SYFT_VERSION}_linux_amd64.tar.gz" curl -sSfLO "${sbase}/syft_${SYFT_VERSION}_checksums.txt" grep "syft_${SYFT_VERSION}_linux_amd64.tar.gz" "syft_${SYFT_VERSION}_checksums.txt" | sha256sum -c - sudo tar -C /usr/local/bin -xzf "syft_${SYFT_VERSION}_linux_amd64.tar.gz" syft
# Grype (binaire vérifié par SHA256) GRYPE_VERSION=0.104.2 gbase="https://github.com/anchore/grype/releases/download/v${GRYPE_VERSION}" curl -sSfLO "${gbase}/grype_${GRYPE_VERSION}_linux_amd64.tar.gz" curl -sSfLO "${gbase}/grype_${GRYPE_VERSION}_checksums.txt" grep "grype_${GRYPE_VERSION}_linux_amd64.tar.gz" "grype_${GRYPE_VERSION}_checksums.txt" | sha256sum -c - sudo tar -C /usr/local/bin -xzf "grype_${GRYPE_VERSION}_linux_amd64.tar.gz" grype
# vexctl go install github.com/openvex/vexctl@v0.4.1
- name: Generate SBOM run: syft ghcr.io/${{ github.repository }}:${{ github.sha }} -o cyclonedx-json > sbom.json
- name: Initial scan id: scan run: | grype sbom:sbom.json -o json > vulns.json echo "cve_count=$(jq '.matches | length' vulns.json)" >> $GITHUB_OUTPUT continue-on-error: true
- name: Generate VEX for known false positives run: | DIGEST=$(docker inspect ghcr.io/${{ github.repository }}:${{ github.sha }} | jq -r '.[0].Id')
# CVE-2024-1234 : libxml2 non utilisé vexctl create \ --author="security@${{ github.repository_owner }}.com" \ --product="pkg:oci/${{ github.repository }}@${DIGEST}" \ --vuln="CVE-2024-1234" \ --status="not_affected" \ --justification="vulnerable_code_not_in_execute_path" \ --impact-statement="libxml2 non utilisé par l'application" \ > app.vex.json
- name: Re-scan with VEX run: | grype sbom:sbom.json --vex app.vex.json -o json > vulns-filtered.json echo "CVE avant VEX: $(jq '.matches | length' vulns.json)" echo "CVE après VEX: $(jq '.matches | length' vulns-filtered.json)"
- name: Fail on critical/high CVE run: | HIGH_COUNT=$(jq '[.matches[] | select(.vulnerability.severity == "Critical" or .vulnerability.severity == "High")] | length' vulns-filtered.json) if [ "$HIGH_COUNT" -gt 0 ]; then echo "❌ $HIGH_COUNT CVE Critical/High détectées" exit 1 fi
- name: Push image + SBOM + VEX run: | docker push ghcr.io/${{ github.repository }}:${{ github.sha }} cosign attach sbom --sbom sbom.json ghcr.io/${{ github.repository }}:${{ github.sha }} cosign attach artifact --artifact app.vex.json ghcr.io/${{ github.repository }}:${{ github.sha }}Bonnes pratiques
Section intitulée « Bonnes pratiques »Ces cinq règles visent toutes le même risque : un VEX qui masque une vulnérabilité réelle. Trois d'entre elles portent sur l'exactitude du périmètre (le bon digest, le bon raisonnement, la bonne date), les deux autres sur la conservation de l'historique, sans lequel plus personne ne peut dire pourquoi une CVE a été écartée ni quand cette décision devrait être réexaminée.
1. Versionner les VEX avec les images
Section intitulée « 1. Versionner les VEX avec les images »Un tag comme latest pointe vers un contenu qui change : un VEX rédigé pour ce
tag continuerait de filtrer des CVE sur une image dont les dépendances ne sont
plus les mêmes. Le digest SHA256 est immuable, il désigne exactement ce que
vous avez analysé.
# Un VEX par digest d'imagevexctl create --product="pkg:oci/mon-app@sha256:abc123..."
# PAS un VEX par tag (les tags sont mutables)# ❌ vexctl create --product="pkg:oci/mon-app:latest"2. Documenter toujours l'impact_statement
Section intitulée « 2. Documenter toujours l'impact_statement »Le code de justification est lu par les outils, l'impact-statement par les
humains qui reprendront le dossier dans six mois ou lors d'un audit. Sans lui,
il devient impossible de vérifier si le raisonnement d'origine tient encore
après une évolution du code.
# ✅ Bon : explication claire pour les humainsvexctl create \ --vuln="CVE-2024-1234" \ --status="not_affected" \ --justification="vulnerable_code_not_in_execute_path" \ --impact-statement="Notre API FastAPI ne parse jamais de XML, uniquement du JSON. Le module xml.dom.minidom est présent dans Python 3.12 mais les fonctions vulnérables ne sont jamais appelées."
# ❌ Mauvais : justification seule (pas d'explication)vexctl create \ --vuln="CVE-2024-1234" \ --status="not_affected" \ --justification="vulnerable_code_not_in_execute_path"3. Réviser les VEX régulièrement
Section intitulée « 3. Réviser les VEX régulièrement »Un VEX marqué not_affected peut devenir affected si :
- Vous ajoutez une fonctionnalité qui utilise le composant vulnérable
- Une nouvelle variante d'exploit contourne les mitigations
- La configuration change (ouverture d'un port, désactivation d'un WAF)
Recommandation : réviser les VEX à chaque changement majeur de l'application ou tous les 3 mois.
4. Ne pas abuser de not_affected
Section intitulée « 4. Ne pas abuser de not_affected »Un VEX généré en boucle sur une liste de CVE ne documente rien : il désactive le scanner tout en donnant l'apparence d'une analyse rigoureuse. C'est la situation la plus dangereuse décrite dans ce guide, puisque l'alerte disparaît sans que la vulnérabilité ait été examinée.
# ❌ Mauvais : marquer toutes les CVE en not_affected sans analysefor cve in $(cat all-cves.txt); do vexctl create --vuln="$cve" --status="not_affected" --justification="vulnerable_code_not_in_execute_path"done
# ✅ Bon : analyser CVE par CVE, documenter le raisonnement# Concentrez-vous sur les Critical/High uniquement si trop nombreuses5. Utiliser merge plutôt que recréer
Section intitulée « 5. Utiliser merge plutôt que recréer »vexctl create écrit sur la sortie standard : la redirection > remplace le
fichier existant et détruit silencieusement tous les statements déjà rédigés.
Passer par merge préserve l'accumulation des décisions d'analyse.
# ✅ Bon : ajouter un statement sans écraser l'existantvexctl create [...] | vexctl merge existing.vex.json - > updated.vex.json
# ❌ Mauvais : écraser le VEX à chaque foisvexctl create [...] > existing.vex.json # perte des statements précédentsErreurs courantes
Section intitulée « Erreurs courantes »Ces quatre erreurs se distinguent par leur mode d'échec. Les trois dernières sont bruyantes : vexctl refuse la commande ou produit un JSON invalide, et vous le voyez immédiatement. La première est silencieuse, le VEX est syntaxiquement correct et accepté par les scanners, mais il ne correspond pas à l'artefact analysé. C'est celle qui coûte le plus cher.
❌ Oublier le digest dans --product
Section intitulée « ❌ Oublier le digest dans --product »Avec un tag, le VEX reste valide au sens du format mais désigne un contenu mouvant : le filtrage s'appliquera à des images que vous n'avez jamais analysées.
# ❌ Tag mutable (peut changer)vexctl create --product="pkg:oci/mon-app:latest"
# ✅ Digest SHA256 (immuable)vexctl create --product="pkg:oci/mon-app@sha256:abc123def456..."❌ Utiliser --statement au lieu de --impact-statement
Section intitulée « ❌ Utiliser --statement au lieu de --impact-statement »Le drapeau court n'existe pas en v0.4.1 et la commande s'arrête sur
unknown flag. Dans un script sans set -e, l'exécution continue et le fichier
de sortie reste vide.
# ❌ Flag qui n'existe pas dans vexctl v0.4.1vexctl create --statement="Explication..."# Error: unknown flag: --statement
# ✅ Flag correctvexctl create --impact-statement="Explication..."❌ Ne pas valider le JSON généré
Section intitulée « ❌ Ne pas valider le JSON généré »Comme la sortie est redirigée, un message d'erreur de vexctl peut se retrouver mêlé au fichier ou le laisser vide. Grype ignore alors un VEX qu'il ne sait pas lire, sans avertissement visible dans le résultat du scan.
# ✅ Toujours vérifier le contenu générévexctl create [...] > app.vex.jsoncat app.vex.json | jq .
# Si jq retourne une erreur, le JSON est invalide❌ Utiliser vexctl verify (commande supprimée)
Section intitulée « ❌ Utiliser vexctl verify (commande supprimée) »La sous-commande a disparu des versions récentes : jq vérifie la syntaxe JSON,
mais rien ne contrôle la cohérence sémantique du document, par exemple un
not_affected sans justification.
# ❌ vexctl verify n'existe plus dans v0.4.1vexctl verify app.vex.json# Error: unknown command "verify"
# ✅ Valider avec jqcat app.vex.json | jq . > /dev/null && echo "✓ VEX valide"Ressources
Section intitulée « Ressources »Deux références comptent au-delà de l'outil. La spécification OpenVEX fixe
la liste exacte des statuts et des justifications acceptés, seule source fiable
puisque vexctl ne valide pas la cohérence sémantique de ce qu'il produit. Le
document de la CISA définit, lui, les informations minimales attendues d'un
VEX destiné à un tiers, ce qui devient déterminant dès que vos documents sortent
de l'organisation dans un contexte de conformité.
Documentation officielle :
Guides connexes :
- VEX : réduire les faux positifs
- Grype : scanner de vulnérabilités
- SBOM : inventaire logiciel
- Cosign : signature d'artefacts
À retenir
Section intitulée « À retenir »3 commandes essentielles :
-
create : générer un statement VEX pour une CVE
Fenêtre de terminal vexctl create --product=... --vuln=... --status=... > file.vex.json -
merge : combiner plusieurs VEX ou ajouter un statement
Fenêtre de terminal vexctl merge existing.vex.json new.vex.json > merged.vex.json -
Validation : vérifier le JSON avec jq
Fenêtre de terminal cat file.vex.json | jq .
Prochaines étapes :
- Scannez une de vos images avec Grype
- Identifiez 1-2 faux positifs
- Créez un VEX avec vexctl
- Rescannez avec
--vexpour vérifier le filtrage