DAST authentifié avec le ZAP Automation Framework et IAST¶
Objectifs pédagogiques¶
À la fin de ce TP, vous devez être capables de :
- décrire l'API par une définition OpenAPI exploitable par un scanner
- scanner l'environnement de staging, authentifié, avec un plan ZAP versionné
- faire échouer la pipeline sur un seuil de risque et alimenter DefectDojo
- mettre en œuvre l'IAST, ou à défaut corréler les alertes DAST avec les traces
- organiser les scans pour un retour rapide sur les MR et une couverture complète la nuit
- écrire les tests de non-régression des failles corrigées
Prérequis¶
- L'API SecurePay déployée en staging et exposée par l'ingress (
api-staging.securepay.localdans ce TP) - Un runner GitLab self-hosted capable de joindre staging
- DefectDojo (voir le TP vulnérabilités) et, pour l'option de corrélation, une stack de traces (Tempo ou Jaeger)
Testé
Le plan de l'atelier 3 a été joué avec l'image ghcr.io/zaproxy/zaproxy:stable contre une API de test : import OpenAPI, toutes les requêtes portant le jeton injecté par variable d'environnement, rapports HTML et XML, code de sortie 1 en présence d'une alerte haute. Le marquage des requêtes par le job replacer et la substitution ${VAR} dans le plan ont aussi été vérifiés.
Contexte¶
Le DAST de SecurePay tourne aujourd'hui sur un conteneur lancé en service CI, sans authentification : il ne voit que la page de connexion et ignore l'ingress, TLS et les en-têtes ajoutés par le proxy. Vous passez à un scan authentifié de l'environnement déployé, et vous cherchez à localiser les failles jusqu'à la ligne de code.
Atelier 1 : Une définition OpenAPI¶
Travail demandé¶
-
Rédigez
openapi.yaml(OpenAPI 3) couvrant les routes de l'API :/health,/api/info,/api/auth/login,/api/payments/process,/api/payments/calculate-tax,/api/payments/recent. Déclarez le schéma de sécurité bearer et les corps de requête. -
Servez la définition par l'API en staging (par exemple sur
/openapi.json), ou rangez-la dans le dépôt et passez-la au scanner. -
Validez-la avec un linter OpenAPI dans la CI.
Critère de réussite¶
Chaque route exposée figure dans la définition, avec ses paramètres : ce qui n'y est pas ne sera pas scanné.
Atelier 2 : Un utilisateur dédié au scan¶
Travail demandé¶
-
Créez un utilisateur de test non administrateur pour le scan. Dans SecurePay, le nom
adminreçoit le rôle ADMIN : ne l'utilisez pas. -
Rangez ses identifiants dans OpenBao (ou des variables Masked et Protected).
-
Vérifiez la durée de vie du jeton renvoyé par
/api/auth/login(1 h dans SecurePay) : elle doit couvrir la durée maximale du scan.
Critère de réussite¶
Le scan s'authentifie avec une identité aux droits minimaux, sans secret en clair dans le dépôt.
Atelier 3 : Le plan ZAP¶
Travail demandé¶
- Rédigez
security/zap/zap-plan.yaml. Les variables${TARGET}et${REPORT_DIR}sont substituées par ZAP à l'exécution : un même plan sert pour tous les environnements.
env:
contexts:
- name: securepay
urls: ["${TARGET}"]
includePaths: ["${TARGET}.*"]
parameters:
failOnError: true
failOnWarning: false
progressToStdout: true
jobs:
- type: openapi
parameters:
apiUrl: ${TARGET}/openapi.json
context: securepay
- type: passiveScan-wait
parameters:
maxDuration: 5
- type: activeScan
parameters:
context: securepay
maxRuleDurationInMins: 2
maxScanDurationInMins: 20
- type: report
parameters:
template: traditional-html
reportDir: ${REPORT_DIR}
reportFile: zap-staging
- type: report
parameters:
template: traditional-xml
reportDir: ${REPORT_DIR}
reportFile: zap-staging
- type: exitStatus
parameters:
errorLevel: High
warnLevel: Medium
- Ajoutez le job DAST, déclenché après la synchronisation de staging :
dast:staging:
stage: dast
tags: [self-hosted]
image:
name: ghcr.io/zaproxy/zaproxy:stable
entrypoint: [""]
variables:
TARGET: https://api-staging.securepay.local
ZAP_AUTH_HEADER_SITE: api-staging.securepay.local
script:
- export REPORT_DIR="$CI_PROJECT_DIR"
- |
TOKEN=$(curl -sf -X POST "$TARGET/api/auth/login" -H 'Content-Type: application/json' \
-d "{\"username\":\"$SCAN_USER\",\"password\":\"$SCAN_PASSWORD\"}" | jq -r .token)
- export ZAP_AUTH_HEADER_VALUE="Bearer $TOKEN"
- zap.sh -cmd -autorun "$CI_PROJECT_DIR/security/zap/zap-plan.yaml"
artifacts:
when: always
paths: [zap-staging.html, zap-staging.xml]
Pas de /zap/wrk en CI
Le dossier /zap/wrk n'existe dans l'image ZAP que si vous y montez un volume, et l'utilisateur zap ne peut pas le créer. En CI, écrivez les rapports dans le dossier du job, d'où la variable REPORT_DIR.
- Importez
zap-staging.xmldans DefectDojo (scan_type="ZAP Scan") avec le job de l'atelier 3 du TP vulnérabilités.
Ce que vous devez observer¶
- Les requêtes du scan portent toutes l'en-tête
Authorization: vérifiez-le dans les journaux de l'ingress ou de l'API - Une alerte haute fait échouer le job ; une alerte moyenne ne produit qu'un avertissement
- Les rapports sont disponibles en artefacts et dans DefectDojo
Critère de réussite¶
Un scan authentifié de staging tourne à chaque synchronisation, avec un résultat bloquant au-delà du seuil défini.
Atelier 4 : Exploiter les résultats¶
Travail demandé¶
-
Triez les alertes dans DefectDojo : vraies failles à corriger, faux positifs justifiés et validés par un coéquipier.
-
Comparez avec les résultats du scan non authentifié : quelles routes et quelles failles n'étaient pas vues ?
-
Corrigez au moins une faille révélée (par exemple les en-têtes de sécurité) et vérifiez sa disparition au scan suivant.
Critère de réussite¶
Vous savez chiffrer ce que l'authentification et le scan du déployé apportent par rapport au DAST précédent.
Atelier 5 : IAST, ou corrélation avec les traces¶
Choisissez l'option A si une offre IAST est disponible (essai), sinon l'option B.
Option A : agent IAST en staging¶
- Avec une offre compatible Node.js (par exemple Datadog Code Security), activez l'agent sur le seul environnement de staging : traceur
dd-tracechargé au démarrage de l'API, variableDD_IAST_ENABLED=true, Agent Datadog déployé dans le cluster, clé d'API rangée dans OpenBao. - Lancez le DAST et les tests fonctionnels pendant que l'agent observe.
- Comparez les constats IAST et DAST : lesquels l'IAST localise-t-il jusqu'à la ligne de code ?
- Documentez dans un ADR où partent les données d'exécution, et l'avis de la PSSI.
Option B : corréler DAST et traces¶
- Ajoutez au plan ZAP un marqueur sur chaque requête :
- type: replacer
rules:
- description: Marqueur de scan
matchType: req_header
matchString: X-Scan-Run
replacementString: ${CI_PIPELINE_ID}
- Dans l'API, copiez cet en-tête dans les spans OpenTelemetry :
const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
new HttpInstrumentation({
headersToSpanAttributes: { server: { requestHeaders: ['x-scan-run'] } },
});
- Pour trois alertes ZAP, retrouvez la trace correspondante (attribut de l'en-tête, URL, horodatage), l'éventuelle exception et la pile d'appels, puis le fichier et la ligne à corriger.
Critère de réussite¶
Au moins trois alertes DAST sont reliées à leur emplacement dans le code, par l'IAST ou par les traces.
Atelier 6 : Optimiser l'exécution des scans¶
Travail demandé¶
-
Relevez la durée actuelle de la pipeline d'une MR.
-
Passez les scans de secrets et de SAST en mode différentiel sur les MR :
sast:semgrep:
variables:
GIT_DEPTH: "0"
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
script:
- semgrep scan --config auto --baseline-commit "$CI_MERGE_REQUEST_DIFF_BASE_SHA"
--json --output semgrep.json
secrets:gitleaks:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
script:
- gitleaks git --log-opts="$CI_MERGE_REQUEST_DIFF_BASE_SHA..HEAD"
--report-format json --report-path gitleaks.json .
-
Créez une pipeline planifiée nocturne qui joue les scans complets : SAST et secrets sur tout l'historique, DAST actif complet, scan des images en production.
-
Parallélisez les jobs indépendants et mettez en cache la base de vulnérabilités de Trivy.
-
Relevez la nouvelle durée de pipeline sur une MR, et vérifiez qu'une faille volontairement introduite dans la MR est toujours bloquée.
Critère de réussite¶
Le temps de retour sur une MR diminue, sans perte de couverture : ce qui n'est plus scanné sur la MR l'est la nuit.
Atelier 7 : Tests de non-régression¶
Travail demandé¶
- Écrivez
tests/security/regression.test.jspour les failles corrigées de SecurePay :
const request = require('supertest');
const app = require('../../src/app');
describe('Non-régression sécurité', () => {
test("la formule de taxe n'exécute pas de code", async () => {
const res = await request(app).post('/api/payments/calculate-tax')
.send({ amount: 100, formula: 'process.exit(1)' });
expect(res.status).toBe(400);
});
test('les en-têtes de sécurité sont présents', async () => {
const res = await request(app).get('/health');
expect(res.headers['x-powered-by']).toBeUndefined();
expect(res.headers['content-security-policy']).toBeDefined();
});
});
-
Vérifiez que ces tests échouent sur une version qui réintroduit la faille. Avec
eval(), la formuleprocess.exit(1)interrompt même le processus de test : la régression ne peut pas passer inaperçue. -
Ajoutez un test à chaque faille corrigée par la suite.
Critère de réussite¶
Chaque faille corrigée est protégée par un test exécuté à chaque pipeline.
Questions de synthèse¶
- Qu'est-ce qu'un scan de l'environnement déployé révèle qu'un scan en service CI ne voit pas ?
- Pourquoi l'utilisateur de scan ne doit-il pas être administrateur ?
- Quelles sont les forces et les limites de l'IAST, et pourquoi sa couverture dépend-elle des tests ?
- En quoi la corrélation DAST et traces diffère-t-elle d'un véritable IAST ?
- Comment raccourcir le retour sur les MR sans réduire la couverture de sécurité ?