Aller au contenu principal
Retour au blog

Docker Scout Health Scores : bloquer les CVE critiques en CI

Flavien Métivier24 septembre 20248 min

Tu déploies une image PHP 8.3 en production, tu sais vaguement qu'elle contient des CVE, mais aucun signal clair ne t'indique si la situation est critique ou simplement négligeable. Le 22 août 2024, Docker a annoncé en bêta publique les Health Scores : une notation alphabétique de A à F pour chaque image hébergée sur Docker Hub. Un coup d'œil suffit désormais pour évaluer la posture sécurité d'une image, sans dépouiller des centaines de vulnérabilités brutes. Ce tutoriel te montre comment lire ce score depuis la CLI, parser le rapport JSON Scout et, surtout, configurer un gate GitHub Actions qui bloque automatiquement tout merge si la note descend sous C.

Docker Scout Health Scores : une note alphabétique pour ta sécurité container

Les Health Scores sont le résultat d'une agrégation intelligente des données que Docker Scout collecte déjà sur chaque image poussée sur Docker Hub : inventaire des paquets, matching CVE, disponibilité des correctifs, fraîcheur de l'image de base. Plutôt qu'une liste brute de vulnérabilités, Docker produit une lettre unique — de A (excellente posture) à F (vulnérabilités critiques non corrigées présentes) — visible directement sur la page Docker Hub de l'image. La note est calculée à partir de plusieurs critères pondérés.

  • Nombre de CVE par niveau de sévérité : Critical, High, Medium, Low
  • Part des CVE fixables — corrigeables par un simple bump de la base image
  • Fraîcheur de l'image de base par rapport aux releases officielles
  • Présence d'une SBOM (Software Bill of Materials) dans les attestations de l'image
  • Ratio CVE non-fixables vs fixables (une CVE sans patch upstream pèse moins lourd)
  • Score CVSS agrégé des vulnérabilités présentes

En pratique, une image officielle php:8.3-fpm-alpine atteint généralement un A ou B grâce à la minimalité d'Alpine et aux mises à jour régulières des mainteneurs Docker. Une image maison construite sur ubuntu:20.04 non mise à jour depuis six mois décrochera facilement un D ou F. La note est visible sur Docker Hub sous l'onglet Tags de chaque repository, à condition que l'image ait été scannée par Scout — automatique pour les images publiques et pour les repositories privés liés à une organisation Docker avec Scout activé.

Prendre en main la CLI Scout

La CLI Docker Scout est intégrée à Docker Desktop depuis la version 4.17. Sur un serveur Linux ou dans une pipeline CI, elle s'installe en standalone. Avant de brancher GitHub Actions, valide que tout fonctionne en local — c'est aussi le meilleur moyen de comprendre ce que le gate CI va analyser.

# Vérifier que Scout est disponible
docker scout version

# Scanner une image locale ou distante
# La première commande donne un résumé rapide
docker scout quickview myrepo/myimage:latest

# Lister tous les CVE de l'image
docker scout cves myrepo/myimage:latest

# Filtrer uniquement Critical et High
docker scout cves --only-severity critical,high myrepo/myimage:latest

# Voir uniquement les CVE pour lesquels un fix existe
docker scout cves --only-fixed myrepo/myimage:latest

# Recommandations de mise à jour de la base image
docker scout recommendations myrepo/myimage:latest

La commande quickview produit un tableau de bord condensé qui résume les CVE par catégorie et identifie la base image utilisée. Lance-la en premier pour avoir une photographie rapide avant d'aller dans le détail.

 Pulled
 SBOM of image already cached, 312 packages indexed

  Target  myrepo/myimage:latest    0C    3H   12M    5L
    digest  sha256:d4f8a1...
  Base image│  php:8.3-fpm-bullseye    0C    2H    9M    4L

  What's next:
    Refresh base image to reduce 3H and 9M  → docker scout recommendations myrepo/myimage:latest
    Analyze full CVE list                   → docker scout cves myrepo/myimage:latest

Extraire et parser le rapport JSON Scout

La CLI Scout peut produire un rapport JSON structuré, indispensable pour du parsing automatisé en pipeline. C'est ce format que tu vas utiliser pour trier les CVE critiques par score CVSS et décider quoi patcher en priorité.

# Générer le rapport JSON complet
docker scout cves --format json myrepo/myimage:latest > scout-report.json

# Vérifier la structure de haut niveau
jq 'keys' scout-report.json
{
  "imageId": "sha256:d4f8a1bc...",
  "tags": ["myrepo/myimage:latest"],
  "platform": "linux/amd64",
  "vulnerabilities": [
    {
      "cvss": {
        "score": 9.8,
        "severity": "CRITICAL"
      },
      "cve": "CVE-2024-2511",
      "packageName": "openssl",
      "packageVersion": "3.1.4",
      "fixedVersion": "3.1.5",
      "description": "Unbounded memory growth with session handling in TLSv1.3"
    }
  ],
  "summary": {
    "critical": 1,
    "high": 3,
    "medium": 12,
    "low": 5
  }
}
# CVE Critical et High triées par score CVSS décroissant
jq '
  .vulnerabilities
  | map(select(.cvss.severity == "CRITICAL" or .cvss.severity == "HIGH"))
  | sort_by(-.cvss.score)
  | .[] | {
      cve: .cve,
      score: .cvss.score,
      severity: .cvss.severity,
      package: .packageName,
      installed: .packageVersion,
      fix: .fixedVersion
    }
' scout-report.json

# Compter les CVE fixables uniquement
jq '[.vulnerabilities[] | select(.fixedVersion != null and .fixedVersion != "")] | length' scout-report.json

Votre dette technique s'accumule ?

Demander un audit

Configurer le gate CI/CD dans GitHub Actions

Docker fournit une action officielle docker/scout-action@v1 qui s'intègre nativement dans GitHub Actions et peut poster un commentaire récapitulatif directement sur la pull request. Voici un workflow complet : build de l'image, push sur Docker Hub, puis scan Scout avec sortie non-nulle si des vulnérabilités hautes ou critiques sont détectées.

# .github/workflows/docker-security.yml
name: Docker Security Gate

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

env:
  REGISTRY: docker.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build-and-scan:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      security-events: write
      pull-requests: write

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=ref,event=branch
            type=ref,event=pr
            type=sha,prefix=sha-

      - name: Build and push
        id: build
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Docker Scout - Scan CVE et gate
        id: scout
        uses: docker/scout-action@v1
        with:
          command: cves
          image: ${{ steps.meta.outputs.tags }}
          only-severities: critical,high
          write-comment: true
          github-token: ${{ secrets.GITHUB_TOKEN }}
          exit-code: true

      - name: Docker Scout - Quickview résumé
        if: always()
        uses: docker/scout-action@v1
        with:
          command: quickview
          image: ${{ steps.meta.outputs.tags }}

Le paramètre clé est exit-code: true : si Scout détecte au moins un CVE de sévérité critical ou high, le step retourne un code d'erreur non-nul et le job échoue. Couplé à une branch protection rule qui exige que ce job passe, le merge est automatiquement bloqué. Le paramètre write-comment: true génère un commentaire récapitulatif sur la PR avec le détail des CVE trouvées — le développeur sait exactement quoi corriger sans ouvrir Docker Hub. Le step quickview avec if: always() assure qu'on dispose du résumé même quand le gate a échoué.

Implémenter le seuil grade C : stratégie en deux temps

Un grade C sur les Health Scores correspond approximativement à la présence de CVE hautes sans correctif immédiat, mais sans critique non-patchable. Pour éviter de bloquer sur des CVE pour lesquelles personne ne peut rien faire (aucun fix upstream disponible), on applique une stratégie en deux temps : avertissement sur les HIGH, blocage strict uniquement sur les CRITICAL fixables.

      # Step 1 : Warning sur les CVE High (ne bloque pas)
      - name: Scout - Gate WARNING (High)
        uses: docker/scout-action@v1
        with:
          command: cves
          image: ${{ steps.meta.outputs.tags }}
          only-severities: high
          write-comment: true
          github-token: ${{ secrets.GITHUB_TOKEN }}
          exit-code: false    # Signalement uniquement

      # Step 2 : Blocage strict sur Critical fixables
      - name: Scout - Gate BLOQUANT (Critical + fix disponible)
        uses: docker/scout-action@v1
        with:
          command: cves
          image: ${{ steps.meta.outputs.tags }}
          only-severities: critical
          only-fixed: true    # Uniquement si un fix existe
          write-comment: false
          exit-code: true     # Bloque le merge

      # Step 3 : Comparaison avec la branche main pour détecter les régressions
      - name: Scout - Régression vs main
        if: github.event_name == 'pull_request'
        uses: docker/scout-action@v1
        with:
          command: compare
          image: ${{ steps.meta.outputs.tags }}
          to: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:main
          only-severities: critical,high
          write-comment: true
          github-token: ${{ secrets.GITHUB_TOKEN }}
          exit-code: true

La combinaison only-severities: critical + only-fixed: true est la règle la plus pragmatique pour un gate : elle ne bloque que lorsqu'une vulnérabilité critique dispose effectivement d'un correctif. Tu évites la situation classique où la pipeline reste rouge des semaines parce qu'un CVE critique est connu mais que le mainteneur du paquet n'a pas encore publié de fix. Le step de comparaison avec main est un bonus précieux : il détecte les régressions introduites par la PR, indépendamment du niveau absolu de vulnérabilités.

Investiguer et prioriser les CVE : commandes clés

Une fois la pipeline en place, le premier scan révèle souvent une liste conséquente de CVE sur l'image de base. Avant de patcher dans tous les sens, identifie les actions à plus fort impact.

# Quelles CVE pourrais-je éliminer immédiatement en changeant la base image ?
docker scout recommendations myrepo/myimage:latest

# Comparer deux versions pour mesurer une amélioration
docker scout compare \
  myrepo/myimage:v1.2.0 \
  --to myrepo/myimage:v1.3.0 \
  --ignore-unchanged

# Scanner uniquement les CVE sans fix dispo (informationnel, pas bloquant)
docker scout cves --only-unfixed myrepo/myimage:latest

# Rapport JSON pour intégration dans un dashboard ou Slack webhook
docker scout cves --format json --only-severity critical myrepo/myimage:latest \
  | jq '{total_critical: .summary.critical, packages: [.vulnerabilities[].packageName] | unique}'

La commande recommendations est le point de départ idéal : elle indique directement quelle version de ta base image éliminerait le plus de CVE en un seul changement de ligne FROM. Pour les projets PHP, le gain est souvent spectaculaire. Passer de php:8.3-fpm-bullseye à php:8.3-fpm-alpine suffit fréquemment à faire monter une image de grade D à grade A, en éliminant des centaines de paquets système inutiles.

# Avant : Debian Bullseye — surface d'attaque maximale, souvent grade C/D
FROM php:8.3-fpm-bullseye

RUN apt-get update && apt-get install -y \
    libpq-dev \
    libicu-dev \
    && docker-php-ext-install pdo_pgsql intl

# Après : Alpine — grade A dans la majorité des cas
FROM php:8.3-fpm-alpine

RUN apk add --no-cache \
    libpq-dev \
    icu-dev \
    icu-libs \
    && docker-php-ext-install pdo_pgsql intl \
    # Nettoyer les headers et outils de build
    && rm -rf /var/cache/apk/*

# Bonne pratique complémentaire : utilisateur non-root
RUN addgroup -g 1000 app && adduser -u 1000 -G app -s /bin/sh -D app
USER app

En combinant un Dockerfile minimal, un gate GitHub Actions rigoureux et la commande recommendations lancée régulièrement, tu passes d'une gestion réactive des CVE à une posture proactive. Le Health Score n'est pas une fin en soi : c'est un tableau de bord qui rend visible ce qui était invisible, et qui te force à agir avant que l'accumulation de dette sécurité devienne une urgence de production.

Cet article vous a plu ? Partagez-le !

Votre dette technique s'accumule ?

Audit complet en 10 jours. Recommandations priorisées et actionnables.