Exercice 03 : Intégrer Trivy dans un pipeline CI/CD GitHub Actions
🎯 Objectifs
À la fin de cet exercice, vous serez capable de :
- ✅ Construire et scanner une image Docker avec Trivy dans GitHub Actions
- ✅ Bloquer le pipeline si une vulnérabilité CRITICAL est détectée
- ✅ Exporter un rapport SARIF pour l'intégration dans GitHub Security
- ✅ Scanner le filesystem du projet (dépendances + code)
- ✅ Scanner les fichiers Dockerfile et IaC avec Trivy config
Durée estimée : 30 minutes
Difficulté : ⭐⭐☆☆☆ (Intermédiaire)
Prérequis : Exercice Trivy débutant complété, Docker, Compte GitHub
📖 Contexte
Vous avez appris à utiliser Trivy manuellement dans le module débutant. L'étape suivante est l'intégration CI/CD : chaque image Docker buildée doit être scannée automatiquement avant d'être poussée sur le registre. Si des vulnérabilités critiques sont trouvées, le build échoue et l'image ne part pas en production.
GitHub Actions propose une action officielle aquasecurity/trivy-action qui simplifie cette intégration et peut envoyer les résultats directement dans l'onglet Security de GitHub (via le format SARIF).
📋 Énoncé
Construisez un pipeline GitHub Actions complet avec scan Trivy, blocage sur les CRITICAL, et rapport de sécurité.
🧭 Déroulement de l'exercice
Tâche 1 : Créer une application et un Dockerfile
Créez une application Node.js simple avec un Dockerfile :
mkdir trivy-ci-demo && cd trivy-ci-demo
git init
# Application
cat > app.js << 'EOF'
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello from secure container!\n');
});
server.listen(8080, () => console.log('Server running on port 8080'));
EOF
# Dockerfile avec une image de base intentionnellement ancienne
cat > Dockerfile << 'EOF'
# Image ancienne avec des vulnérabilités connues (pour la démonstration)
FROM node:16-alpine
WORKDIR /app
COPY app.js .
# Utilisateur non-root (bonne pratique)
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
USER nodejs
EXPOSE 8080
CMD ["node", "app.js"]
EOF
# .dockerignore
cat > .dockerignore << 'EOF'
.git
.github
*.md
node_modules
EOF
git add .
git commit -m "feat: application Node.js avec Dockerfile"Tâche 2 : Créer le workflow GitHub Actions avec Trivy
mkdir -p .github/workflows
cat > .github/workflows/security-scan.yml << 'EOF'
name: Security Scan - Trivy
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
# Job 1 : Scanner le filesystem (dépendances du projet)
scan-filesystem:
name: Scanner le code source
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Scanner le filesystem avec Trivy
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
format: 'sarif'
output: 'trivy-fs-results.sarif'
severity: 'CRITICAL,HIGH'
exit-code: '0' # Ne pas bloquer sur le scan filesystem
- name: Uploader les résultats dans GitHub Security
uses: github/codeql-action/upload-sarif@v3
if: always() # Uploader même si des vulns sont trouvées
with:
sarif_file: 'trivy-fs-results.sarif'
category: 'trivy-filesystem'
# Job 2 : Builder et scanner l'image Docker
scan-docker:
name: Scanner l'image Docker
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Construire l'image Docker
run: docker build -t mon-app:${{ github.sha }} .
- name: Scanner l'image avec Trivy (bloquant sur CRITICAL)
uses: aquasecurity/trivy-action@master
with:
image-ref: 'mon-app:${{ github.sha }}'
format: 'table'
exit-code: '1' # ⚠️ Bloque le pipeline si des CRITICAL sont trouvées
severity: 'CRITICAL'
ignore-unfixed: true # Ignorer si pas de correctif disponible
- name: Générer le rapport SARIF
if: always() # Générer même si le job précédent a échoué
uses: aquasecurity/trivy-action@master
with:
image-ref: 'mon-app:${{ github.sha }}'
format: 'sarif'
output: 'trivy-image-results.sarif'
severity: 'CRITICAL,HIGH,MEDIUM'
- name: Uploader le rapport dans GitHub Security
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: 'trivy-image-results.sarif'
category: 'trivy-docker'
EOFIndice : Il y a deux étapes Trivy séparées dans le job Docker :
1. Une qui bloque le pipeline (
exit-code: '1') mais ne génère pas de SARIF (table format)2. Une qui génère le rapport SARIF sans bloquer (
if: always())C'est séparé car si on blocait avec le SARIF, on ne pourrait pas uploader les résultats.
Tâche 3 : Scanner les fichiers IaC avec Trivy
Ajoutez un scan des fichiers Dockerfile et configurations Kubernetes :
# Ajouter un scan config au workflow
cat >> .github/workflows/security-scan.yml << 'EOF'
# Job 3 : Scanner les fichiers de configuration (IaC)
scan-config:
name: Scanner les configs IaC
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Scanner Dockerfile et configs K8s
uses: aquasecurity/trivy-action@master
with:
scan-type: 'config'
scan-ref: '.'
format: 'table'
severity: 'CRITICAL,HIGH'
exit-code: '0' # Informatif, ne pas bloquer
EOFCréez un manifeste Kubernetes pour que Trivy ait quelque chose à analyser :
cat > deployment.yaml << 'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: mon-app
spec:
replicas: 2
selector:
matchLabels:
app: mon-app
template:
metadata:
labels:
app: mon-app
spec:
containers:
- name: mon-app
image: mon-app:latest
ports:
- containerPort: 8080
# Mauvaise pratique détectée par Trivy :
# - pas de securityContext
# - pas de resource limits
# - image tag "latest"
EOFVérification : Trivy doit signaler des problèmes dans deployment.yaml (absence de securityContext, resources, utilisation de latest).
Tâche 4 : Pousser et observer le pipeline
git add .github/ deployment.yaml
git commit -m "ci: intégrer Trivy dans le pipeline GitHub Actions"
git push origin main- Dans l'onglet Actions, observez les 3 jobs s'exécuter
- Si le scan Docker échoue sur CRITICAL, le job affiche un ❌
- Dans l'onglet Security → Code scanning, les résultats SARIF apparaissent avec :
- Le fichier/image concerné
- La CVE et sa description
- La version corrigée disponible
Indice : Si
node:16-alpinen'a pas de CRITICAL (l'image Alpine est bien maintenue), le pipeline passe. Pour tester le blocage, utilisez temporairementFROM node:14(version EOL avec plus de vulnérabilités).
Tâche 5 : Corriger et améliorer le Dockerfile
Mettez à jour le Dockerfile vers une image récente et observez la diminution des vulnérabilités :
cat > Dockerfile << 'EOF'
# Image récente et maintenue
FROM node:22-alpine
WORKDIR /app
COPY app.js .
# Utilisateur non-root
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001 && \
chown -R nodejs:nodejs /app
USER nodejs
# Metadata
LABEL org.opencontainers.image.source="https://github.com/votre-org/trivy-ci-demo"
EXPOSE 8080
CMD ["node", "app.js"]
EOF
git add Dockerfile
git commit -m "security: mettre à jour vers node:22-alpine"
git pushVérification : Le nouveau pipeline doit passer sans CRITICAL pour node:22-alpine.
✅ Vérification du résultat
- Le workflow
security-scan.ymlcontient 3 jobs (filesystem, docker, config) - Le job docker bloque (
exit-code: '1') sur les CRITICAL - Les rapports SARIF sont uploadés dans GitHub Security
- L'onglet Security → Code scanning affiche des résultats Trivy
deployment.yamlgénère des alertes config (securityContext manquant)
💡 À retenir
Structure d'un pipeline de sécurité Docker complet :
Build → Trivy FS scan → Trivy Image scan (bloquant) → Push registre
↓
SARIF → GitHub SecurityLa règle : ne jamais pousser une image avec des CRITICAL sur le registre. Utiliser ignore-unfixed: true pour ne bloquer que sur les CVE pour lesquelles un fix existe.
✨ Solution Complète
# Workflow minimal bloquant sur les CRITICAL
- name: Scanner l'image (bloquant)
uses: aquasecurity/trivy-action@master
with:
image-ref: 'mon-app:latest'
exit-code: '1'
severity: 'CRITICAL'
ignore-unfixed: true