From 1bec2c1376d4b22fcf666321e0e43c3b0e492fe1 Mon Sep 17 00:00:00 2001 From: Dorian Date: Mon, 21 Sep 2026 16:49:44 +0200 Subject: [PATCH] fix(ci): diagnostique les 400 du scan DAST avec socat et echoue si toutes les reponses sont des 4xx --- .github/workflows/dast.yml | 36 ++++++++++++++++++++++++++++++++++-- docs/architecture/50-cicd.md | 8 ++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dast.yml b/.github/workflows/dast.yml index bfef702..b7c4169 100644 --- a/.github/workflows/dast.yml +++ b/.github/workflows/dast.yml @@ -94,10 +94,22 @@ jobs: run: uv run --frozen --no-sync --no-build alembic upgrade head working-directory: apps/backend + # Diagnostic (à retirer une fois le scan authentifié qui fonctionne) : au deuxième passage + # ZAP importait bien le contrat (81 endpoints) mais TOUTES ses requêtes recevaient un 400 + # « Invalid HTTP request received » d'uvicorn, y compris `/health/live` sans authentification. + # uvicorn (httptools) ne dit pas ce qui l'a fait refuser. `socat -v` s'intercale sur le port + # 8000 et journalise les octets échangés, sans rien modifier. + - name: Installe socat + run: sudo apt-get update -qq && sudo apt-get install -y -qq socat + + # L'API écoute sur 8001, `socat` relaie 8000 vers elle. `--http h11` : autre analyseur que + # httptools (llhttp, strict), au cas où c'est lui qui refuse les requêtes de ZAP. - name: Démarre l'API run: | - nohup uv run --frozen --no-sync --no-build uvicorn app.main:create_app --factory --host 0.0.0.0 --port 8000 \ - > "$RUNNER_TEMP/api.log" 2>&1 & + nohup uv run --frozen --no-sync --no-build uvicorn app.main:create_app --factory \ + --http h11 --host 127.0.0.1 --port 8001 > "$RUNNER_TEMP/api.log" 2>&1 & + nohup socat -v -d TCP-LISTEN:8000,fork,reuseaddr TCP:127.0.0.1:8001 \ + > /dev/null 2> "$RUNNER_TEMP/socat.log" & for _ in $(seq 1 30); do curl -fsS http://localhost:8000/api/v1/health/ready >/dev/null 2>&1 && exit 0 sleep 2 @@ -169,6 +181,12 @@ jobs: mkdir -p zap-logs cp "$RUNNER_TEMP/zap-stdout.log" zap-logs/zap-stdout.log || true docker cp zap:/home/zap/.ZAP/zap.log zap-logs/zap.log || true + cp "$RUNNER_TEMP/api.log" zap-logs/api.log || true + # Octets échangés entre ZAP et l'API : début du dump, et contexte du premier 400. Le + # jeton (valable une heure, base jetable) est masqué avant publication. + masque() { sed -E 's/(Authorization: Bearer )[A-Za-z0-9._-]+/\1[MASQUE]/Ig'; } + head -c 200000 "$RUNNER_TEMP/socat.log" | masque > zap-logs/socat-debut.log || true + grep -n -m3 -B14 -A3 "400 Bad Request" "$RUNNER_TEMP/socat.log" | masque > zap-logs/socat-premier-400.log || true docker rm -f zap >/dev/null 2>&1 || true # `continue-on-error` sur le scan ne doit pas faire passer pour vert un scan qui n'a rien @@ -185,6 +203,20 @@ jobs: exit 1 fi + # Deuxième garde-fou : le contrat peut être importé et ZAP n'obtenir que des 4xx (constaté au + # deuxième passage : 81 endpoints, 100 % de 400, job vert). Un scan dont toutes les réponses + # sont des erreurs client n'a rien testé de l'API. + - name: Vérifie que l'API a répondu autre chose que des erreurs client + run: | + if [ ! -f zap-out/zap-report.md ]; then + echo "::error::Aucun rapport ZAP produit : le scan n'a rien testé." + exit 1 + fi + if grep -q "status code 4xx | 100 %" zap-out/zap-report.md; then + echo "::error::100 % des réponses sont des erreurs client (4xx) : le scan n'a rien testé de l'API. Voir zap-logs/socat-premier-400.log dans l'artefact zap-report." + exit 1 + fi + - name: Publie le résumé if: always() run: | diff --git a/docs/architecture/50-cicd.md b/docs/architecture/50-cicd.md index 7b55240..c9ac95c 100644 --- a/docs/architecture/50-cicd.md +++ b/docs/architecture/50-cicd.md @@ -228,6 +228,14 @@ donc le contrat **depuis un fichier** (`-t /zap/wrk/openapi.json -O http://local renomme dans cette copie, sans toucher au contrat versionné, les deux schémas de sécurité aux noms accentués (`Jeton d'accès`, `Cookie de rafraîchissement`) que l'analyseur de ZAP peut refuser. La cause exacte du 400 n'est pas confirmée : si l'import échoue encore, `zap-logs/zap.log` la donne. +Deuxième diagnostic (contrat importé, 81 endpoints) : **toutes** les requêtes de ZAP recevaient un 400 +`Invalid HTTP request received` d'uvicorn, y compris `/api/v1/health/live` sans authentification, +et le job restait vert. Un second garde-fou fait donc échouer le job si 100 % des réponses sont des +4xx. Tant que la cause n'est pas établie, le job intercale `socat -v` entre ZAP et l'API (octets +échangés publiés dans `zap-logs/`, jeton masqué) et lance uvicorn avec `--http h11` : uvicorn n'indique +pas ce que son analyseur a refusé. Ce diagnostic est à retirer une fois le scan authentifié qui +fonctionne. + Piège de permissions : le dossier `zap-out` appartient à l'uid 1000 du conteneur, le runner n'y écrit plus après le `chown` ; les journaux vont donc dans `zap-logs/`, que le runner possède.