fix(ci): corrige la cle matchstr de l'en-tete Authorization dans le scan DAST et retire le diagnostic socat

This commit is contained in:
Dorian
2026-09-21 16:59:13 +02:00
parent 1bec2c1376
commit 6d741e45fb
2 changed files with 15 additions and 26 deletions
+8 -20
View File
@@ -94,22 +94,10 @@ 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 \ 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 & --host 0.0.0.0 --port 8000 > "$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
@@ -131,6 +119,11 @@ jobs:
# appartient à l'utilisateur du conteneur (uid 1000), sans droits d'écriture pour les autres # appartient à l'utilisateur du conteneur (uid 1000), sans droits d'écriture pour les autres
# (règle Sonar S2612 : pas de `chmod 777`). # (règle Sonar S2612 : pas de `chmod 777`).
# #
# Piège : la clé du nom d'en-tête est `matchstr`, pas `matchstring`. ZAP accepte n'importe quelle
# clé `-config` sans erreur ; avec la mauvaise, il ajoutait à TOUTES les requêtes un en-tête au
# nom vide (`: Bearer <jeton>`), que uvicorn refuse par un 400 (« Invalid HTTP request
# received »), y compris sur les routes publiques.
#
# Les routes d'authentification qui changent l'état du compte du scan sont exclues : un # Les routes d'authentification qui changent l'état du compte du scan sont exclues : un
# scan actif y déclencherait la limitation de débit du login, la réinitialisation de mots de # scan actif y déclencherait la limitation de débit du login, la réinitialisation de mots de
# passe et la fermeture des sessions, sans rien apprendre de plus. # passe et la fermeture des sessions, sans rien apprendre de plus.
@@ -167,7 +160,7 @@ jobs:
-z "-config replacer.full_list(0).description=auth \ -z "-config replacer.full_list(0).description=auth \
-config replacer.full_list(0).enabled=true \ -config replacer.full_list(0).enabled=true \
-config replacer.full_list(0).matchtype=REQ_HEADER \ -config replacer.full_list(0).matchtype=REQ_HEADER \
-config replacer.full_list(0).matchstring=Authorization \ -config replacer.full_list(0).matchstr=Authorization \
-config replacer.full_list(0).regex=false \ -config replacer.full_list(0).regex=false \
-config replacer.full_list(0).replacement='Bearer ${{ steps.jeton.outputs.jeton }}' \ -config replacer.full_list(0).replacement='Bearer ${{ steps.jeton.outputs.jeton }}' \
-config globalexcludeurl.url_list.url(0).description=auth-etat \ -config globalexcludeurl.url_list.url(0).description=auth-etat \
@@ -182,11 +175,6 @@ jobs:
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 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
@@ -213,7 +201,7 @@ jobs:
exit 1 exit 1
fi fi
if grep -q "status code 4xx | 100 %" zap-out/zap-report.md; then 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." echo "::error::100 % des réponses sont des erreurs client (4xx) : le scan n'a rien testé de l'API. Voir zap-logs/api.log et zap-logs/zap.log dans l'artefact zap-report."
exit 1 exit 1
fi fi
+7 -6
View File
@@ -229,12 +229,13 @@ renomme dans cette copie, sans toucher au contrat versionné, les deux schémas
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 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, `Invalid HTTP request received` d'uvicorn, y compris `/api/v1/health/live` sans authentification, et
et le job restait vert. Un second garde-fou fait donc échouer le job si 100 % des réponses sont des le job restait vert. Un dump des octets échangés (`socat -v`, retiré depuis) a montré la cause : ZAP
4xx. Tant que la cause n'est pas établie, le job intercale `socat -v` entre ZAP et l'API (octets ajoutait à chaque requête une ligne d'en-tête au **nom vide**, `: Bearer <jeton>`. La clé de
échangés publiés dans `zap-logs/`, jeton masqué) et lance uvicorn avec `--http h11` : uvicorn n'indique configuration du nom d'en-tête pour la règle Replacer est **`matchstr`** ; le job écrivait
pas ce que son analyseur a refusé. Ce diagnostic est à retirer une fois le scan authentifié qui `matchstring` (nom utilisé par le job d'automatisation ZAP, pas par `-config`). ZAP accepte
fonctionne. n'importe quelle clé `-config` sans erreur, il a donc laissé le nom vide. Le job porte deux garde-fous
qui font échouer un scan qui n'a rien testé : moins de 10 URL importées, ou 100 % de réponses 4xx.
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.