Aller au contenu

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