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 »Workflow 1 : Traiter les faux positifs d’un scan Grype
Section intitulée « Workflow 1 : Traiter les faux positifs d’un scan Grype »-
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 »# 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) »# 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 »# 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 »GitHub Actions
Section intitulée « GitHub Actions »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@v4
- name: Build image run: docker build -t ghcr.io/${{ github.repository }}:${{ github.sha }} .
- name: Install tools run: | # Syft curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin v1.38.2
# Grype curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin v0.104.2
# 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 »1. Versionner les VEX avec les images
Section intitulée « 1. Versionner les VEX avec les images »# 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 »# ✅ 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 »# ❌ 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 »# ✅ 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 »❌ Oublier le digest dans —product
Section intitulée « ❌ Oublier le digest dans —product »# ❌ 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 »# ❌ 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é »# ✅ 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) »# ❌ 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 »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