IAST & DAST authentifié en environnement distribué
Objectifs pédagogiques
- Situer SAST, DAST et IAST et savoir ce que chacun voit et ne voit pas
- Scanner un environnement déployé, authentifié, avec un plan ZAP versionné
- Faire échouer la pipeline sur un seuil de risque, sans bloquer pour du bruit
- Comprendre l'IAST, ses prérequis et ses limites, et disposer d'une approche de repli
- Optimiser les ressources : scans différentiels sur les MR, scans complets planifiés
- Transformer chaque faille corrigée en test de non-régression
SAST, DAST, IAST
Trois angles complémentaires
| SAST | DAST | IAST |
| Analyse | code source, au repos | application en marche, de l'extérieur | application en marche, de l'intérieur |
| Moment | dès le commit | après déploiement | pendant les tests |
| Voit | tout le code, même mort | ce qui est exposé et configuré | les flux réellement exécutés |
| Faux positifs | nombreux | modérés | très rares |
| Localise | la ligne de code | l'URL et le paramètre | l'URL et la ligne de code |
| Limite | ignore la configuration réelle | ne voit pas le code | dépend du langage et de la couverture des tests |
Pourquoi scanner l'environnement déployé
- Un conteneur lancé seul en service CI ne montre ni l'ingress, ni TLS, ni les en-têtes ajoutés ou retirés par le proxy, ni les politiques réseau
- L'environnement de staging, proche de la production, révèle ce que verra un attaquant
- Contreparties à maîtriser :
- un scan actif crée des données et peut déclencher des effets de bord ;
- la limitation de débit ou un WAF peuvent fausser le résultat ;
- le scan doit s'authentifier pour couvrir les routes protégées
- Règle : scan actif en staging uniquement, avec un utilisateur de test dédié aux droits minimaux
DAST authentifié avec ZAP
Du script packagé au plan versionné
- Les scripts packagés (
zap-baseline.py, zap-api-scan.py) conviennent pour démarrer - Le ZAP Automation Framework décrit tout le scan dans un plan YAML :
- contextes et périmètre ;
- import de la définition OpenAPI ;
- scans passif et actif, avec leurs limites de durée ;
- rapports et code de sortie
- Le plan est versionné, revu par MR et rejoué à l'identique
- Exécution sans interface :
zap.sh -cmd -autorun plan.yaml
Le plan : contexte et paramètres
env:
contexts:
- name: securepay-staging
urls: ["https://api-staging.securepay.local"]
includePaths: ["https://api-staging.securepay.local.*"]
parameters:
failOnError: true # une erreur du plan fait échouer l'exécution
failOnWarning: false
progressToStdout: true
- Le contexte borne le scan : rien en dehors de
includePaths n'est attaqué - Un plan par environnement cible, jamais la production en scan actif
Le plan : les jobs
jobs:
- type: openapi
parameters:
apiUrl: https://api-staging.securepay.local/openapi.json
context: securepay-staging
- type: passiveScan-wait
parameters: {maxDuration: 5}
- type: activeScan
parameters:
context: securepay-staging
maxRuleDurationInMins: 2
maxScanDurationInMins: 20
- type: report
parameters: {template: traditional-html, reportDir: /zap/wrk, reportFile: zap-staging}
- type: report
parameters: {template: traditional-xml, reportDir: /zap/wrk, reportFile: zap-staging}
- type: exitStatus
parameters: {errorLevel: High, warnLevel: Medium}
L'authentification par JWT
# Jeton d'un utilisateur de test dédié au scan
TOKEN=$(curl -sf -X POST "$TARGET/api/auth/login" -H 'Content-Type: application/json' \
-d "{\"username\":\"$SCAN_USER\",\"password\":\"$SCAN_PASSWORD\"}" | jq -r .token)
docker run --rm -v "$PWD:/zap/wrk/:rw" \
-e ZAP_AUTH_HEADER_VALUE="Bearer $TOKEN" \
-e ZAP_AUTH_HEADER_SITE="api-staging.securepay.local" \
ghcr.io/zaproxy/zaproxy:stable zap.sh -cmd -autorun /zap/wrk/zap-plan.yaml
- ZAP ajoute l'en-tête à chaque requête vers le site indiqué (vérifié : toutes les requêtes d'un scan le portaient)
- La durée de vie du jeton doit couvrir celle du scan
- Pour un parcours de connexion complexe : authentification par script ou par navigateur
Décider du résultat
- Le job
exitStatus fixe le code de sortie selon le niveau de risque le plus élevé trouvé errorLevel: High : une alerte haute fait échouer la pipeline (testé : code de sortie 1) warnLevel: Medium : une alerte moyenne produit un avertissement sans bloquer - Le rapport XML est importé dans DefectDojo (
scan_type="ZAP Scan") - Les faux positifs se qualifient dans DefectDojo, avec justification et valideur, plutôt qu'en masquant des règles dans le plan
IAST
Le principe
- Un agent instrumente l'application dans son runtime
- Il suit les données de leur entrée (paramètre HTTP) jusqu'à leur usage dangereux (requête SQL,
eval, appel système) : c'est le taint tracking - Il signale une vulnérabilité quand une donnée non assainie atteint un point sensible, avec la pile d'appels et la ligne de code
- Il ne voit que ce qui est exécuté : sa couverture dépend des tests qui exercent l'application
- Son surcoût réserve son usage aux environnements de test
L'IAST pour Node.js
- Aucun agent IAST open source n'est disponible aujourd'hui pour Node.js
- Offres commerciales, accessibles en essai :
- Datadog Code Security : variable
DD_IAST_ENABLED=true avec le traceur dd-trace, Agent Datadog récent, résultats dans le Vulnerability Explorer ; - d'autres éditeurs proposent un agent Node.js dans leurs suites
- L'écosystème Java offre plus de choix
- Choisir un outil IAST, c'est aussi choisir où partent les données d'exécution : point à valider avec la PSSI
Coupler IAST et DAST
- Le DAST envoie des requêtes offensives sur toutes les routes de la définition OpenAPI
- L'agent IAST observe, de l'intérieur, ce que ces requêtes déclenchent dans le code
- Les tests fonctionnels complètent la couverture des parcours métier
- Résultat : la largeur du DAST, la précision de l'IAST, et la ligne de code à corriger
- Le tout tourne en staging, déclenché après chaque synchronisation
Sans agent : corréler DAST et traces
# Dans le plan ZAP : marquer chaque requête du scan
- type: replacer
rules:
- description: Marqueur de scan
matchType: req_header
matchString: X-Scan-Run
replacementString: pipeline-42
// Dans l'API : copier l'en-tête dans les spans OpenTelemetry
new HttpInstrumentation({
headersToSpanAttributes: { server: { requestHeaders: ['x-scan-run'] } },
})
- Pour une alerte ZAP (URL, paramètre, heure), on retrouve la trace, l'exception et la pile d'appels
- C'est une aide à la localisation, pas une détection : pas de taint tracking
Optimiser les ressources
Le bon scan au bon moment
| Déclencheur | Scans | Objectif |
| Merge Request | secrets et SAST différentiels, tests | retour en quelques minutes |
| Synchronisation de staging | DAST authentifié, IAST | valider le déployé |
| Pipeline planifiée nocturne | SAST et secrets complets, DAST actif complet, scan d'images | couverture totale |
- Le retour rapide se joue sur la MR ; la couverture complète, la nuit
- Le temps de pipeline se mesure avant et après : c'est un indicateur suivi
Scans différentiels 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 .
- Seuls les constats introduits par la MR remontent ; le stock est traité par le scan nocturne
Autres leviers
- Parallélisation : les scans indépendants tournent dans le même stage
- Cache : la base de vulnérabilités de Trivy et les règles des scanners sont mises en cache entre les jobs
- Limites de durée : chaque scan actif a une durée maximale par règle et au total
- Ciblage : le DAST ne rejoue que les routes de la définition OpenAPI
- Artefacts : les rapports alimentent DefectDojo, pas des téléchargements manuels
Non-régression
Chaque faille corrigée devient un test
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();
});
});
- Le test échoue dès que la faille est réintroduite, quel que soit l'auteur du changement
Anti-patterns à proscrire
- Un DAST non authentifié qui ne voit que la page de connexion
- Un scan actif sur la production
- Un compte de scan avec des droits d'administrateur
- Masquer des règles dans le plan pour faire passer la pipeline
- Lancer tous les scans complets sur chaque MR, puis les désactiver parce qu'ils sont trop lents
- Corriger une faille sans test de non-régression
À retenir
- SAST, DAST et IAST se complètent : code au repos, application exposée, flux réellement exécutés
- Le DAST se joue sur l'environnement déployé, authentifié, avec un plan ZAP versionné et un code de sortie maîtrisé
- L'IAST localise la ligne fautive ; pour Node.js, il passe par une offre commerciale ou par la corrélation DAST et traces
- Scans différentiels sur les MR, complets la nuit : rapidité et couverture
- Une faille corrigée sans test de non-régression reviendra
Pour aller plus loin