TP : Pipeline DevSecOps locale avec Gitea, Woodpecker, Trivy et Semgrep
Prérequis
Environnement technique
- Docker installé et fonctionnel.
- Docker Compose disponible (
docker compose version). - Git installé.
- curl installé.
- jq installé pour lire et filtrer les rapports JSON.
- Accès à un terminal Bash ou Zsh.
- Accès Internet pour télécharger les images Docker.
Installer jq
Sur Debian / Ubuntu :
sudo apt updatesudo apt install -y jqSur macOS avec Homebrew :
brew install jqInstallation des outils
Sur Debian / Ubuntu (ou WSL2 Ubuntu)
sudo apt-get updatesudo apt-get install -y docker.io docker-compose-plugin git curl jqsudo systemctl enable dockersudo systemctl start dockersudo usermod -aG docker "$USER"Se déconnecter puis se reconnecter si l’utilisateur vient d’être ajouté au groupe Docker.
Sur macOS
brew install git curl jqInstaller Docker Desktop depuis docker.com, puis vérifier :
docker --versiondocker compose versionSur Windows
L’environnement recommandé est WSL2 avec Ubuntu :
wsl --versionwsl --install -d UbuntuInstaller Docker Desktop et activer l’intégration WSL2, puis dans Ubuntu WSL :
sudo apt-get updatesudo apt-get install -y git curl jqdocker --versiondocker compose versionVérification des outils
docker --versiondocker compose versiongit --versioncurl --versionjq --versionPrécaution
Ce TP crée uniquement un environnement local.
Aucune ressource cloud n’est créée.
Les conteneurs et volumes doivent être supprimés à la fin du TP.
Le mot de passe Gitea est défini en clair dans scripts/env.sh. Ne pas utiliser ce fichier dans un environnement partagé.
Outils utilisés
| Composant | Image Docker | Rôle |
|---|---|---|
| Gitea | gitea/gitea:1.26 | Forge Git locale |
| Woodpecker Server | woodpeckerci/woodpecker-server:v3.15 | Moteur CI local |
| Woodpecker Agent | woodpeckerci/woodpecker-agent:v3.15 | Agent CI local |
| Trivy | aquasec/trivy:0.71.0 | Scan secrets, SCA, SBOM |
| Semgrep | semgrep/semgrep:1.127.0 | SAST |
Objectifs
À la fin de ce TP, l’apprenant doit être capable de :
- Déployer une forge Git locale avec Gitea.
- Déployer un moteur CI local avec Woodpecker.
- Connecter Woodpecker à Gitea via OAuth.
- Écrire une pipeline DevSecOps avec plusieurs étapes de contrôle.
- Détecter des secrets en clair avec Trivy.
- Détecter des patterns dangereux avec Semgrep.
- Détecter des dépendances vulnérables avec Trivy.
- Générer un SBOM au format CycloneDX avec Trivy.
- Bloquer une pipeline avec un gate de sécurité.
- Corriger les findings et valider le passage de la pipeline.
Contrôles intégrés
| Contrôle | Outil | Résultat attendu |
|---|---|---|
| Secrets | Trivy | Détecter une clé ou un mot de passe en clair |
| SAST | Semgrep | Détecter un pattern de code dangereux |
| SCA | Trivy | Détecter des dépendances vulnérables |
| SBOM | Trivy | Générer un inventaire CycloneDX |
| Gate CI | Script Bash | Bloquer la pipeline si des risques sont détectés |
Le TP suit deux phases :
| Phase | Description |
|---|---|
| Version vulnérable | La pipeline échoue volontairement |
| Version corrigée | La pipeline passe après correction |
Configuration du fichier hosts
Le TP utilise les noms locaux gitea et woodpecker qui doivent pointer vers 127.0.0.1.
Sur Linux ou macOS
echo "127.0.0.1 gitea woodpecker" | sudo tee -a /etc/hostsgrep "gitea woodpecker" /etc/hostsSur Windows
Ouvrir PowerShell en administrateur :
Add-Content -Path "C:\Windows\System32\drivers\etc\hosts" -Value "127.0.0.1 gitea woodpecker"Select-String -Path "C:\Windows\System32\drivers\etc\hosts" -Pattern "gitea"Commandes
1. Créer l’arborescence du TP
mkdir -p tp-pipeline-devsecops-localcd tp-pipeline-devsecops-local
mkdir -p platform demo-app reports scriptsfind . -maxdepth 2 -type d | sortRésultat attendu :
../demo-app./platform./reports./scripts2. Définir les variables du TP
cat > scripts/env.sh <<'EOF'export TP_NAME="tp-pipeline-devsecops-local"export GITEA_URL="http://gitea:3000"export WOODPECKER_URL="http://woodpecker:8000"export GITEA_USER="formation"export GITEA_PASSWORD="Formation1234"export GITEA_EMAIL="formation@example.local"export REPO_NAME="demo-devsecops"export REPO_DIR="demo-app"export WOODPECKER_AGENT_SECRET="formation-woodpecker-agent-secret-local"EOFsource scripts/env.shecho "$TP_NAME"echo "$GITEA_URL"echo "$WOODPECKER_URL"echo "$REPO_NAME"3. Créer le fichier .env de la plateforme
cat > platform/.env <<EOFWOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET}WOODPECKER_GITEA_CLIENT=TO_BE_FILLEDWOODPECKER_GITEA_SECRET=TO_BE_FILLEDEOFcat platform/.env4. Créer le fichier Docker Compose
cat > platform/docker-compose.yml <<'EOF'services: gitea: image: gitea/gitea:1.26 container_name: devsecops-gitea environment: - USER_UID=1000 - USER_GID=1000 - GITEA__database__DB_TYPE=sqlite3 - GITEA__server__DOMAIN=gitea - GITEA__server__ROOT_URL=http://gitea:3000/ - GITEA__server__HTTP_PORT=3000 - GITEA__service__DISABLE_REGISTRATION=false - GITEA__webhook__ALLOWED_HOST_LIST=external,loopback,gitea,woodpecker volumes: - gitea_data:/data ports: - "3000:3000" - "2222:22" networks: - devsecops
woodpecker: image: woodpeckerci/woodpecker-server:v3.15 container_name: devsecops-woodpecker depends_on: - gitea environment: - WOODPECKER_OPEN=true - WOODPECKER_ADMIN=formation - WOODPECKER_HOST=http://woodpecker:8000 - WOODPECKER_GITEA=true - WOODPECKER_GITEA_URL=http://gitea:3000 - WOODPECKER_GITEA_CLIENT=${WOODPECKER_GITEA_CLIENT} - WOODPECKER_GITEA_SECRET=${WOODPECKER_GITEA_SECRET} - WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET} volumes: - woodpecker_server_data:/var/lib/woodpecker ports: - "8000:8000" networks: - devsecops
woodpecker-agent: image: woodpeckerci/woodpecker-agent:v3.15 container_name: devsecops-woodpecker-agent command: agent depends_on: - woodpecker environment: - WOODPECKER_SERVER=woodpecker:9000 - WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET} - WOODPECKER_BACKEND=docker - WOODPECKER_BACKEND_DOCKER_NETWORK=devsecops-local volumes: - /var/run/docker.sock:/var/run/docker.sock networks: - devsecops
networks: devsecops: name: devsecops-local
volumes: gitea_data: woodpecker_server_data:EOF5. Démarrer Gitea
cd platformdocker compose up -d giteacd ..Attendre le démarrage :
until curl -s http://gitea:3000 >/dev/null; do echo "Gitea en cours de démarrage..." sleep 5doneecho "Gitea disponible."curl -I http://gitea:3000 | tee reports/01-gitea-http.txtRésultat attendu :
HTTP/1.1 200 OK6. Créer l’utilisateur administrateur Gitea
cd platformdocker compose exec -u git gitea gitea admin user create \ --username "${GITEA_USER}" \ --password "${GITEA_PASSWORD}" \ --email "${GITEA_EMAIL}" \ --admin \ --must-change-password=false || truecd ..curl -s -u "${GITEA_USER}:${GITEA_PASSWORD}" \ "${GITEA_URL}/api/v1/user" \ | jq .login \ | tee reports/02-gitea-user-check.txtRésultat attendu :
"formation"Configuration OAuth Gitea pour Woodpecker
7. Créer l’application OAuth dans Gitea
Ouvrir Gitea dans un navigateur :
http://gitea:3000Se connecter avec :
| Champ | Valeur |
|---|---|
| Utilisateur | formation |
| Mot de passe | Formation1234 |
Créer une application OAuth :
Avatar utilisateur → Settings → Applications → OAuth2 Applications| Champ | Valeur |
|---|---|
| Application name | woodpecker |
| Redirect URI | http://woodpecker:8000/authorize |
Copier les deux valeurs générées :
| Valeur | Variable |
|---|---|
| Client ID | WOODPECKER_GITEA_CLIENT |
| Client Secret | WOODPECKER_GITEA_SECRET |
8. Renseigner les secrets OAuth dans le fichier .env
nano platform/.envRemplacer TO_BE_FILLED par les valeurs générées dans Gitea.
Vérifier sans afficher les secrets complets :
grep "WOODPECKER_GITEA_CLIENT" platform/.envgrep "WOODPECKER_GITEA_SECRET" platform/.env9. Démarrer Woodpecker
cd platformdocker compose up -dcd ..cd platformdocker compose ps | tee ../reports/03-platform-containers.txtcd ..Résultat attendu :
devsecops-gitea Updevsecops-woodpecker Updevsecops-woodpecker-agent Upcurl -I http://woodpecker:8000 | tee reports/04-woodpecker-http.txtRésultat attendu :
HTTP/1.1 200 OKOuvrir Woodpecker dans un navigateur et se connecter avec Gitea :
http://woodpecker:8000Création de l’application vulnérable
10. Créer la structure applicative
mkdir -p demo-app/app demo-app/rules demo-app/scripts demo-app/reportscat > demo-app/.gitignore <<'EOF'reports/*.jsonreports/*.txtreports/*.cdx.json__pycache__/*.pyc.venv/EOF11. Créer une application Python vulnérable
cat > demo-app/app/main.py <<'EOF'from flask import Flask, requestimport subprocess
app = Flask(__name__)
@app.route("/")def index(): name = request.args.get("name", "world") command = "echo Hello " + name result = subprocess.run( command, shell=True, capture_output=True, text=True ) return result.stdout
if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=True)EOFcat > demo-app/app/config.py <<'EOF'AWS_ACCESS_KEY_ID = "AKIA1234567890ABCDEF"AWS_SECRET_ACCESS_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"DATABASE_PASSWORD = "SuperSecretPassword123!"EOFcat > demo-app/requirements.txt <<'EOF'Flask==0.12.2Jinja2==2.10requests==2.19.1PyYAML==5.3.1EOFcat > demo-app/README.md <<'EOF'# Demo DevSecOps
Application volontairement vulnérable pour tester une pipeline DevSecOps locale.
| Contrôle | Outil ||---|---|| Secrets | Trivy || SAST | Semgrep || SCA | Trivy || SBOM | Trivy |EOF12. Créer les règles SAST Semgrep
cat > demo-app/rules/semgrep.yml <<'EOF'rules: - id: python-subprocess-shell-true languages: - python severity: ERROR message: "Utilisation dangereuse de subprocess avec shell=True" patterns: - pattern: subprocess.$FUNC(..., shell=True, ...)
- id: flask-debug-true languages: - python severity: ERROR message: "Mode debug Flask activé" pattern: app.run(..., debug=True, ...)EOF13. Créer le script de gate sécurité
cat > demo-app/scripts/security-gate.sh <<'EOF'#!/usr/bin/env bashset -euo pipefail
REPORT_DIR="${REPORT_DIR:-reports}"SECRETS_REPORT="${REPORT_DIR}/trivy-secrets.json"SAST_REPORT="${REPORT_DIR}/semgrep-sast.json"SCA_REPORT="${REPORT_DIR}/trivy-sca.json"SBOM_FILE="${REPORT_DIR}/sbom.cdx.json"
for f in "$SECRETS_REPORT" "$SAST_REPORT" "$SCA_REPORT" "$SBOM_FILE"; do if [ ! -f "$f" ]; then echo "Rapport manquant : $f" exit 2 fidone
SECRET_COUNT=$(jq '[.Results[]? | .Secrets[]?] | length' "$SECRETS_REPORT")SAST_COUNT=$(jq '.results | length' "$SAST_REPORT")SCA_HIGH_CRITICAL_COUNT=$(jq '[.Results[]? | .Vulnerabilities[]? | select(.Severity == "HIGH" or .Severity == "CRITICAL")] | length' "$SCA_REPORT")
echo "Synthèse sécurité"echo "Secrets détectés : $SECRET_COUNT"echo "Findings SAST Semgrep : $SAST_COUNT"echo "Vulnérabilités HIGH/CRITICAL : $SCA_HIGH_CRITICAL_COUNT"echo "SBOM généré : $SBOM_FILE"echo
if [ "$SECRET_COUNT" -gt 0 ]; then echo "Gate refusé : secrets détectés." exit 1fi
if [ "$SAST_COUNT" -gt 0 ]; then echo "Gate refusé : findings SAST détectés." exit 1fi
if [ "$SCA_HIGH_CRITICAL_COUNT" -gt 0 ]; then echo "Gate refusé : vulnérabilités HIGH ou CRITICAL détectées." exit 1fi
echo "Gate accepté : aucun blocage détecté."EOFchmod +x demo-app/scripts/security-gate.sh14. Créer la pipeline Woodpecker
cat > demo-app/.woodpecker.yaml <<'EOF'steps: prepare: image: alpine:3.20 commands: - mkdir -p reports - echo "Pipeline DevSecOps locale" - echo "Contrôles : secrets, SAST, SCA, SBOM"
secrets-trivy: image: aquasec/trivy:0.71.0 commands: - trivy fs --scanners secret --format json --output reports/trivy-secrets.json . - trivy fs --scanners secret --format table .
sast-semgrep: image: semgrep/semgrep:1.127.0 commands: - semgrep scan --config rules/semgrep.yml --json --output reports/semgrep-sast.json . || true - semgrep scan --config rules/semgrep.yml . || true
sca-trivy: image: aquasec/trivy:0.71.0 commands: - trivy fs --scanners vuln --severity HIGH,CRITICAL --format json --output reports/trivy-sca.json . - trivy fs --scanners vuln --severity HIGH,CRITICAL --format table .
sbom-trivy: image: aquasec/trivy:0.71.0 commands: - trivy fs --format cyclonedx --output reports/sbom.cdx.json .
security-gate: image: alpine:3.20 commands: - apk add --no-cache bash jq - bash scripts/security-gate.shEOFExécution locale des contrôles avant CI
15. Lancer le scan secrets avec Trivy
cd demo-appmkdir -p reportsdocker run --rm \ -v "$PWD":/src \ -w /src \ aquasec/trivy:0.71.0 \ fs --scanners secret --format json --output reports/trivy-secrets.json .jq '[.Results[]? | .Secrets[]?] | length' reports/trivy-secrets.json \ | tee reports/local-secret-count.txtRésultat attendu : 1 ou plus.
16. Lancer le scan SAST avec Semgrep
docker run --rm \ -v "$PWD":/src \ -w /src \ semgrep/semgrep:1.127.0 \ semgrep scan --config rules/semgrep.yml --json --output reports/semgrep-sast.json . || truejq '.results | length' reports/semgrep-sast.json \ | tee reports/local-sast-count.txtRésultat attendu : 2.
17. Lancer le scan SCA avec Trivy
docker run --rm \ -v "$PWD":/src \ -w /src \ aquasec/trivy:0.71.0 \ fs --scanners vuln --severity HIGH,CRITICAL --format json --output reports/trivy-sca.json .jq '[.Results[]? | .Vulnerabilities[]? | select(.Severity == "HIGH" or .Severity == "CRITICAL")] | length' \ reports/trivy-sca.json \ | tee reports/local-sca-high-critical-count.txtRésultat attendu : 1 ou plus.
18. Générer le SBOM
docker run --rm \ -v "$PWD":/src \ -w /src \ aquasec/trivy:0.71.0 \ fs --format cyclonedx --output reports/sbom.cdx.json .ls -lh reports/sbom.cdx.jsonjq '.bomFormat, .specVersion' reports/sbom.cdx.jsonRésultat attendu :
"CycloneDX"19. Exécuter le gate local
set +edocker run --rm \ -v "$PWD":/src \ -w /src \ alpine:3.20 \ sh -c "apk add --no-cache bash jq >/dev/null && bash scripts/security-gate.sh" \ | tee reports/local-security-gate.txtGATE_STATUS=${PIPESTATUS[0]}set -e
echo "${GATE_STATUS}" | tee reports/local-security-gate-status.txtRésultat attendu :
Gate refusécd ..Création du dépôt dans Gitea
20. Créer le dépôt Gitea avec l’API
curl -s -u "${GITEA_USER}:${GITEA_PASSWORD}" \ -H "Content-Type: application/json" \ -X POST "${GITEA_URL}/api/v1/user/repos" \ -d "{\"name\":\"${REPO_NAME}\",\"private\":false}" \ | jq . \ | tee reports/05-gitea-repo-create.jsoncurl -s -u "${GITEA_USER}:${GITEA_PASSWORD}" \ "${GITEA_URL}/api/v1/repos/${GITEA_USER}/${REPO_NAME}" \ | jq '.full_name' \ | tee reports/06-gitea-repo-check.txtRésultat attendu :
"formation/demo-devsecops"21. Initialiser le dépôt Git local
cd demo-app
git initgit config user.name "Formation DevSecOps"git config user.email "formation@example.local"
git add .git commit -m "Initial vulnerable DevSecOps demo"git branch -M main
git remote add origin "http://${GITEA_USER}:${GITEA_PASSWORD}@gitea:3000/${GITEA_USER}/${REPO_NAME}.git"git push -u origin main
cd ..Activation du dépôt dans Woodpecker
22. Activer le dépôt dans Woodpecker
Ouvrir Woodpecker dans un navigateur :
http://woodpecker:8000Se connecter avec Gitea, puis activer le dépôt :
Repositories → formation/demo-devsecops → Enable23. Déclencher la pipeline
cd demo-appgit commit --allow-empty -m "Trigger vulnerable pipeline"git pushcd ..Dans Woodpecker, vérifier le build formation/demo-devsecops.
Résultat attendu : la pipeline échoue à l’étape security-gate.
Correction de l’application
24. Corriger les secrets en clair
cat > demo-app/app/config.py <<'EOF'import os
AWS_ACCESS_KEY_ID = os.getenv("AWS_ACCESS_KEY_ID", "")AWS_SECRET_ACCESS_KEY = os.getenv("AWS_SECRET_ACCESS_KEY", "")DATABASE_PASSWORD = os.getenv("DATABASE_PASSWORD", "")EOF25. Corriger le code dangereux
cat > demo-app/app/main.py <<'EOF'from flask import Flask, requestimport osimport subprocess
app = Flask(__name__)
@app.route("/")def index(): name = request.args.get("name", "world") result = subprocess.run( ["echo", "Hello", name], check=True, capture_output=True, text=True ) return result.stdout
if __name__ == "__main__": debug_enabled = os.getenv("FLASK_DEBUG", "0") == "1" app.run(host="0.0.0.0", port=5000, debug=debug_enabled)EOF26. Corriger les dépendances vulnérables
cat > demo-app/requirements.txt <<'EOF'Flask==3.1.3requests==2.32.4PyYAML==6.0.2EOFSi un scan SCA détecte encore une vulnérabilité HIGH ou CRITICAL liée à une version devenue vulnérable après la rédaction du TP, mettre à jour la dépendance concernée vers la dernière version corrigée disponible sur PyPI.
27. Relancer les scans locaux
cd demo-apprm -rf reportsmkdir -p reportsdocker run --rm -v "$PWD":/src -w /src aquasec/trivy:0.71.0 \ fs --scanners secret --format json --output reports/trivy-secrets.json .
docker run --rm -v "$PWD":/src -w /src semgrep/semgrep:1.127.0 \ semgrep scan --config rules/semgrep.yml --json --output reports/semgrep-sast.json . || true
docker run --rm -v "$PWD":/src -w /src aquasec/trivy:0.71.0 \ fs --scanners vuln --severity HIGH,CRITICAL --format json --output reports/trivy-sca.json .
docker run --rm -v "$PWD":/src -w /src aquasec/trivy:0.71.0 \ fs --format cyclonedx --output reports/sbom.cdx.json .docker run --rm \ -v "$PWD":/src \ -w /src \ alpine:3.20 \ sh -c "apk add --no-cache bash jq >/dev/null && bash scripts/security-gate.sh" \ | tee reports/local-security-gate-after-fix.txtRésultat attendu :
Gate acceptécd ..28. Pousser la correction
cd demo-appgit add .git commit -m "Fix DevSecOps findings"git pushcd ..Dans Woodpecker, vérifier le nouveau build.
Résultat attendu :
Pipeline passedRapport de synthèse
29. Créer un rapport de synthèse local
{ echo "# Rapport TP : Pipeline DevSecOps locale" echo echo "## Environnement" echo echo "| Élément | Valeur |" echo "|---|---|" echo "| TP | ${TP_NAME} |" echo "| Forge | Gitea 1.26 |" echo "| CI | Woodpecker v3.15 |" echo "| Dépôt | ${GITEA_USER}/${REPO_NAME} |" echo "| URL Gitea | ${GITEA_URL} |" echo "| URL Woodpecker | ${WOODPECKER_URL} |" echo echo "## Contrôles intégrés" echo echo "| Contrôle | Outil | Fichier de sortie |" echo "|---|---|---|" echo "| Secrets | Trivy 0.71.0 | reports/trivy-secrets.json |" echo "| SAST | Semgrep 1.127.0 | reports/semgrep-sast.json |" echo "| SCA | Trivy 0.71.0 | reports/trivy-sca.json |" echo "| SBOM | Trivy 0.71.0 | reports/sbom.cdx.json |" echo "| Gate CI | Script Bash + jq | scripts/security-gate.sh |" echo echo "## Résultats" echo echo "| Étape | Résultat |" echo "|---|---|" echo "| Premier push | Pipeline échouée |" echo "| Correction | Findings corrigés |" echo "| Second push | Pipeline réussie |"} > reports/rapport-tp-pipeline-devsecops-local.md30. Afficher le rapport
cat reports/rapport-tp-pipeline-devsecops-local.md31. Lister les fichiers générés
find . -maxdepth 4 -type f | sort \ | tee reports/07-generated-files.txtNettoyage
32. Arrêter la plateforme et supprimer les volumes
cd platformdocker compose down -vcd ..docker volume ls | grep -E "gitea|woodpecker" || true33. Supprimer les images de test (optionnel)
docker image rm aquasec/trivy:0.71.0 semgrep/semgrep:1.127.0 || true34. Supprimer les entrées hosts
Sur Linux ou macOS :
sudo sed -i '/gitea woodpecker/d' /etc/hostsgrep "gitea woodpecker" /etc/hosts || trueSur Windows (PowerShell administrateur) :
(Get-Content "C:\Windows\System32\drivers\etc\hosts") | Where-Object { $_ -notmatch "gitea woodpecker" } | Set-Content "C:\Windows\System32\drivers\etc\hosts"35. Supprimer le dossier du TP
Se placer dans le dossier parent avant d’exécuter cette commande.
cd ..rm -rf tp-pipeline-devsecops-localRésultat attendu
À la fin du TP, les éléments suivants doivent avoir été validés :
| Élément | Validation |
|---|---|
| Gitea | Forge locale accessible sur http://gitea:3000 |
| Woodpecker | CI locale accessible sur http://woodpecker:8000 |
| Dépôt applicatif | Dépôt formation/demo-devsecops créé |
| Pipeline | .woodpecker.yaml exécuté automatiquement |
| Secrets | Trivy détecte les secrets en clair |
| SAST | Semgrep détecte les patterns dangereux |
| SCA | Trivy détecte les dépendances vulnérables |
| SBOM | Trivy génère un fichier CycloneDX |
| Gate CI | La pipeline vulnérable est bloquée |
| Correction | La pipeline corrigée passe |
| Nettoyage | Conteneurs et volumes supprimés |
Aucune ressource cloud, aucune clé AWS et aucun compte externe ne sont utilisés pendant ce TP.