fix(ci): diagnostique les 400 du scan DAST avec socat et echoue si toutes les reponses sont des 4xx

This commit is contained in:
Dorian
2026-09-21 16:49:44 +02:00
parent 00fab49d80
commit 1bec2c1376
2 changed files with 42 additions and 2 deletions
+34 -2
View File
@@ -94,10 +94,22 @@ jobs:
run: uv run --frozen --no-sync --no-build alembic upgrade head run: uv run --frozen --no-sync --no-build alembic upgrade head
working-directory: apps/backend 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 - name: Démarre l'API
run: | run: |
nohup uv run --frozen --no-sync --no-build uvicorn app.main:create_app --factory --host 0.0.0.0 --port 8000 \ nohup uv run --frozen --no-sync --no-build uvicorn app.main:create_app --factory \
> "$RUNNER_TEMP/api.log" 2>&1 & --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 for _ in $(seq 1 30); do
curl -fsS http://localhost:8000/api/v1/health/ready >/dev/null 2>&1 && exit 0 curl -fsS http://localhost:8000/api/v1/health/ready >/dev/null 2>&1 && exit 0
sleep 2 sleep 2
@@ -169,6 +181,12 @@ jobs:
mkdir -p zap-logs mkdir -p zap-logs
cp "$RUNNER_TEMP/zap-stdout.log" zap-logs/zap-stdout.log || true 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 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 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 # `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 exit 1
fi 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é - name: Publie le résumé
if: always() if: always()
run: | run: |
+8
View File
@@ -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 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 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. 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 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. plus après le `chown` ; les journaux vont donc dans `zap-logs/`, que le runner possède.