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
Mise à jour :
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.
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 :
.vex.json pour les faux positifsgrype --vex filtre les faux positifs documentés → 11 CVEAvant d’utiliser vexctl, comprenez ce qu’il génère :
Un document VEX contient :
Chaque statement précise :
not_affected, affected, fixed, ou under_investigationnot_affected)Connaissances :
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 versionvexctl propose deux commandes essentielles : create pour générer des
statements VEX, et merge pour les combiner.
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 |
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
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” |
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.jsonLa 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.
Scanner et exporter les CVE en JSON
# 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.jsonExtraire la liste des CVE uniques
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-zzzzAnalyser chaque CVE manuellement
Pour chaque CVE, vérifier :
curl https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=CVE-2024-1234Créer un statement VEX pour chaque faux positif
# 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.jsonAjouter les autres CVE au même VEX
# 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.json
mv temp.vex.json mon-app.vex.jsonRescanner avec le VEX
# 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)"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"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.jsondoneUne fois vos VEX créés avec vexctl, distribuez-les de façon sécurisée.
# 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)# 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.0# 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.jsonname: Build + SBOM + VEX
on: push: branches: [main]
jobs: security-scan: runs-on: ubuntu-latest 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 }}# 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"# ✅ 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"Un VEX marqué not_affected peut devenir affected si :
Recommandation : réviser les VEX à chaque changement majeur de l’application ou tous les 3 mois.
# ❌ 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 nombreuses# ✅ 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édents# ❌ Tag mutable (peut changer)vexctl create --product="pkg:oci/mon-app:latest"
# ✅ Digest SHA256 (immuable)vexctl create --product="pkg:oci/mon-app@sha256:abc123def456..."# ❌ Flag qui n'existe pas dans vexctl v0.4.1vexctl create --statement="Explication..."# Error: unknown flag: --statement
# ✅ Flag correctvexctl create --impact-statement="Explication..."# ✅ 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# ❌ 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"Documentation officielle :
Guides connexes :
3 commandes essentielles :
create : générer un statement VEX pour une CVE
vexctl create --product=... --vuln=... --status=... > file.vex.jsonmerge : combiner plusieurs VEX ou ajouter un statement
vexctl merge existing.vex.json new.vex.json > merged.vex.jsonValidation : vérifier le JSON avec jq
cat file.vex.json | jq .Prochaines étapes :
--vex pour vérifier le filtrage