---
name: dast-fix
description: >
  Auto-remediate DAST (OWASP ZAP) Medium+ findings from a scheduled scan: triage each finding,
  fix only the ones provably owned by THIS service (security headers, CSP, SRI), open a
  `dast-autofix/*` MR for the CI gate to verify and auto-merge, and escalate the rest as GitLab
  issues. Runs headless in CI via `claude-ci "/dast-fix"` inside the weekly DAST pipeline.
  Use when the user says /dast-fix, 'corrige les findings DAST', 'remédie le scan ZAP',
  'fix les alertes de sécurité', 'autofix DAST', 'traite le rapport ZAP', 'corrige les headers
  de sécurité manquants', 'fix the DAST findings', 'remediate the ZAP scan', 'fix the security
  headers', 'auto-fix the OWASP findings', or for the weekly CI DAST autofix job. This skill remediates an
  existing scan's findings (it consumes the `gl-dast-*.json` artifacts), it does not run the scan.
argument-hint: "(no args — reads gl-dast-*.json artifacts and dast-alert-thread.txt from the CWD)"
allowed-tools:
  - Read
  - Edit
  - Write
  - Glob
  - Grep
  - Bash
  - Skill
---

Tu corriges automatiquement les findings DAST Medium+ d'un scan ZAP planifié. Tu tournes **headless dans la CI** (pipeline hebdo `$DAST_SCHEDULE`), repo déjà checkout, artefacts de scan dans le CWD. Aucune interaction humaine possible : tu décides seul, prudemment, et tu laisses le gate CI + la review humaine sur l'issue rattraper ce que tu n'as pas pu prouver.

**Principe directeur (non négociable) :** mieux vaut escalader en issue que merger un fix no-op. Un finding peut venir du **host partagé** (la baseline scanne la racine `/`, pas ton préfixe de service) : si tu ne peux pas affirmer que CE service contrôle la réponse fautive, tu n'y touches pas.

## Required tools

- `glab`

Run `pysae-ai-tools tools require <tools>` before starting; if it exits non-zero, abort and report the install command from stderr to the user.

## Step 1 : Charger les findings

```bash
ls gl-dast-*.json 2>/dev/null
cat dast-alert-thread.txt 2>/dev/null   # "<channel> <ts>" du message d'alerte Slack, si présent
```

Parse chaque `gl-dast-*.json`, garde les alertes `riskcode >= 2` (Medium+). Pour chacune, retiens : `name`, `riskcode`, `cweid`/`alertRef` si dispo, et les `instances` (URLs/paramètres touchés). Si aucun finding Medium+ → écris `echo "no Medium+ finding"` et termine, rien à faire.

Lis le profil du repo : si la variable CI `DAST_AUTOFIX_BROWSER_APP` vaut `true`, ce service sert une app navigateur (SPA). Sinon, considère-le API-only.

## Step 2 : Dédup — ne pas réouvrir une MR déjà ouverte

```bash
glab mr list --label dast-autofix --state opened 2>/dev/null
```

S'il existe déjà une MR `dast-autofix` ouverte sur ce repo, **arrête-toi** (commente-la éventuellement avec les nouveaux findings) : on ne lance pas deux boucles autofix en parallèle.

## Step 3 : Triage / attribution POSITIVE

Pour chaque finding, classe-le en **FIXABLE** ou **ESCALADE**. Règle d'attribution : tu dois pouvoir pointer l'endroit du code de CE repo qui contrôle la réponse incriminée.

- **Headers de réponse manquants** (CSP, HSTS, X-Content-Type-Options, etc.) : FIXABLE si le repo a un serveur HTTP dont tu peux modifier les headers globaux (ex. app Fastify/Express). Sur app navigateur (`DAST_AUTOFIX_BROWSER_APP=true`), le finding **CSP** est **ESCALADE** (une CSP trop stricte casse les scripts en silence, le gate ne le verra pas) — les autres headers restent FIXABLE.
- **Sub Resource Integrity (SRI) manquant** : FIXABLE seulement si CE repo sert lui-même le HTML contenant les `<script>`/`<link>` externes visés. Un service API-only qui ne sert pas de HTML → **ESCALADE** (le finding vient du host partagé, pas de toi).
- **Tout finding dont tu ne trouves pas l'origine dans ce repo** → **ESCALADE**.

Documente ta décision par finding (1 ligne) ; tu la mettras dans la description de MR / l'issue.

### Exemple de triage

| Finding (ZAP) | Service | Décision | Raison |
|---|---|---|---|
| CSP Header Not Set | gtfsrt-to-siri (API Fastify) | **FIXABLE** | Le service contrôle ses headers → `@fastify/helmet`. `Dast-Review-Assert: Content-Security-Policy`. |
| Sub Resource Integrity Missing | gtfsrt-to-siri (API-only, zéro HTML) | **ESCALADE** | Le repo ne sert aucun `<script>`/`<link>` → finding du host partagé, pas de lui. |
| CSP Header Not Set | op (SPA, `DAST_AUTOFIX_BROWSER_APP=true`) | **ESCALADE** | CSP sur navigateur casse en silence ; le gate header ne le verrait pas → issue. |
| X-Content-Type-Options Missing | op (SPA) | **FIXABLE** | Header non-CSP, sans risque de casse → fix + `Dast-Review-Assert: X-Content-Type-Options`. |

## Step 4 : Appliquer les fixes (findings FIXABLE)

Applique le remède idiomatique du framework détecté. Repère le bootstrap du serveur (`grep -rl "fastify(" src` / `express()` / etc.).

- **CSP + headers de sécurité, service API** : ajoute le middleware de durcissement standard du framework et assure-toi qu'il émet `Content-Security-Policy`. Pour une API qui ne sert ni HTML ni scripts, une politique verrouillée est sûre et correcte :
  - Fastify : `@fastify/helmet` enregistré tôt, avec une CSP type `default-src 'none'; frame-ancestors 'none'; base-uri 'none'`. Ajoute la dépendance au `package.json` si absente.
  - Express : `helmet()` équivalent.
- **SRI** : ajoute `integrity="sha384-…"` + `crossorigin="anonymous"` aux balises `<script>`/`<link>` externes servies par le repo (calcule le hash de la ressource pinned).

Touche **uniquement** ce qui corrige le finding. Pas d'amélioration adjacente (cf. règles).

Lance la qualité du repo (le gate CI re-vérifiera, mais on ne pousse pas du rouge) :

```bash
# adapte aux commandes du repo (package.json scripts, Makefile, etc.)
npm run lint && npm run typecheck && npm test
```

Si tu ne peux pas faire passer les tests/lint pour un finding, repasse-le en ESCALADE plutôt que de pousser du cassé.

## Step 5 : Branche, commits, MR

```bash
BRANCH="dast-autofix/${CI_PIPELINE_ID}"
git checkout -b "$BRANCH" "origin/${CI_DEFAULT_BRANCH}"
# 1 commit par classe de finding
git add <fichiers précis>
git commit -m "$(cat <<'EOF'
fix(security): set Content-Security-Policy header (DAST baseline)

Co-Authored-By: Claude <noreply@anthropic.com>
EOF
)"
git push -u origin "$BRANCH"
```

Ouvre la MR vers la branche par défaut, label `dast-autofix`, **en embarquant ces trailers parsables en fin de description** (le gate CI les relit) :

> Le **titre de la MR** doit rester un en-tête Conventional Commits valide (`fix(security): …` ci-dessous l'est) : la MR est squash-mergée et GitLab réutilise le titre comme message de commit, validé par commitlint — voir [../references/commit-rules.md § MR title](../references/commit-rules.md).

```bash
glab mr create \
  --source-branch "$BRANCH" \
  --target-branch "$CI_DEFAULT_BRANCH" \
  --label dast-autofix \
  --title "fix(security): DAST autofix — <résumé findings>" \
  --yes \
  --description "$(cat <<EOF
## Auto-fix DAST

Findings corrigés dans ce repo (vérifiés au gate par re-scan des headers sur l'env review) :
<liste FIXABLE : nom + fichier touché>

Findings escaladés en issue (non attribuables à ce service / CSP sur SPA) :
<liste ESCALADE + lien issue>

<!-- machine-readable trailers, do not edit -->
Dast-Review-Assert: <Header-1>,<Header-2>
Slack-Channel: <channel ou vide>
Slack-Thread-TS: <ts ou vide>
EOF
)"
```

`Dast-Review-Assert` liste les noms de headers HTTP que le service doit désormais émettre (ex. `Content-Security-Policy`). Le gate `dast_header_check` curl l'env review et exige leur présence. Si aucun finding FIXABLE de type header, laisse `Dast-Review-Assert:` vide (le gate passera mais l'auto-merge restera prudent — voir les jobs CI).

## Step 6 : Escalader le reste en issues

Pour chaque finding ESCALADE, **d'abord vérifier qu'une issue ne le couvre pas déjà** — sinon le scan hebdo recrée la même issue chaque semaine (spam) :

```bash
glab issue list --label DAST --state opened --search "<nom du finding>"
```

Si une issue correspond, **commente-la** au lieu d'en recréer une :

```bash
glab issue note <iid> --message "Toujours présent au scan du ${CI_PIPELINE_URL}."
```

Sinon, crée l'issue structurée :

```bash
pysae-ai-tools glab retry issue create \
  --title "DAST: <nom du finding> — attribution à confirmer" \
  --label "DAST" \
  --no-editor \
  --yes \
  --description "$(cat <<'EOF'
Finding DAST Medium+ non auto-corrigé.

- **Finding** : <nom, riskcode, CWE>
- **Instances** : <URLs/params ZAP>
- **Pourquoi pas auto-fixé** : <host partagé / API-only sans HTML / CSP sur SPA>
- **Repo probablement concerné** : <hypothèse>
- **Remède suggéré** : <piste>

Pipeline source : <CI_PIPELINE_URL>
EOF
)"
```

Mets le lien de l'issue dans la description de MR (section escaladés). S'il n'y a aucun finding FIXABLE, ne crée pas de MR : crée seulement les issues.

## Gotchas

- **Host partagé = attribution piégée.** La baseline ZAP scanne la racine du host partagé `/`, pas le préfixe de ton service. Un finding CSP/SRI vient souvent du frontend ou de l'API principale, pas de toi. Sans preuve d'appartenance → ESCALADE, jamais de fix no-op.
- **CSP sur SPA casse en silence.** Une CSP trop stricte charge la page mais bloque ses scripts ; le gate « header présent » ne le voit pas. `DAST_AUTOFIX_BROWSER_APP=true` → CSP en issue, point.
- **Le gate ne prouve que les headers de `Dast-Review-Assert`.** Un fix dont tu ne déclares pas le header attendu n'est pas vérifié → verdict MANUAL, pas d'auto-merge. Déclare chaque header que tu ajoutes.
- **Ne pousse jamais du rouge.** La pipeline de la MR déploie en review puis curl les headers ; un build/lint cassé bloque tout le gate. Tests KO sur un finding → repasse-le en ESCALADE.
- **Dédup MR ET issues.** Sans le check du Step 6, le scan hebdo recrée chaque semaine la même issue pour un finding host-partagé jamais corrigé ici → spam de `#tech-cve` / du backlog.

## Règles critiques

- **Attribution positive ou escalade.** Jamais de fix spéculatif. Le doute → issue.
- **Surgical.** Chaque ligne modifiée trace vers un finding. Pas de refacto adjacent, pas de reformatage.
- **CSP sur SPA → issue**, jamais auto-mergé en v1 (`DAST_AUTOFIX_BROWSER_APP=true`).
- **Ne pousse jamais du rouge.** Tests/lint cassés → repasse le finding en escalade.
- **Une seule boucle.** MR `dast-autofix` déjà ouverte → stop.
- **Headless.** Aucune question ; les zones grises partent en issue pour œil humain.

## Self-check avant de terminer

- [ ] Chaque finding Medium+ est soit FIXABLE (corrigé + dans `Dast-Review-Assert` si header), soit ESCALADE (issue créée).
- [ ] La MR porte le label `dast-autofix` et les trailers `Dast-Review-Assert` / `Slack-Channel` / `Slack-Thread-TS`.
- [ ] Tests + lint verts sur la branche.
- [ ] Aucune MR `dast-autofix` doublon, et aucune issue DAST doublon (recherche faite avant chaque création).
