hors suivi
Leçon

Trois familles d'analyse, trois angles morts

Jour 1 · SAST — lire le code sans l'exécuter · 25 min

Vous avez passé trois jours à fabriquer une chaîne de livraison qui refuse un secret en clair et une requête SQL concaténée. Deux outils, deux portes. La question de ce module est celle qui vient juste après, et c'est celle qu'on pose en entretien d'architecte sécurité : que reste-t-il que ces portes ne voient pas ?

La réponse tient en trois familles d'analyse. Elles ne se remplacent pas, elles ne se classent pas, et chacune est aveugle exactement là où les deux autres regardent.

SAST analyse statique Lit : votre code source N’exécute rien Donne : fichier + ligne Ignore : ce qui est vrai SCA analyse de composition Lit : vos dépendances N’exécute rien Donne : CVE + version Ignore : votre code DAST analyse dynamique Lit : les réponses HTTP Exécute l’application Donne : une preuve Ignore : où c’est écrit Angle mort commun aux trois la logique d’autorisation — VULN-04 (IDOR) n’est trouvée par aucun des trois outils Aucune famille ne recouvre les deux autres. Un programme d’analyse qui n’en a qu’une n’a pas 33 % de la couverture : il a 100 % d’un tiers, et zéro du reste.
Les trois familles ne se remplacent pas. Elles ne se recouvrent qu’à la marge — et laissent un angle mort qu’aucune ne comble.

Ce que chacune fait, en une phrase

FamilleNom completEntréeQuestion qu'elle pose
SASTStatic Application Security Testingle code source, sans l'exécuter« ce que ce code peut faire est-il dangereux ? »
SCASoftware Composition Analysisla liste des dépendances et leurs versions« quelqu'un a-t-il déjà publié une faille dans ce que j'importe ? »
DASTDynamic Application Security Testingl'application en train de tourner, par HTTP« cette attaque précise fonctionne-t-elle, oui ou non ? »

Le mot qui change tout est preuve. Le SAST vous donne un emplacementapp.py, ligne 126 — et une suspicion. Le DAST vous donne une preuve — la charge envoyée, la réponse reçue — et aucun emplacement. Un rapport professionnel a besoin des deux : sans l'emplacement, le développeur ne sait pas quoi corriger ; sans la preuve, il n'est pas convaincu qu'il y a quelque chose à corriger.

La prédiction que vous allez vérifier

Voici les dix vulnérabilités de devsecops-lab, et ce que chaque famille devrait en dire. Ce tableau est une hypothèse, pas une vérité. Vous allez le confronter à la réalité pendant trois jours, et vous en corrigerez au moins une case.

FailleSASTSCADAST
VULN-01Injection SQL sur /loginoui — concaténation vers un curseurnonoui — la réponse change
VULN-02XSS réfléchi sur /searchoui — gabarit construit dynamiquementnonoui — la charge revient dans la page
VULN-03Injection de commande sur /pingouishell=True sur une entréenonoui, mais bruyant
VULN-04IDOR sur /profilenonnonnon — il faut deux comptes pour le voir
VULN-05Secret en duroui — c'est Gitleaksnonnon — invisible de l'extérieur
VULN-06Hachage md5 d'un mot de passeouinonnon
VULN-07yaml.load non sûrouipartiellement — via une CVE de PyYAMLdifficilement
VULN-08SSRF sur /fetchoui — par motifnonseulement avec un serveur d'écoute externe
VULN-09debug=True, en-têtes absentsoui — Banditnonoui — bannière et en-têtes
VULN-10Dépendances à CVEnonoui — c'est sa seule raison d'êtrenon
La ligne à regarder est la quatrième

VULN-04, l'IDOR, n'est trouvée par aucun des trois outils. Rien dans le code ne distingue /profile/1 de /profile/2 : ce sont deux entiers. Aucun scanner ne sait que le premier vous appartient et pas le second — c'est une règle métier, elle n'est écrite nulle part.

Retenez la conséquence, elle vaut pour toute votre carrière : l'outillage ne supprime pas la revue humaine, il la libère pour ce que lui seul peut faire. Vous trouverez cette faille au M5, à la main, en deux minutes.

Les deux mots qu'il faut employer correctement dès aujourd'hui

  • Faux positif — l'outil signale, le problème n'existe pas. Coût : votre temps, et à la longue votre confiance. Un outil à 40 % de faux positifs finit désactivé, c'est empiriquement ce qui arrive.
  • Faux négatif — l'outil se tait, le problème existe. Coût : un incident. C'est le plus grave et le moins visible : personne ne se plaint jamais d'un faux négatif.

Toute la configuration d'un outil d'analyse est un arbitrage entre les deux. Serrer les règles fait monter les faux positifs ; les desserrer fait monter les faux négatifs. Il n'existe pas de réglage qui annule les deux — et c'est pour cela qu'on cumule trois familles plutôt que d'en perfectionner une.

Où en est votre application ce matin

Au module précédent, vous avez corrigé VULN-01 (injection SQL) et VULN-05 (secret en dur) pour faire passer votre pipeline au vert. Les huit autres sont encore là, et app.py porte toujours ses commentaires VULN-NN.

Ces commentaires sont votre corrigé. La règle du module : vous ne les regardez qu'après avoir lu la sortie de l'outil. L'exercice n'est pas de retrouver ce que vous savez déjà, c'est de mesurer l'écart entre ce que l'outil dit et ce que vous savez.

✓ terminé
Leçon

Ce qu'un moteur SAST voit — et ce qu'il ne verra jamais

Jour 1 · SAST — lire le code sans l'exécuter · 25 min

Un moteur SAST ne lit pas votre code comme vous. Il en fait un arbre syntaxique — la structure grammaticale du programme — puis il cherche des formes dans cet arbre. Il n'exécute rien, ne connaît aucune valeur, et ne sait pas ce que votre application fait.

Premier mécanisme : le motif à métavariables

Une règle Semgrep décrit un bout de code avec des trous. Les trous sont des métavariables, écrites $QUELQUECHOSE, qui capturent n'importe quel sous-arbre. L'ellipse ... capture n'importe quelle suite d'éléments.

Motif de la règleCe qu'il attrapeCe qu'il n'attrape pas
yaml.load($D)yaml.load(request.data), yaml.load(f.read())yaml.safe_load(request.data)
subprocess.run(..., shell=True, ...)l'appel quel que soit le nombre d'arguments avant et aprèssubprocess.run(["ping", h])
hashlib.md5(...)tout usage de md5, y compris légitimehashlib.sha256(...)

Ce mécanisme est robuste à la mise en forme — retours à la ligne, espaces, ordre des arguments nommés — là où une expression régulière casse au premier saut de ligne. C'est précisément ce qui distingue un moteur SAST d'un grep bien écrit.

Mais il a une limite dure : il décrit un point du programme. La troisième ligne du tableau le montre — hashlib.md5 sur un mot de passe est une faille, hashlib.md5 sur le chemin d'un fichier de cache n'en est pas une, et le motif ne peut pas les distinguer. Vous rencontrerez ce cas exact ce matin.

Second mécanisme : le suivi de teinte

Pour distinguer les deux cas précédents, il faut suivre d'où vient la donnée. C'est le suivi de teinte : on déclare des sources (les entrées non fiables), des puits (les endroits dangereux) et des assainisseurs (ce qui rend la donnée sûre). Le moteur signale un chemin qui va d'une source à un puits sans passer par un assainisseur.

source request.args.get() propagation "ping -c1 " + hote assainisseur shlex.quote() puits check_output(shell=True) Chemin rouge : la teinte atteint le puits sans être lavée → le moteur signale. Chemin vert : l’assainisseur retire la teinte → le moteur se tait.
Le suivi de teinte (taint tracking) : c’est la seule façon pour un moteur statique de distinguer une entrée dangereuse d’une constante inoffensive.

Sur VULN-03, cela donne exactement trois éléments :

  • sourcerequest.args.get("host", "127.0.0.1") : la valeur vient du client, elle est teintée ;
  • puitssubprocess.check_output(..., shell=True) : le premier argument est interprété par un interpréteur de commandes ;
  • assainisseurshlex.quote(), ou une validation stricte du format, retirerait la teinte.

Une règle de teinte ne signale donc pas shell=True en soi. C'est ce qui la rend beaucoup plus précise qu'un motif — et beaucoup plus longue à écrire correctement.

Ce que le moteur ne verra jamais

LimitePourquoiConséquence concrète
La logique métier« cet utilisateur a-t-il le droit de lire cette fiche ? » n'est écrit nulle partVULN-04 ne sortira jamais, quel que soit le jeu de règles
Le code appelé dynamiquementgetattr(module, nom)() : le nom n'est connu qu'à l'exécutionun chemin d'attaque réel reste invisible
L'inter-fichiersSemgrep OSS analyse fichier par fichier ; l'inter-procédural inter-fichiers est payantune teinte qui traverse un import se perd
La configuration effectivela valeur réelle d'une variable d'environnement n'existe qu'au déploiementun DEBUG lu depuis l'environnement ne se juge pas statiquement
Les dépendancesil analyse votre code, pas les 40 Mo de site-packagesVULN-10 est hors de portée — c'est le sujet de demain
Les jeux de règles que vous allez employer

p/python — les règles générales du langage. p/flask — les pièges propres au cadriciel. p/security-audit — un jeu large et volontairement bruyant, conçu pour un audit ponctuel et pas pour un pipeline. Vous allez mesurer vous-même ce que « bruyant » veut dire, puis décider ce qui bloque.

Ils se chargent depuis le registre public au premier appel, puis sont mis en cache. Aucun compte, aucune connexion. Vos règles maison, écrites cet après-midi, vivront dans .semgrep/ et fonctionneront hors ligne.

SARIF : pourquoi on ne lit jamais la sortie texte

SARIF (Static Analysis Results Interchange Format) est un JSON normalisé OASIS : une règle, un emplacement, un niveau, un message. GitHub l'ingère dans son onglet Security, VS Code l'affiche dans le code, et n'importe quel script le lit sans expression régulière.

La sortie texte d'un outil change à chaque version mineure. Un pipeline qui la grep casse silencieusement — et un contrôle de sécurité qui casse silencieusement est pire que pas de contrôle du tout.

Pour vous repérer

La confusion qui coûte le plus cher à cet endroit : croire qu'un moteur SAST comprend votre programme. Il n'en exécute aucune ligne. Il transforme votre fichier en arbre syntaxique et cherche des formes dans cet arbre — c'est tout. Une variable qui vaut "admin" ou une valeur venue du réseau ont exactement la même tête, sauf si une règle de teinte les distingue explicitement.

Test d'auto-vérification : si vous savez dire pourquoi subprocess.check_output(CMD, shell=True) avec CMD constante sera quand même signalée par p/python, et pourquoi la règle de teinte que vous écrirez cet après-midi ne la signalera pas, la leçon est acquise. C'est exactement l'écart entre un motif et un chemin.

✓ terminé
Quiz

Quiz — SAST : portée et limites

Jour 1 · SAST — lire le code sans l'exécuter · 10 min

1. Une règle de suivi de teinte se distingue d'un simple motif parce qu'elle :

2. Semgrep ne signale pas VULN-04, l'IDOR de /profile. Comment qualifiez-vous ce résultat ?

3. Votre requirements.txt épingle urllib3==1.26.4, affectée par plusieurs CVE. Que remonte Semgrep ?

4. hashlib.md5(chemin.encode()).hexdigest() sert à nommer un fichier de cache. La règle md5 se déclenche. C'est :

5. Pourquoi produire du SARIF plutôt que d'analyser la sortie texte de l'outil dans le pipeline ?

✓ terminé
Travaux pratiques

TP 1 — Première passe Semgrep sur devsecops-lab

Jour 1 · SAST — lire le code sans l'exécuter · 1 h

Obtenir un inventaire SAST complet de votre application, au format lisible puis au format machine, et le confronter à la prédiction de la première leçon.

Point de départ

Votre dépôt devsecops-lab tel que vous l'avez laissé mercredi soir : VULN-01 et VULN-05 corrigées, le pipeline au vert, huit vulnérabilités encore en place. Docker démarré. Aucun conteneur n'a besoin de tourner pour ce TP — le SAST n'exécute rien.

Toutes les commandes se lancent depuis la racine du dépôt. Vérifiez-le avec pwd et ls app.py avant la première.

  1. Placez-vous dans le dépôt, créez le dossier d'artefacts et assurez-vous qu'il n'est pas versionné. Les sorties d'outils ne se committent pas : elles se régénèrent, et elles vieillissent mal dans un historique Git.
    cd ~/devsecops-lab
    mkdir -p artefacts
    grep -q '^artefacts/' .gitignore || echo 'artefacts/' >> .gitignore
    
    docker run --rm -v "$(pwd)":/src -w /src semgrep/semgrep:1.85.0 \
      semgrep scan --metrics=off \
        --config p/python --config p/flask --config p/security-audit \
        .
  2. Lisez la sortie du haut vers le bas, sans la trier. Chaque bloc donne le nom de la règle, le fichier, la ligne, un extrait de code et un message. Notez à la main, sur papier ou dans un fichier, le nombre total de résultats — vous en aurez besoin dans quinze minutes.
  3. Relancez la même analyse en produisant du SARIF. C'est le format que vous exploiterez partout ensuite : dans le pipeline, dans l'onglet Security de GitHub, et dans vos rapports.
    docker run --rm -v "$(pwd)":/src -w /src semgrep/semgrep:1.85.0 \
      semgrep scan --metrics=off \
        --config p/python --config p/flask --config p/security-audit \
        --sarif --output artefacts/semgrep-2026-08-06.sarif .
  4. Comptez par règle, avec un script — jamais à l'œil. Ce décompte est votre première mesure de bruit : beaucoup de résultats pour peu de règles distinctes signale une règle qui se déclenche en boucle, et non un code particulièrement mauvais.
    python3 - <<'PY'
    import collections
    import json
    
    d = json.load(open("artefacts/semgrep-2026-08-06.sarif"))
    run = d["runs"][0]
    niveau = {r["id"]: r.get("defaultConfiguration", {}).get("level", "?")
              for r in run["tool"]["driver"].get("rules", [])}
    c = collections.Counter(r["ruleId"] for r in run["results"])
    for regle, n in c.most_common():
        lieu = run["results"][0]["locations"][0]["physicalLocation"]["artifactLocation"]["uri"]
        print(f"{n:3}  {niveau.get(regle, '?'):8}  {regle}")
    print("---")
    print("total :", sum(c.values()), "resultats,", len(c), "regles distinctes")
    PY
  5. Sortez maintenant la liste des emplacements, une ligne par résultat. C'est cette liste que vous allez trier au TP suivant.
    python3 - <<'PY'
    import json
    
    d = json.load(open("artefacts/semgrep-2026-08-06.sarif"))
    for r in d["runs"][0]["results"]:
        p = r["locations"][0]["physicalLocation"]
        print(f'{p["artifactLocation"]["uri"]}:{p["region"]["startLine"]:<4} '
              f'{r["ruleId"].split(".")[-1]}')
    PY
  6. Ouvrez app.py à côté de cette liste et faites la confrontation, vulnérabilité par vulnérabilité. Pour chacune des dix VULN-NN, répondez par oui ou non : Semgrep l'a-t-il trouvée ? Écrivez la réponse, ne la gardez pas en tête.
  7. Dernière étape, et c'est celle qui compte pour le pipeline : relancez en mode bloquant. --error fait sortir Semgrep en code 1 dès qu'une règle correspond, et --severity ERROR restreint aux règles de haute confiance. Comparez le nombre de résultats avec celui de l'étape 2.
    docker run --rm -v "$(pwd)":/src -w /src semgrep/semgrep:1.85.0 \
      semgrep scan --metrics=off --error --severity ERROR \
        --config p/python --config p/flask .
    echo "code de sortie : $?" 
  8. Notez les deux chiffres côte à côte : tous jeux de règles, toutes gravités contre deux jeux de règles, gravité ERROR. L'écart entre les deux est exactement la marge de manœuvre dont vous disposez pour régler un seuil de blocage. Vous vous en servirez lundi.
Questions de réflexion — à préparer par écrit
  • Combien de résultats pour combien de règles distinctes ? Que vous dit ce rapport sur la qualité de votre code, et que ne vous dit-il pas ?
  • Le jeu p/security-audit est décrit par son éditeur comme destiné à un audit ponctuel et non à un pipeline. Après l'avoir exécuté, formulez en deux phrases pourquoi.
  • Quelles vulnérabilités de la table VULN-01..10 Semgrep n'a-t-il pas trouvées ? Pour chacune, la cause est-elle un défaut de règle ou une limite de la méthode ?
Pour vous repérer

Deux pièges à cet endroit précis, et ils ressemblent tous les deux à une panne.

Le premier : $(pwd) désigne le dossier depuis lequel vous tapez la commande. Lancée un cran trop haut, l'analyse scanne votre dossier personnel — elle met trois minutes, sort deux cents résultats et aucun ne concerne app.py. Vérifiez toujours par pwd avant de lancer.

Le second : le premier appel télécharge les jeux de règles depuis le registre public. Sans réseau, Semgrep échoue sur p/python avec un message de résolution — ce n'est pas votre commande qui est fausse. Les appels suivants sont servis par le cache du conteneur… qui disparaît avec --rm. C'est pour cela que les règles maison de cet après-midi vivront dans le dépôt.

✓ terminé
Travaux pratiques

TP 2 — Trier : vrai positif, faux positif, angle mort

Jour 1 · SAST — lire le code sans l'exécuter · 1 h

Rendre un verdict argumenté sur chaque résultat de la passe précédente, et produire la liste de ce que le SAST n'a pas vu.

Ce TP produit un livrable écrit

Le tri n'a de valeur que consigné. Ouvrez dès maintenant rapports/sast-devsecops-lab.md — vous y écrirez au fil de l'eau, et vous le finirez en fin de journée au TP 5.

Un verdict sans justification écrite sera refait à l'identique dans trois mois par quelqu'un qui n'aura pas votre contexte. C'est la première raison pour laquelle les équipes finissent par ignorer leurs propres outils.

  1. Créez un faux positif authentique — pas une simulation. Ce petit module de cache est du code que vous pourriez écrire demain, et il emploie md5 pour une raison parfaitement légitime : nommer un fichier de manière déterministe.
    mkdir -p outils
    # Collez le contenu ci-dessous dans outils/cache.py
    
  2. Voici le fichier. Recopiez-le tel quel dans outils/cache.py.
    #!/usr/bin/env python3
    """Petit cache disque pour les reponses de /fetch. Rien de cryptographique ici."""
    import hashlib
    import pathlib
    
    CACHE = pathlib.Path("/tmp/dso-cache")
    
    
    def clef(url: str) -> str:
        """Nom de fichier deterministe pour une URL. Le condense ne protege aucun secret."""
        return hashlib.md5(url.encode()).hexdigest()
    
    
    def lire(url: str):
        f = CACHE / clef(url)
        return f.read_text() if f.exists() else None
    
    
    def ecrire(url: str, contenu: str) -> None:
        CACHE.mkdir(parents=True, exist_ok=True)
        (CACHE / clef(url)).write_text(contenu)
  3. Analysez ce seul fichier. Semgrep signale l'usage de md5, exactement comme il le signale sur /hash dans app.py.
    docker run --rm -v "$(pwd)":/src -w /src semgrep/semgrep:1.85.0 \
      semgrep scan --metrics=off --config p/python outils/cache.py
  4. Arrêtez-vous une minute sur ce résultat. La même règle, deux verdicts opposés : sur app.py c'est VULN-06, un mot de passe haché avec un algorithme cassé ; sur outils/cache.py c'est un index de cache, où md5 est adapté et rapide. Aucun réglage de l'outil ne fera cette distinction à votre place — parce qu'elle dépend de ce que la valeur protège.
  5. Corrigez le faux positif de la bonne manière : en rendant l'intention explicite dans le code. usedforsecurity=False existe depuis Python 3.9 ; Semgrep et Bandit le respectent tous les deux, parce que le code dit désormais la vérité.
    def clef(url: str) -> str:
        """Nom de fichier deterministe pour une URL.
    
        usedforsecurity=False declare l'intention au lecteur ET a l'outil : ce condense
        sert d'index de cache, il ne protege aucun secret. Sans ce drapeau, la meme
        ligne est indiscernable de VULN-06, qui hache un mot de passe.
        """
        return hashlib.md5(url.encode(), usedforsecurity=False).hexdigest()
  6. Relancez l'analyse du fichier : l'alerte a disparu, et vous n'avez rien désactivé.
    docker run --rm -v "$(pwd)":/src -w /src semgrep/semgrep:1.85.0 \
      semgrep scan --metrics=off --config p/python outils/cache.py
  7. Posez maintenant le .semgrepignore du dépôt. Attention au sens de ce fichier : il retire des chemins de l'analyse, il ne fait pas taire des règles. Les fixtures que vous écrirez cet après-midi doivent en faire partie — elles contiennent du code volontairement vulnérable.
    # .semgrepignore — ce que l'analyse ne doit pas parcourir.
    # Attention : ce fichier retire des FICHIERS de l'analyse, pas des regles.
    # On n'y met jamais du code applicatif pour faire taire une alerte.
    
    .git/
    artefacts/
    rapports/
    __pycache__/
    *.pyc
    venv/
    .venv/
    
    # Les fixtures de test des regles maison contiennent du code volontairement
    # vulnerable : elles doivent etre analysees par --test, jamais par le scan normal.
    .semgrep/tests/
  8. Reprenez maintenant la liste complète du TP 1 et rendez un verdict pour chaque ligne. Écrivez-la dans rapports/sast-devsecops-lab.md sous forme de tableau à quatre colonnes : règle · emplacement · verdict · justification. Trois verdicts possibles, et trois seulement : vrai positif, faux positif, à discuter. Pas de quatrième catégorie, pas de ligne sautée.
  9. Écrivez ensuite la section qui compte le plus, et qu'aucun outil ne produira jamais pour vous : « Ce que le SAST n'a pas vu ». Pour chaque vulnérabilité absente de la sortie, donnez la cause — logique métier, dépendance, configuration d'exécution — et le moyen par lequel vous comptez la trouver. Vous vérifierez cette prévision demain et lundi.
  10. Committez le tout sur une branche dédiée, avec un message qui dit ce qui a été fait.
    git switch -c feat/triage-sast
    git add outils/cache.py .semgrepignore rapports/sast-devsecops-lab.md
    git commit -m "docs(sast): triage de la premiere passe semgrep et faux positif md5"
Questions de réflexion — à préparer par écrit
  • Vous avez corrigé le faux positif en modifiant le code plutôt qu'en annotant. Dans quelles situations l'annotation reste-t-elle la seule option raisonnable ?
  • Un nosemgrep nu désactive toutes les règles sur la ligne, y compris celles écrites après lui. Comment détecteriez-vous, dans six mois, la présence d'annotations trop larges dans le dépôt ?
  • Votre liste d'angles morts en compte combien ? Laquelle vous semble la plus dangereuse en production, et pourquoi ce n'est pas forcément celle qui a le score le plus élevé ?
Pour vous repérer

La tentation, à cet endroit, est de faire taire l'outil. Retenez la hiérarchie des trois gestes, du meilleur au pire :

  1. Changer le code pour qu'il exprime l'intention — ici, usedforsecurity=False. L'alerte disparaît et le prochain lecteur comprend. C'est le seul geste qui améliore quelque chose.
  2. Annoter avec l'identifiant de la règlenosemgrep: la.regle.exacte suivi d'un motif écrit. L'alerte disparaît, les autres règles continuent de s'appliquer sur cette ligne.
  3. Annoter sans identifiant — un nosemgrep nu désactive toutes les règles sur la ligne, pour toujours, y compris celles qui n'existent pas encore. C'est un trou permanent : ne le faites jamais.

Et .semgrepignore n'est pas dans cette liste : il exclut des fichiers de l'analyse. Y mettre app.py pour avoir la paix est le geste qui rend le pipeline décoratif.

✓ terminé
Leçon

Anatomie d'une règle Semgrep

Jour 1 · SAST — lire le code sans l'exécuter · 25 min

Une règle Semgrep est un document YAML. Elle se lit, elle se relit en revue de code, elle se teste, elle se versionne. C'est la différence entre configurer un outil et écrire de la sécurité.

La structure minimale

rules:
  - id: exemple-minimal
    languages: [python]
    severity: WARNING
    message: >-
      Message affiche au developpeur. Il doit dire QUOI faire, pas seulement
      ce qui ne va pas.
    metadata:
      cwe: "CWE-000: exemple"
      owasp: "A00:2021 - exemple"
      confidence: HIGH
    patterns:
      - pattern: dangereux($ARG, ...)
      - pattern-not: dangereux("...", ...)
    fix: sur($ARG)
CléRôleÀ savoir
ididentifiant unique, stableil apparaît dans le SARIF, dans les annotations nosemgrep et dans vos rapports : le changer casse tout ce qui le référence
severityINFO, WARNING ou ERRORc'est ce que filtre --severity dans le pipeline ; réservez ERROR à ce qui doit bloquer
messagece que lit le développeuril doit dire quoi faire. « Usage dangereux détecté » ne corrige rien
metadataCWE, OWASP, confiance, référence internechamp libre ; c'est là que vous rattachez vos VULN-NN
patternsliste combinée en ETtoutes les entrées doivent correspondre au même endroit
pattern-eitherliste combinée en OUune seule entrée suffit
pattern-notexclusionretire les correspondances qui satisfont aussi ce motif
fixremplacement suggéréaffiché par défaut, appliqué avec --autofix — jamais en CI

Le mode teinte

Quand la règle doit suivre un chemin plutôt que reconnaître un point, on passe en mode: taint. Trois clés remplacent alors patterns :

  • pattern-sources — d'où vient la donnée non fiable ;
  • pattern-sanitizers — ce qui la rend sûre (facultatif, mais c'est lui qui supprime les faux positifs) ;
  • pattern-sinks — où elle ne doit pas arriver.

focus-metavariable: $CMD, dans un puits, restreint la teinte à un argument précis : sans lui, une valeur teintée passée en troisième argument déclencherait la règle alors que seul le premier est interprété par l'interpréteur de commandes.

Deux confusions qui coûtent une demi-heure chacune

ET contre OU. patterns: combine en conjonction, pattern-either: en disjonction. Une règle qui ne remonte jamais rien a le plus souvent un patterns: décrivant deux formes incompatibles — vous avez demandé un code qui soit simultanément deux choses différentes.

pattern-not contre pattern-not-inside. Le premier exclut ce qui est le motif ; le second exclut ce qui se trouve dans le motif. Pour ignorer tout ce qui est écrit dans un bloc try de test, c'est pattern-not-inside qu'il faut.

Le test de la règle : la partie qu'on saute et qu'il ne faut pas sauter

Semgrep sait tester ses propres règles. Vous écrivez un fichier de fixture contenant du code annoté, et semgrep --test vérifie que la règle remonte exactement ce qu'elle doit remonter :

  • # ruleid: mon-id avant une ligne — la règle doit la signaler ;
  • # ok: mon-id avant une ligne — la règle ne doit pas la signaler.

L'appariement entre la règle et sa fixture se fait par nom de fichier : .semgrep/flask-regles.yml se teste avec .semgrep/tests/flask-regles.py. Un nom qui ne correspond pas, et le test passe en ne testant rien.

Pourquoi les cas « ok » comptent plus que les cas « ruleid »

Une règle qui signale absolument tout satisfait tous les cas ruleid: imaginables. Ce sont les cas ok: qui mesurent la précision : ils encodent les faux positifs que vous refusez de subir.

Dans la fixture que vous allez écrire, la règle de teinte aura trois cas ok: pour un seul ruleid: — la valeur assainie, la commande constante et l'appel sans interpréteur. C'est la bonne proportion.

Où les ranger

Dans .semgrep/, à la racine du dépôt, versionné. Trois conséquences immédiates : les règles fonctionnent hors ligne, elles passent en revue de code comme le reste, et une règle cassée se reverte comme un commit.

Pour vous repérer

Le piège de syntaxe qui fait perdre le plus de temps : la différence entre pattern-not et pattern-not-inside. Le premier retire les correspondances identiques à un motif ; le second retire celles qui se trouvent à l'intérieur d'un contexte. Une règle qui remonte trop confond presque toujours les deux.

Le second : dans un bloc patterns:, les entrées se combinent en ET. Dans pattern-either:, en OU. Une règle qui ne remonte jamais rien a très souvent un patterns: là où il fallait un pattern-either: — vous avez écrit une conjonction impossible.

Test d'auto-vérification : si vous savez dire pourquoi votre fixture doit contenir au moins un cas ok: par règle, et pas seulement des cas ruleid:, vous avez compris à quoi sert --test. Une règle qui signale tout passe tous les tests ruleid: du monde.

✓ terminé
Travaux pratiques

TP 3 — Écrire .semgrep/flask-regles.yml

Jour 1 · SAST — lire le code sans l'exécuter · 1 h 15

Produire trois règles maison — un motif, une règle de teinte, une règle avec correction suggérée — les tester, puis les brancher au pipeline.

L'ordre de travail

Fixture d'abord, règle ensuite, test, puis application au code réel. Cet ordre n'est pas négociable dans ce TP : c'est le seul qui vous dise la différence entre « ma règle ne trouve rien » et « ma règle est cassée ».

Les trois règles ci-dessous sont données complètes. Recopiez-les, mais lisez chaque clé : au point de contrôle, votre encadrant vous demandera de retirer une ligne au hasard et de prédire ce que devient le résultat.

  1. Créez l'arborescence des règles et de leurs tests.
    mkdir -p .semgrep/tests
  2. Écrivez la fixture avant les règles, dans .semgrep/tests/flask-regles.py. Elle décrit ce que vous attendez : quatre cas à signaler, cinq à ne pas signaler.
    # .semgrep/tests/flask-regles.py
    # Fixture des regles maison. Chaque annotation est une assertion :
    #   ruleid: <id>   la regle DOIT remonter la ligne qui suit
    #   ok: <id>       la regle NE DOIT PAS remonter la ligne qui suit
    # Ce fichier n'est jamais importe : il est lu par « semgrep --test ».
    import ipaddress
    import shlex
    import subprocess
    
    import yaml
    from flask import render_template_string, request
    
    
    def gabarit_ko():
        terme = request.args.get("q", "")
        # ruleid: caplogy-flask-gabarit-dynamique
        return render_template_string("<h1>Resultats pour " + terme + "</h1>")
    
    
    def gabarit_ok():
        # ok: caplogy-flask-gabarit-dynamique
        return render_template_string("<h1>Resultats pour {{ terme }}</h1>",
                                      terme=request.args.get("q", ""))
    
    
    def shell_ko():
        hote = request.args.get("host", "127.0.0.1")
        # ruleid: caplogy-entree-http-vers-shell
        return subprocess.check_output("ping -c1 " + hote, shell=True)
    
    
    def shell_assaini():
        hote = shlex.quote(request.args.get("host", "127.0.0.1"))
        # ok: caplogy-entree-http-vers-shell
        return subprocess.check_output("ping -c1 " + hote, shell=True)
    
    
    def shell_constant():
        # ok: caplogy-entree-http-vers-shell
        return subprocess.check_output("uptime", shell=True)
    
    
    def shell_liste():
        hote = str(ipaddress.ip_address(request.args.get("host", "127.0.0.1")))
        # ok: caplogy-entree-http-vers-shell
        return subprocess.check_output(["ping", "-c1", hote])
    
    
    def yaml_ko(donnee):
        # ruleid: caplogy-yaml-load-non-sur
        return yaml.load(donnee, Loader=yaml.Loader)
    
    
    def yaml_ok(donnee):
        # ok: caplogy-yaml-load-non-sur
        return yaml.safe_load(donnee)
  3. Écrivez maintenant les trois règles dans .semgrep/flask-regles.yml. Remarquez la progression : la première est un motif avec exclusion, la deuxième une règle de teinte complète, la troisième un motif assorti d'une correction suggérée.
    # .semgrep/flask-regles.yml
    # Regles maison de devsecops-lab. Elles vivent dans le depot : elles fonctionnent
    # hors ligne, elles se relisent en revue, et elles evoluent avec le code.
    rules:
    
      # ------------------------------------------------------------------ VULN-02
      # severity WARNING, PAS ERROR, et c'est un choix : le motif a de vrais faux
      # positifs (un gabarit constant defini plus haut declenche la regle). On ne
      # bloque donc pas le pipeline dessus. C'est le DAST du jour 3 qui fera autorite
      # sur le XSS : lui apporte la PREUVE que ce motif ne peut pas donner.
      - id: caplogy-flask-gabarit-dynamique
        languages: [python]
        severity: WARNING
        message: >-
          render_template_string() recoit un gabarit construit dynamiquement. Toute
          entree utilisateur qui y arrive est interpretee par Jinja2 : XSS reflechi
          au minimum, execution de gabarit (SSTI) au pire. Passer un gabarit fixe et
          transmettre la valeur en variable : render_template_string("{{ x }}", x=v).
        metadata:
          cwe: "CWE-79: Improper Neutralization of Input During Web Page Generation"
          owasp: "A03:2021 - Injection"
          vuln: VULN-02
          confidence: MEDIUM
        patterns:
          - pattern-either:
              - pattern: render_template_string($GABARIT, ...)
              - pattern: flask.render_template_string($GABARIT, ...)
          - pattern-not: render_template_string("...", ...)
          - pattern-not: flask.render_template_string("...", ...)
    
      # ------------------------------------------------------------------ VULN-03
      - id: caplogy-entree-http-vers-shell
        languages: [python]
        severity: ERROR
        mode: taint
        message: >-
          Une valeur issue de la requete HTTP atteint un interpreteur de commandes.
          Passer une liste d'arguments sans shell=True, ou valider strictement la
          valeur avant de la concatener.
        metadata:
          cwe: "CWE-78: Improper Neutralization of Special Elements used in an OS Command"
          owasp: "A03:2021 - Injection"
          vuln: VULN-03
          confidence: HIGH
        pattern-sources:
          - pattern-either:
              - pattern: request.args.get(...)
              - pattern: request.form.get(...)
              - pattern: request.args[...]
              - pattern: request.form[...]
              - pattern: flask.request.args.get(...)
        pattern-sanitizers:
          - pattern: shlex.quote(...)
          - pattern: ipaddress.ip_address(...)
        pattern-sinks:
          - patterns:
              - pattern-either:
                  - pattern: subprocess.check_output($CMD, ..., shell=True, ...)
                  - pattern: subprocess.run($CMD, ..., shell=True, ...)
                  - pattern: subprocess.Popen($CMD, ..., shell=True, ...)
                  - pattern: os.system($CMD)
              - focus-metavariable: $CMD
    
      # ------------------------------------------------------------------ VULN-07
      - id: caplogy-yaml-load-non-sur
        languages: [python]
        severity: ERROR
        message: >-
          yaml.load() avec un Loader complet construit des objets Python arbitraires
          (tag !!python/object/apply) : c'est une execution de code a distance des
          que la donnee vient du reseau. Utiliser yaml.safe_load().
        metadata:
          cwe: "CWE-502: Deserialization of Untrusted Data"
          owasp: "A08:2021 - Software and Data Integrity Failures"
          vuln: VULN-07
          confidence: HIGH
        patterns:
          - pattern-either:
              - pattern: yaml.load($D)
              - pattern: yaml.load($D, Loader=yaml.Loader)
              - pattern: yaml.load($D, Loader=yaml.UnsafeLoader)
              - pattern: yaml.load($D, Loader=yaml.FullLoader)
        fix: yaml.safe_load($D)
  4. Lancez le test. La sortie doit annoncer un nombre de cas exécuté et aucun échec. Si elle annonce zéro test trouvé, arrêtez-vous : le nom de base de la fixture ne correspond pas à celui du fichier de règles.
    docker run --rm -v "$(pwd)":/src -w /src semgrep/semgrep:1.85.0 \
      semgrep --test --config .semgrep/ .semgrep/tests/
  5. Cassez volontairement une règle et relancez le test. Retirez par exemple la ligne - pattern-not: render_template_string("...", ...) : le cas gabarit_ok doit maintenant échouer. C'est la preuve que votre test teste quelque chose. Remettez la ligne, relancez, revérifiez le vert.
  6. Appliquez enfin les règles au code réel. Vous devez voir remonter VULN-02, VULN-03 et VULN-07, et rien d'autre — trois résultats pour trois règles. Notez les gravités : VULN-03 et VULN-07 sortent en ERROR, VULN-02 en WARNING. Ce n'est pas un oubli, c'est une décision — voir l'encadré ci-dessous.
    docker run --rm -v "$(pwd)":/src -w /src semgrep/semgrep:1.85.0 \
      semgrep scan --metrics=off --config .semgrep/ --error app.py
    echo "code de sortie : $?" 
  7. Regardez ce que propose le champ fix:, sans l'appliquer. --dryrun affiche le correctif que --autofix écrirait. Vous corrigerez VULN-07 demain, à la main et sur une branche dédiée.
    docker run --rm -v "$(pwd)":/src -w /src semgrep/semgrep:1.85.0 \
      semgrep scan --metrics=off --config .semgrep/ --autofix --dryrun app.py
  8. Branchez les règles maison au pipeline. Remplacez le job sast de .github/workflows/ci.yml par celui-ci : il ajoute --config /src/.semgrep, restreint le blocage à la gravité ERROR, et publie le SARIF dans l'onglet Security de GitHub.
      sast:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - name: semgrep
            run: |
              docker run --rm -v "$PWD:/src" -w /src semgrep/semgrep:1.85.0 \
                semgrep scan --metrics=off --error --severity ERROR \
                  --config p/python --config p/flask --config /src/.semgrep \
                  --sarif --output /src/semgrep.sarif /src
          - name: publier le sarif
            if: always()
            uses: github/codeql-action/upload-sarif@v3
            with:
              sarif_file: semgrep.sarif
  9. Notez le changement de doctrine dans ce job : on charge p/python et p/flask, mais plus p/security-audit. Vous avez mesuré son bruit ce matin ; il reste excellent pour un audit lancé à la main, il n'a pas sa place dans une porte bloquante. Écrivez cette phrase dans votre rapport : c'est votre première décision de seuil, et elle sera défendue lundi.
  10. Committez, poussez, et vérifiez que le pipeline devient rouge — c'est attendu : VULN-03 et VULN-07 sont toujours dans app.py et vos règles viennent de les déclarer ERROR, donc bloquantes. VULN-02, elle, n'apparaît qu'en avertissement dans l'onglet Security : vous la traiterez lundi, avec le DAST, qui en apportera la preuve.
    git add .semgrep .github/workflows/ci.yml
    git commit -m "feat(sast): trois regles maison semgrep et leur fixture de test"
    git push -u origin feat/triage-sast
  11. Regardez au passage ce que vos annotations du 5 août n'ont pas fait. Les lignes /ping et /config portent encore un # nosemgrep: <règle> posé au module 1, avec échéance au 10 août. Il n'a rien suspendu ici : il nommait une règle de p/security-audit, et vos règles s'appellent caplogy-*. C'est la démonstration exacte de ce qui a été exigé de vous : une exception nommée ne couvre que la règle qu'elle nomme, et elle ne devient jamais un blanc-seing sur la ligne. Un nosemgrep nu, lui, aurait avalé silencieusement vos trois règles neuves. Laissez les annotations en place jusqu'à lundi : vous les lèverez avec les corrections, pas avant.
  12. Rangez ce constat dans votre journal — il fait partie de ce que vous défendrez lundi.
    # ajoutez une ligne a journal-incidents.md :
    # | 2026-08-06 | nosemgrep du 5 aout inoperant sur les regles maison | exception nommee, pas nue | comportement attendu, aucune action | 0 |
Questions de réflexion — à préparer par écrit
  • Votre règle de teinte accepte shlex.quote() comme assainisseur. Dans quel cas cet assainisseur serait-il insuffisant, et que faudrait-il ajouter ?
  • Vous avez classé la règle XSS en WARNING et les deux autres en ERROR. Défendez ce choix : pourquoi ne pas tout bloquer, et qu'est-ce qui rattrapera le XSS ?
  • La règle caplogy-flask-gabarit-dynamique signalerait render_template_string(GABARIT_CONSTANT), où la constante est définie plus haut dans le fichier. Faux positif ou vrai positif ? Justifiez, puis dites comment vous traiteriez le cas.
Pour vous repérer

Écrivez la fixture d'abord. Ce n'est pas un rituel : une règle vide passe silencieusement quand on n'a rien à lui opposer, et vous perdrez vingt minutes à chercher pourquoi app.py ne remonte rien alors que c'est la règle qui ne compile pas.

Les deux pannes qui vous attendent, dans l'ordre de fréquence :

  • Le test ne teste rien — « 0 tests found » ou un compte à zéro. Neuf fois sur dix, le nom de base ne correspond pas : flask-regles.yml exige flask-regles.py, pas test-flask.py.
  • La règle de teinte ne remonte rien — vérifiez que la source et le puits sont dans la même fonction. Semgrep OSS suit la teinte à l'intérieur d'une fonction ; franchir un appel vers un autre fichier demande l'analyse inter-fichiers, qui est payante.

Si --test refuse obstinément d'apparier, repliez-vous sur --config .semgrep/flask-regles.yml .semgrep/tests/flask-regles.py en scan normal et vérifiez à l'œil que les lignes remontées sont celles annotées ruleid:. Le contrôle est manuel, mais il vaut mieux qu'un test vert qui ne teste rien.

✓ terminé
Travaux pratiques

TP 4 — SonarQube CE : une découverte, puis on éteint

Jour 1 · SAST — lire le code sans l'exécuter · 45 min

Voir de ses yeux ce qu'apporte une plateforme SAST à serveur — quality gate, security hotspots, dette technique — et savoir dire pourquoi elle n'entre pas dans ce pipeline.

Un TP borné dans le temps et dans l'espace disque

Vous allumez, vous scannez une fois, vous lisez, vous éteignez, vous supprimez l'image. Quarante-cinq minutes, dont trois d'attente au démarrage. Aucun autre TP du parcours ne dépend de ce conteneur.

Prévoyez 2 Go de RAM libres et environ 1,5 Go de disque. Si votre poste est juste, arrêtez d'abord juice-shop (docker stop juice-shop) — vous le relancerez au M5.

  1. Lancez le serveur sur le réseau du parcours. Suivez les journaux et attendez la ligne SonarQube is operational — deux à trois minutes.
    docker run -d --name sonarqube --network dso-net \
      -p 9000:9000 sonarqube:10.6-community
    
    # Le demarrage prend deux a trois minutes : SonarQube leve un Elasticsearch interne.
    # Attendez la ligne « SonarQube is operational » avant d'ouvrir le navigateur.
    docker logs -f sonarqube
  2. Ouvrez http://localhost:9000. Identifiants initiaux admin / admin ; SonarQube impose immédiatement un changement de mot de passe. Créez ensuite un projet local nommé devsecops-lab, puis générez un jeton d'analyse dans My Account → Security. Le jeton n'est affiché qu'une fois.
  3. Lancez le scanner. Notez le sonar.host.url : il désigne le conteneur par son nom sur dso-net, pas localhost — depuis l'intérieur d'un conteneur, localhost c'est lui-même.
    docker run --rm --network dso-net \
      -v "$(pwd)":/usr/src \
      sonarsource/sonar-scanner-cli:11.0 \
      -Dsonar.projectKey=devsecops-lab \
      -Dsonar.sources=. \
      -Dsonar.exclusions=artefacts/**,rapports/**,.semgrep/tests/** \
      -Dsonar.host.url=http://sonarqube:9000 \
      -Dsonar.token=COLLEZ_ICI_VOTRE_JETON
  4. Retournez sur l'interface. Trois choses à regarder, et rien d'autre : le Quality Gate (passé ou échoué, et sur quelle condition exactement), l'onglet Security Hotspots, et l'onglet Issues filtré sur Vulnerability.
  5. Arrêtez-vous sur la notion de security hotspot : c'est un point du code à faire relire par un humain, pas une vulnérabilité affirmée. SonarQube assume ainsi son incertitude au lieu de la masquer derrière une gravité — c'est une réponse honnête au problème du faux positif, et une idée que vous pouvez reprendre dans votre propre triage. Choisissez-en un, tranchez-le (Safe ou To review) et écrivez le motif.
  6. Ouvrez maintenant votre rapport et écrivez la section « SonarQube CE : ce qu'il apporte, ce qu'il coûte ». Trois différences concrètes avec Semgrep, chiffrées quand c'est possible : quelque chose que Sonar a trouvé et pas Semgrep, quelque chose que Semgrep a trouvé et pas Sonar, et le coût d'exploitation du serveur. Interdiction d'écrire « c'est plus visuel ».
  7. Éteignez et supprimez. Vérifiez l'espace récupéré : c'est un chiffre que vous défendrez au point de contrôle.
    docker rm -f sonarqube
    docker image rm sonarqube:10.6-community sonarsource/sonar-scanner-cli:11.0
    docker system df   # verifiez l'espace recupere : environ 2 Go
  8. Dernier réflexe, celui qui distingue une découverte d'une accumulation : docker ps -a ne doit plus montrer aucune trace de SonarQube. Un service qu'on essaie et qu'on n'éteint pas devient une dépendance que personne n'a décidée.
Questions de réflexion — à préparer par écrit
  • SonarQube conserve l'historique des analyses et affiche une tendance. Sur quel type de projet cette fonction justifie-t-elle à elle seule un serveur à maintenir ?
  • Le security hotspot est une catégorie que Semgrep n'a pas. Comment obtiendriez-vous l'équivalent avec Semgrep, sans serveur ?
  • SonarQube héberge une copie de tout votre code source. Quelles conséquences en tirez-vous sur son propre durcissement, son exposition réseau et ses sauvegardes ?
Pour vous repérer

Ce TP a une fin explicite : le conteneur est supprimé avant le point de contrôle. Ce n'est pas de la coquetterie de ménage. SonarQube réserve environ 2 Go de mémoire en permanence, et vous en aurez besoin la semaine prochaine pour un cluster Kubernetes à deux nœuds. Une découverte qui reste allumée devient une dépendance involontaire.

Sous Linux, le démarrage échoue souvent sur max virtual memory areas vm.max_map_count [65530] is too low : c'est l'Elasticsearch embarqué. La parade est sudo sysctl -w vm.max_map_count=262144. Sous Docker Desktop (macOS), la valeur est déjà correcte.

Le jeton se génère dans My Account → Security après avoir changé le mot de passe admin. Il n'apparaît qu'une fois : copiez-le tout de suite.

✓ terminé
Travaux pratiques

TP 5 — Consigner la passe SAST

Jour 1 · SAST — lire le code sans l'exécuter · 45 min

Fermer la journée par un document qu'un tiers peut relire sans vous, et le faire entrer dans le dépôt par une demande de fusion.

Le plan attendu

Cinq sections, dans cet ordre : 1. périmètre et outillage (versions exactes des images) · 2. résultats bruts et chiffres · 3. tableau de triage complet · 4. ce que le SAST n'a pas vu, et par quoi vous comptez le trouver · 5. SonarQube CE : apport et coût.

Ce plan préfigure celui du rapport de pentest du M5 (§ 8 du référentiel). Vous l'écrivez trois fois d'ici la soutenance : autant le prendre en main maintenant.

  1. Relisez rapports/sast-devsecops-lab.md et complétez les sections 1 et 2. La section 1 porte les versions exactes : semgrep/semgrep:1.85.0, les jeux de règles employés, la date. Un rapport sans versions n'est pas rejouable, et un résultat non rejouable n'est pas un résultat.
  2. Complétez la section 2 avec les chiffres réels : nombre de résultats tous jeux confondus, nombre de règles distinctes, nombre de résultats en gravité ERROR sur p/python et p/flask seuls. Ces trois nombres sont l'argumentaire de votre futur seuil de blocage.
  3. Vérifiez que la section 3 ne laisse aucune ligne sans verdict et sans justification, et que la section 4 liste les angles morts avec la manière de les couvrir. Deux d'entre eux seront traités dès demain.
  4. Committez et poussez.
    git add rapports/sast-devsecops-lab.md
    git commit -m "docs(sast): rapport de la passe statique du 6 aout"
    git push
  5. Ouvrez la demande de fusion vers main. Dans la description, écrivez ce qu'un relecteur doit vérifier et ce qui reste ouvert.
    gh pr create --base main --head feat/triage-sast \
      --title "SAST : regles maison, triage et rapport du 6 aout" \
      --body "Trois regles maison + fixture testee. Rapport de triage complet.
    Le pipeline est ROUGE volontairement : VULN-02, VULN-03 et VULN-07 sont
    detectees par les nouvelles regles et seront corrigees au J2.
    A verifier en relecture : les cas 'ok:' de la fixture, et la decision
    de retirer p/security-audit de la porte bloquante."
  6. Ne fusionnez pas. Cette PR reste ouverte jusqu'à demain, pipeline rouge : c'est l'état normal d'une branche qui a découvert des failles mais ne les a pas encore corrigées. Une équipe qui fusionne quand même est une équipe qui a désactivé sa porte sans le dire.
Questions de réflexion — à préparer par écrit
  • Votre rapport permettrait-il à quelqu'un d'autre de reproduire exactement vos résultats dans six mois ? Qu'est-ce qui manquerait le plus ?
  • Vous laissez une PR ouverte avec un pipeline rouge. Quelle information faut-il y ajouter pour que ce ne soit pas lu comme un abandon ?
✓ terminé
Point de contrôle

M2 · J1 — Règle Semgrep maison : VULN-02 détectée, faux positif justifié

Jour 1 · SAST — lire le code sans l'exécuter · 30 min

Vérifiez avant de continuer
Ce que vous présentez au point de 16 h 30

Votre encadrant ne vous demandera pas de raconter : il vous fera retirer une ligne de l'une de vos règles et prédire quel test tombe. Puis il ouvrira app.py et outils/cache.py côte à côte et vous demandera pourquoi la même règle donne deux verdicts.

Préparez vos terminaux : le dépôt ouvert, Docker démarré, et la commande --test dans votre historique.

✓ terminé
Leçon

Votre code n'est qu'une fraction de ce que vous livrez

Jour 2 · SCA — la surface qu'on hérite · 25 min

Votre app.py fait cent trente lignes. L'image Docker que vous avez construite lundi pèse près d'un gigaoctet. Entre les deux, il y a tout ce que vous n'avez pas écrit et que vous livrez pourtant : c'est le domaine de la SCA.

devsecops-lab 130 lignes que vous avez écrites Flask 2.0.1 PyYAML 5.3.1 requests 2.25.1 Werkzeug Jinja2 itsdangerous urllib3 certifi idna 6 lignes déclarées Les six paquets du milieu sont dans votre requirements.txt. Ceux du bas n’y sont pas — et ils exécutent exactement les mêmes droits que votre code. C’est là que se logent la plupart des CVE que vous subissez.
Dépendances directes et transitives. Vous choisissez la deuxième ligne ; vous héritez de la troisième.

Direct et transitif

Votre requirements.txt déclare six paquets. pip install en installe une bonne quinzaine : Flask tire Werkzeug, Jinja2, itsdangerous, click, blinker et MarkupSafe ; requests tire urllib3, certifi, idna et charset-normalizer.

  • Les dépendances directes, vous les avez choisies. Vous pouvez en changer.
  • Les dépendances transitives, personne ne les a choisies. Elles s'exécutent dans le même processus, avec les mêmes droits, avec le même accès à vos secrets et à votre base.

Historiquement, la grande majorité des vulnérabilités qu'une application subit — par opposition à celles qu'elle contient — viennent de cette troisième ligne. C'est aussi la raison pour laquelle la SCA est le seul domaine où l'outil est réellement plus fort que l'humain : nul ne suit à la main les advisories de quinze paquets.

Le vocabulaire, correctement employé

TermeCe que c'estCe que ce n'est pas
CVEun identifiant mondial unique, CVE-AAAA-NNNNNni une gravité, ni un verdict sur votre application
Advisoryl'avis qui dit quelles versions sont touchées et lesquelles corrigent (GHSA sur GitHub, OSV chez Google, l'avis de l'éditeur)une source unique — deux bases se contredisent régulièrement
CVSSun score de gravité intrinsèque, de 0 à 10une probabilité d'exploitation, ni un niveau de risque pour vous
PURLl'adresse normalisée d'un paquet : pkg:pypi/pyyaml@5.3.1un nom local — c'est ce qui permet de croiser deux outils sans ambiguïté
SBOMl'inventaire complet et daté de ce que contient une livraisonun document d'audit qu'on produit une fois par an

Le SBOM, et pourquoi ce n'est pas de la paperasse

Un SBOM (Software Bill of Materials) est la nomenclature de votre logiciel : chaque composant, sa version, son PURL, sa licence, et le lien vers son parent. Deux formats dominent — CycloneDX (OWASP, orienté sécurité) et SPDX (Linux Foundation, orienté licences). Vous produirez du CycloneDX.

La question à laquelle un SBOM répond, et rien d'autre ne répond

Une faille critique est publiée un vendredi à 18 h dans une bibliothèque très répandue. Votre direction demande : « sommes-nous concernés, et où ? »

Avec un SBOM par version livrée, la réponse prend une requête et cinq minutes. Sans, elle prend deux jours de fouille dans les dépôts et les images — et elle est incomplète.

C'est aussi pour cette raison que le SBOM devient une exigence contractuelle : décret américain 14028 depuis 2021, règlement européen sur la cyber-résilience (CRA) qui monte en charge. Vous en signerez, dans votre carrière, avant d'en réclamer.

La chaîne d'approvisionnement, deux menaces à connaître

  • Le typosquatting — un paquet malveillant publié sous un nom voisin d'un paquet populaire (reqeusts, python-dateutil contre dateutil). Il s'installe par une faute de frappe et s'exécute à l'installation.
  • La confusion de dépendances — un paquet interne, non publié, dont le nom est enregistré sur le dépôt public par un tiers. Le gestionnaire, mal configuré, préfère la version publique, plus récente.

Aucune des deux ne se détecte par un scanner de CVE : ce ne sont pas des failles, ce sont des paquets hostiles. Elles se traitent par un miroir interne et un index verrouillé — hors périmètre de ce parcours, mais à savoir nommer.

Épingler protège de la surprise, pas de la faille

Votre requirements.txt épingle tout en == : deux installations à six mois d'écart donnent le même résultat, et c'est indispensable pour qu'un incident soit reproductible.

Mais épingler fige aussi les vulnérabilités. Un dépôt épinglé et jamais mis à jour pourrit exactement à la vitesse à laquelle les advisories sortent. C'est pour cela que l'épinglage n'a de sens qu'accompagné d'un mécanisme de mise à jour surveillée — vous installerez Dependabot en fin de journée.

✓ terminé
Leçon

Lire une CVE : le score, le vecteur, et la question qui décide

Jour 2 · SCA — la surface qu'on hérite · 25 min

Trivy va vous rendre une liste. Longue. Chaque ligne portera un identifiant, une gravité et un score. La tentation est de trier par score décroissant et de commencer par le haut : c'est la mauvaise méthode, et c'est celle que tout le monde applique.

Le score n'est que le résumé du vecteur

Un score CVSS v3.1 se calcule à partir de huit métriques de base. Le vecteur les donne toutes ; le nombre n'en est que la moyenne pondérée. Lisez le vecteur.

MétriqueValeursLa question qu'elle pose
AVAttack VectorNetwork, Adjacent, Local, Physicald'où l'attaquant doit-il opérer ?
ACAttack ComplexityLow, Highfaut-il des conditions particulières, hors du contrôle de l'attaquant ?
PRPrivileges RequiredNone, Low, Highfaut-il déjà un compte, et lequel ?
UIUser InteractionNone, Requiredfaut-il qu'une victime clique ?
SScopeUnchanged, Changedl'impact déborde-t-il du composant vulnérable ?
C/I/ANone, Low, Highconfidentialité, intégrité, disponibilité : combien est perdu ?

Ainsi CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H se lit en une phrase : depuis le réseau, sans condition particulière, sans compte, sans qu'aucune victime n'ait à cliquer, l'attaquant obtient tout. C'est 9.8, et c'est le pire cas courant.

Les bandes de sévérité — celles du parcours

BandeScore CVSS v3.1Délai de correction attendu
Critical9.0 – 10.0immédiat
High7.0 – 8.9sous 7 jours
Medium4.0 – 6.9sous 30 jours
Low0.1 – 3.9planifié
Info0.0à discrétion

Ce tableau vaut pour tous vos rapports jusqu'à la soutenance. Le M4 y ajoutera DREAD comme grille de priorisation métier, en complément et jamais en remplacement.

Les deux chiffres que le CVSS ne donne pas

  • EPSS — la probabilité, estimée statistiquement, qu'une CVE soit exploitée dans les trente prochains jours. Une CVE à 9.8 avec un EPSS de 0,2 % et une CVE à 7.5 avec un EPSS de 35 % ne se traitent pas dans le même ordre.
  • CISA KEV — le catalogue des vulnérabilités effectivement exploitées et observées dans la nature. Y figurer déplace immédiatement une CVE en tête de file, quel que soit son score.

Et la question qui décide vraiment

L'atteignabilité

Le code vulnérable est-il sur un chemin que votre application emprunte ?

Trois niveaux, du plus vague au plus utile : le paquet est installé ; la bibliothèque est importée ; la fonction vulnérable est appelée avec une donnée que l'attaquant contrôle. Seul le troisième niveau est un risque.

Les plateformes commerciales vendent cette analyse sous le nom de reachability. Vous allez la faire à la main, sur trois CVE, dans une heure — et vous verrez qu'elle renverse au moins une priorité.

Les trois décisions possibles, et seulement trois

  1. Corriger — monter de version. C'est le cas par défaut, et pour une dépendance c'est souvent gratuit. Quand c'est gratuit, on ne discute pas : on corrige, même si la faille n'est pas atteignable.
  2. Compenser — la correction casse quelque chose, ou n'existe pas encore (unfixed). On réduit alors l'exposition autrement : filtrage en amont, désactivation de la fonction, isolation réseau. La CVE reste, le risque baisse.
  3. Accepter — le risque est jugé négligeable pour ce déploiement. Cette décision est légitime, à trois conditions : elle est écrite, elle est datée d'une échéance de réexamen, et elle est prise par quelqu'un qui a l'autorité pour la prendre.
Une exception sans date n'est pas une exception, c'est un oubli

Le fichier .trivyignore.yaml que vous écrirez tout à l'heure porte un champ expired_at. Passée cette date, l'exception cesse de s'appliquer et le pipeline redevient rouge.

C'est volontaire, et c'est la seule mécanique qui empêche un fichier d'exclusions de devenir, en deux ans, la liste de tout ce que l'équipe a renoncé à comprendre.

Pour vous repérer

Le réflexe à désapprendre aujourd'hui : trier par score décroissant et commencer par le haut. Un 9.8 dans une fonction que votre code n'appelle jamais est moins urgent qu'un 6.5 sur le chemin d'authentification. Le score mesure la faille, pas votre exposition.

Lisez le vecteur, pas le nombre. AV:N (réseau) contre AV:L (local) change tout : si le composant n'est joignable que depuis un processus déjà compromis, l'attaquant a déjà gagné. PR:H (privilèges élevés requis) signifie que la faille suppose un compte administrateur — la question devient « qui a ce compte ? ».

Test d'auto-vérification : si vous savez dire ce que change UI:R par rapport à UI:N pour une application sans interface utilisateur, le vecteur est acquis.

✓ terminé
Quiz

Quiz — CVE, CVSS et SBOM

Jour 2 · SCA — la surface qu'on hérite · 10 min

1. Trivy remonte une CVE notée 9.8 dans un module de PyYAML que votre application n'importe jamais. Que faites-vous en premier ?

2. Que dit exactement le vecteur CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N ?

3. À quoi sert concrètement un SBOM le jour où une faille majeure est publiée ?

4. Une CVE Medium (5.3) touche une fonction que votre code appelle sur chaque requête ; la version corrigée est publiée et compatible. Que faites-vous ?

5. Vous épinglez toutes vos dépendances en ==. Quel est l'effet exact ?

✓ terminé
Travaux pratiques

TP 6 — Trivy fs : l'inventaire des CVE de votre application

Jour 2 · SCA — la surface qu'on hérite · 1 h

Obtenir la liste complète des vulnérabilités connues de vos dépendances, comprendre l'arbre qui les porte, et mesurer l'effet de chaque filtre sur le décompte.

Ce que Trivy lit, et ce qu'il ne lit pas

En mode fs, Trivy analyse les fichiers de déclaration de dépendances qu'il trouve — ici requirements.txt. Il ne lit pas votre app.py : il ne saura jamais si vous appelez le code vulnérable. C'est exactement l'angle mort que vous avez écrit hier soir dans votre rapport SAST, vu de l'autre côté.

Le scan de l'image Docker, qui voit en plus les paquets du système d'exploitation, c'est le M3. Aujourd'hui, on reste au niveau du dépôt.

  1. Créez le volume de cache — une fois pour tout le parcours — puis lancez le premier scan en sortie lisible.
    cd ~/devsecops-lab
    docker volume create trivy-cache 2>/dev/null
    
    docker run --rm \
      -v "$(pwd)":/src -w /src \
      -v trivy-cache:/root/.cache/trivy \
      aquasec/trivy:0.53.0 \
      fs --scanners vuln --format table .
  2. Lisez le tableau colonne par colonne : Library, Vulnerability, Severity, Installed Version, Fixed Version, Title. La colonne qui commande vos actions n'est ni la gravité ni le titre : c'est Fixed Version. Vide, il n'existe pas de correctif et vous devrez compenser.
  3. Affichez maintenant l'arbre des dépendances, restreint aux gravités hautes. C'est là que le transitif devient visible : vous verrez apparaître des paquets que vous n'avez jamais déclarés.
    docker run --rm \
      -v "$(pwd)":/src -w /src \
      -v trivy-cache:/root/.cache/trivy \
      aquasec/trivy:0.53.0 \
      fs --scanners vuln --dependency-tree --severity HIGH,CRITICAL .
  4. Relevez deux paquets remontés qui ne figurent pas dans votre requirements.txt. Écrivez-les. Ce sont vos dépendances transitives : personne ne les a choisies, elles tournent avec vos droits.
  5. Produisez la version machine, celle qui servira au rapport et au pipeline.
    docker run --rm \
      -v "$(pwd)":/src -w /src \
      -v trivy-cache:/root/.cache/trivy \
      aquasec/trivy:0.53.0 \
      fs --scanners vuln --format json \
         --output artefacts/trivy-fs-2026-08-07.json .
  6. Comptez et triez par script. Notez le premier des quatre nombres de la journée : le décompte brut, toutes gravités confondues.
    python3 - <<'PY'
    import collections
    import json
    
    d = json.load(open("artefacts/trivy-fs-2026-08-07.json"))
    par_gravite = collections.Counter()
    lignes = []
    for res in d.get("Results", []):
        for v in res.get("Vulnerabilities", []) or []:
            par_gravite[v["Severity"]] += 1
            lignes.append((v["Severity"], v["PkgName"], v["InstalledVersion"],
                           v.get("FixedVersion", "-"), v["VulnerabilityID"],
                           (v.get("CVSS", {}).get("nvd", {}) or {}).get("V3Score", "-")))
    
    ordre = ["CRITICAL", "HIGH", "MEDIUM", "LOW", "UNKNOWN"]
    lignes.sort(key=lambda l: (ordre.index(l[0]) if l[0] in ordre else 9, l[1]))
    for g, paquet, installee, corrigee, cve, score in lignes:
        print(f"{g:9} {paquet:12} {installee:9} -> {corrigee:9} {cve:18} {score}")
    print("---")
    for g in ordre:
        if par_gravite[g]:
            print(f"{g:9} {par_gravite[g]}")
    PY
  7. Choisissez maintenant trois CVE dans la liste, de gravités différentes, et allez lire leur advisory : le lien figure dans la sortie JSON, champ PrimaryURL. Pour chacune, recopiez le vecteur CVSS complet dans votre rapport et traduisez-le en une phrase française. Ces trois-là seront le matériau du TP suivant.
  8. Appliquez enfin les filtres du futur seuil de blocage, et regardez le code de sortie. --severity HIGH,CRITICAL restreint la gravité, --ignore-unfixed écarte ce qui n'a pas de correctif, --exit-code 1 fait échouer la commande.
    docker run --rm \
      -v "$(pwd)":/src -w /src \
      -v trivy-cache:/root/.cache/trivy \
      aquasec/trivy:0.53.0 \
      fs --scanners vuln --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 .
    echo "code de sortie : $?" 
  9. Écrivez les quatre nombres côte à côte dans votre rapport : brut, HIGH+CRITICAL, HIGH+CRITICAL corrigeables, et le code de sortie. Cet écart est votre marge de négociation quand une équipe vous dira que la porte est trop stricte. Un seuil se défend avec des chiffres, pas avec une doctrine.
Questions de réflexion — à préparer par écrit
  • Combien de vulnérabilités disparaissent entre le décompte brut et le décompte filtré ? Où sont-elles passées, et qui s'en occupe désormais ?
  • --ignore-unfixed écarte les failles sans correctif. Défendez ce choix pour une porte de pipeline, puis attaquez-le pour un rapport d'audit.
  • Trivy ne lit pas votre code. Nommez une conséquence concrète de cette limite sur la liste que vous venez d'obtenir.
Pour vous repérer

Trivy télécharge sa base de vulnérabilités au premier appel — une centaine de mégaoctets. Avec --rm et sans volume de cache, il la retélécharge à chaque commande : c'est la raison numéro un des « Trivy est lent ». Le volume trivy-cache de la première étape règle le problème une fois pour toutes.

Second point, et il va vous surprendre : la base de Trivy est vivante. Les identifiants exacts que vous verrez aujourd'hui ne sont pas ceux d'un cours écrit il y a six mois, et c'est normal. Ne recopiez jamais une liste de CVE depuis un support — la seule liste qui vaut est celle que votre commande vient de produire.

Enfin, --ignore-unfixed retire les vulnérabilités sans correctif publié. Utile dans une porte bloquante — on ne bloque pas une équipe sur ce qu'elle ne peut pas corriger — mais dangereux dans un rapport : ces failles existent, et il faut leur trouver une compensation.

✓ terminé
Travaux pratiques

TP 7 — Produire un SBOM CycloneDX et demander un second avis

Jour 2 · SCA — la surface qu'on hérite · 1 h

Générer l'inventaire normalisé de votre application, le relire, puis le soumettre à un second moteur pour mesurer le désaccord entre bases de vulnérabilités.

Un SBOM ne se commite pas, il se date et se joint

artefacts/ est dans votre .gitignore depuis hier, et c'est volontaire. Un SBOM décrit un instant de la chaîne de dépendances : versionner tous ses états successifs pollue l'historique sans rien apporter.

La bonne place d'un SBOM est en pièce jointe d'une livraison : attaché à une release GitHub, publié dans un registre, ou archivé comme artefact de pipeline. Vous ferez cela au M3, quand l'image sera construite en CI.

  1. Générez le SBOM au format CycloneDX. C'est Trivy qui le produit — inutile d'ajouter Syft, qui ferait exactement la même chose avec un outil de plus à maintenir.
    docker run --rm \
      -v "$(pwd)":/src -w /src \
      -v trivy-cache:/root/.cache/trivy \
      aquasec/trivy:0.53.0 \
      fs --format cyclonedx --output artefacts/sbom.cdx.json .
  2. Ouvrez-le. Pas dans un éditeur : par script, et regardez la structure. bomFormat, specVersion, l'horodatage, l'outil générateur, puis la liste des composants avec leur purl.
    python3 - <<'PY'
    import json
    
    d = json.load(open("artefacts/sbom.cdx.json"))
    print("format      :", d.get("bomFormat"), "spec", d.get("specVersion"))
    print("genere le   :", d["metadata"]["timestamp"])
    print("outil       :", d["metadata"]["tools"])
    comps = d.get("components", [])
    print("composants  :", len(comps))
    print()
    for c in comps[:25]:
        print(f'  {c.get("name", "?"):22} {c.get("version", "?"):12} {c.get("purl", "")}')
    PY
  3. Comparez deux nombres : le nombre de composants du SBOM et le nombre de lignes de votre requirements.txt. L'écart, c'est l'arbre transitif. Écrivez les deux chiffres — c'est l'illustration la plus directe de la leçon de ce matin.
  4. Vérifiez que le SBOM est exploitable : Trivy sait le rescanner directement, sans repasser par le système de fichiers. C'est ainsi qu'on analyse une livraison dont on n'a plus le code source.
    docker run --rm \
      -v "$(pwd)":/src -w /src \
      -v trivy-cache:/root/.cache/trivy \
      aquasec/trivy:0.53.0 \
      sbom --format table artefacts/sbom.cdx.json
  5. Demandez maintenant un second avis à un autre moteur, sur exactement le même inventaire. Grype n'entre pas dans votre outillage permanent — il intervient une fois, ici, pour une démonstration précise.
    docker run --rm \
      -v "$(pwd)":/src -w /src \
      -v grype-db:/root/.cache/grype \
      anchore/grype:v0.79.2 \
      sbom:/src/artefacts/sbom.cdx.json -o table
  6. Produisez la sortie JSON de Grype, puis croisez les deux moteurs par script.
    docker run --rm \
      -v "$(pwd)":/src -w /src \
      -v grype-db:/root/.cache/grype \
      anchore/grype:v0.79.2 \
      sbom:/src/artefacts/sbom.cdx.json -o json \
      > artefacts/grype-2026-08-07.json
    
    python3 - <<'PY'
    import json
    
    trivy = set()
    d = json.load(open("artefacts/trivy-fs-2026-08-07.json"))
    for res in d.get("Results", []):
        for v in res.get("Vulnerabilities", []) or []:
            trivy.add((v["PkgName"].lower(), v["VulnerabilityID"]))
    
    grype = set()
    g = json.load(open("artefacts/grype-2026-08-07.json"))
    for m in g.get("matches", []):
        grype.add((m["artifact"]["name"].lower(), m["vulnerability"]["id"]))
    
    print(f"Trivy   : {len(trivy)} couples paquet/CVE")
    print(f"Grype   : {len(grype)} couples paquet/CVE")
    print(f"Communs : {len(trivy & grype)}")
    print()
    print("-- vus par Trivy seul --")
    for p, c in sorted(trivy - grype):
        print(f"   {p:16} {c}")
    print("-- vus par Grype seul --")
    for p, c in sorted(grype - trivy):
        print(f"   {p:16} {c}")
    PY
  7. Regardez les trois nombres : vus par Trivy seul, vus par Grype seul, communs. Le désaccord n'est pas un défaut. Choisissez une CVE remontée par un seul des deux, allez lire son advisory, et écrivez dans votre rapport pourquoi les deux positions se défendent — intervalle de versions affectées interprété différemment, source primaire distincte, ou correctif rétroporté par une distribution.
  8. Écrivez enfin la phrase qui va dans tous vos rapports jusqu'à la soutenance, et remplacez les valeurs par les vôtres : « Aucune vulnérabilité HIGH ou CRITICAL corrigeable n'est connue de Trivy 0.53.0 sur le périmètre analysé au 7 août 2026. » Notez ce qu'elle ne dit pas : elle ne dit pas que l'application est sûre.
Questions de réflexion — à préparer par écrit
  • Combien de composants dans votre SBOM pour combien de lignes déclarées ? Que feriez-vous de ce chiffre dans une revue d'architecture ?
  • Trivy et Grype divergent. Lequel retenez-vous pour la porte du pipeline, et quel rôle donnez-vous à l'autre ? Justifiez en une décision, pas en une préférence.
  • Votre SBOM ne contient aucune licence exploitable en l'état. En quoi cela deviendrait-il un problème si devsecops-lab était un produit vendu ?
Pour vous repérer

Le point qui fait la valeur du TP : attendez-vous à un désaccord. Deux moteurs sur le même SBOM ne rendent pas la même liste, et ce n'est pas un bug. Ils ne consultent pas les mêmes sources, ne les fusionnent pas de la même façon, et ne tranchent pas identiquement les intervalles de versions affectées.

Ce que cela vous apprend, et que vous garderez : « zéro vulnérabilité » est une propriété de l'outil, pas de l'application. La formulation juste est « zéro vulnérabilité connue de Trivy 0.53.0 au 7 août 2026 ». C'est cette phrase-là qui va dans un rapport.

Si le tag v0.79.2 de Grype n'existe plus dans le registre, prenez la dernière v0.79.x disponible et notez la version exacte dans votre rapport : un second avis n'a de valeur que si l'on sait de quel moteur il provient.

✓ terminé
Travaux pratiques

TP 8 — Exploitabilité réelle et correction de VULN-10

Jour 2 · SCA — la surface qu'on hérite · 1 h 15

Prouver qu'une CVE de dépendance est réellement exploitable, décider de chaque CVE par l'atteignabilité, corriger VULN-10 en montant les versions, et poser un fichier d'exceptions daté.

Périmètre : votre poste, et lui seul

Le script de démonstration exécute une charge utile qui lance une commande système. Il tourne sur votre machine, dans votre dépôt, contre du code que vous possédez. C'est exactement le cadre du M1 : localhost uniquement, jamais un service distant, même « pour vérifier ».

Ce que vous prouvez ici, c'est que VULN-10 (PyYAML à CVE) et VULN-07 (le yaml.load de votre code) sont la même histoire vue de deux côtés : une dépendance vulnérable n'est un risque que parce que votre code l'appelle sur une entrée hostile.

  1. Écrivez le script de démonstration dans demo/pyyaml-poc.py et lancez-le. Il montre côte à côte l'exécution de code par yaml.load et le refus par yaml.safe_load.
    mkdir -p demo
    # Collez le contenu ci-dessous dans demo/pyyaml-poc.py, puis :
    python3 demo/pyyaml-poc.py
  2. Voici le script. Recopiez-le tel quel.
    # demo/pyyaml-poc.py — a lancer UNIQUEMENT sur votre poste, jamais ailleurs.
    # Demontre que yaml.load avec un Loader complet execute du code arbitraire.
    # C'est la CVE de PyYAML 5.3.1 (FullLoader contournable) rendue concrete.
    import subprocess
    import sys
    
    import yaml
    
    # Charge utile : un tag !!python/object/apply qui appelle une commande shell.
    CHARGE = """!!python/object/apply:subprocess.check_output
    - ["echo", "code-execute-par-la-deserialisation"]
    """
    
    
    def sans_reseau_via_endpoint():
        """Ce que ferait un attaquant contre /config de devsecops-lab."""
        print(">>> yaml.load(Loader complet) sur une charge hostile :")
        resultat = yaml.load(CHARGE, Loader=yaml.Loader)
        print("    resultat :", resultat)
        print(">>> la commande a bien ete executee cote serveur.")
    
    
    def defense():
        print(">>> yaml.safe_load sur la meme charge :")
        try:
            yaml.safe_load(CHARGE)
        except yaml.YAMLError as e:
            print("    refuse :", type(e).__name__)
    
    
    if __name__ == "__main__":
        sans_reseau_via_endpoint()
        defense()
  3. Arrêtez-vous sur ce que vous venez de voir : une chaîne de caractères, désérialisée, a exécuté une commande. C'est la jonction exacte entre VULN-10 (la version de PyYAML est vulnérable) et VULN-07 (votre /config appelle yaml.load sur le corps de la requête). L'une sans l'autre serait inoffensive.
  4. Établissez l'atteignabilité, à la main, pour les paquets remontés en HIGH et CRITICAL. Ce grep est grossier — c'est le geste que les plateformes commerciales facturent sous le nom de reachability. Pour chaque paquet : la bibliothèque est-elle seulement importée ?
    cd ~/devsecops-lab
    
    # Pour chaque paquet remonte HIGH+, la fonction vulnerable est-elle importee ?
    # grep grossier, volontairement : c'est le geste manuel que les plateformes vendent.
    for pkg in werkzeug jinja2 pyyaml requests urllib3; do
      echo "=== $pkg ==="
      grep -rin --include='*.py' "import $pkg\|from $pkg" . || echo "   jamais importe"
    done
  5. Écrivez maintenant, dans votre rapport, un verdict par CVE HIGH+ : corriger, compenser ou accepter, avec la raison. L'atteignabilité est votre argument principal : une CVE dans une fonction jamais importée descend dans la file ; une CVE sur un chemin emprunté à chaque requête monte. Interdiction de répondre « je monte tout » : ce n'est pas un tri.
  6. Corrigez VULN-10 pour de bon : branche dédiée, montée de version, reconstruction, rescan bloquant. Attendez-vous à ce que l'application casse — c'est la partie instructive.
    git switch main
    git switch -c fix/vuln-10-deps
    
    # Remplacez requirements.txt par les versions corrigees (etape suivante),
    # puis reconstruisez et rescannez.
    docker build -t devsecops-lab:dev .
    
    docker run --rm \
      -v "$(pwd)":/src -w /src \
      -v trivy-cache:/root/.cache/trivy \
      aquasec/trivy:0.53.0 \
      fs --scanners vuln --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 .
    echo "code de sortie apres montee de version : $?" 
  7. Voici les versions corrigées de requirements.txt. Remplacez le contenu, puis relancez la construction et le scan de l'étape précédente. Si app.py ne démarre plus, lisez la trace : Flask 3 et Werkzeug 3 déplacent certains imports, corrigez-les une erreur à la fois.
    Flask==3.0.3
    Werkzeug==3.0.3
    Jinja2==3.1.4
    PyYAML==6.0.1
    requests==2.32.3
    urllib3==2.2.2
  8. Pour ce qui reste — les CVE non corrigeables (unfixed) ou dont la correction casserait la compatibilité — posez un fichier d'exceptions daté. Chaque entrée porte une expired_at : sans elle, l'exception n'existe pas.
    # .trivyignore.yaml — exceptions DATEES. Sans expired_at, pas d'exception.
    # Chaque entree dit : quelle CVE, pourquoi on l'accepte, jusqu'a quand.
    vulnerabilities:
      - id: CVE-EXEMPLE-0000
        # Paquet non atteignable : la fonction affectee n'est jamais importee
        # (verifie le 2026-08-07, voir rapports/sca-devsecops-lab.md section 4).
        # Reexamen impose a la date ci-dessous : ensuite, le pipeline redevient rouge.
        statement: "non atteignable sur ce deploiement ; correctif casse la compat 2.x"
        expired_at: 2026-11-07
  9. Relancez une dernière fois le scan bloquant avec le fichier d'exceptions en place. Le code de sortie doit être 0, et vous devez pouvoir expliquer, pour chaque ligne du .trivyignore.yaml, pourquoi elle est là et quand elle expire. Une exception que vous ne savez pas justifier est une exception à retirer.
  10. Committez la correction et le fichier d'exceptions séparément — deux gestes, deux messages.
    git add requirements.txt
    git commit -m "fix(VULN-10): montee des dependances hors versions a CVE"
    git add .trivyignore.yaml demo/pyyaml-poc.py
    git commit -m "docs(sca): exceptions datees et POC de deserialisation"
Questions de réflexion — à préparer par écrit
  • Vous avez prouvé l'exécution de code par yaml.load. En quoi cette preuve change-t-elle la priorité de VULN-07 par rapport à ce qu'en disait le SAST seul hier ?
  • La montée de Flask a-t-elle cassé quelque chose ? Si oui, quoi, et comment l'avez-vous diagnostiqué ? Si non, qu'est-ce qui aurait pu casser ?
  • Votre .trivyignore.yaml a une échéance. Que se passe-t-il exactement le lendemain de cette date, et pourquoi est-ce une bonne chose ?
Pour vous repérer

La démonstration de la charge utile YAML se fait sur votre poste, jamais ailleurs — c'est la règle de droit du M1, elle ne s'assouplit pas parce que le code tient en dix lignes. Le script s'exécute en local, dans votre dépôt, point.

La montée de version est le geste le plus risqué de la semaine, et c'est voulu : passer Flask de 2.0 à 3.0 peut casser votre application. C'est exactement pour cela qu'on le fait sur une branche dédiée, avec un rebuild et un test derrière. Si l'application ne démarre plus, ce n'est pas un échec du TP : c'est la leçon du TP. Vous lisez l'erreur, vous ajustez, vous recommencez.

Test d'auto-vérification : si vous savez dire, pour chacune des CVE HIGH+ restantes, laquelle vous corrigez, laquelle vous compensez et laquelle vous acceptez — et pourquoi — la journée est acquise. Un « je monte tout » n'est pas une réponse, c'est une absence de tri.

✓ terminé
Travaux pratiques

TP 9 — Porte SCA au pipeline et veille Dependabot

Jour 2 · SCA — la surface qu'on hérite · 45 min

Ajouter au pipeline un job qui produit le SBOM et bloque sur les CVE corrigeables, et activer une surveillance automatique des mises à jour de sécurité.

Ce que vous ajoutez au pipeline existant

Votre ci.yml porte déjà secrets (Gitleaks, M1) et sast (Semgrep, hier). Vous ajoutez un troisième job, sca, au même niveau. Demain lundi, un quatrième — dast — viendra le compléter, et vous fixerez les seuils des quatre ensemble.

Ne touchez pas aux deux jobs existants. Ajoutez le nouveau, indenté exactement comme eux sous jobs:.

  1. Reprenez la branche de correction et ajoutez le job sca à .github/workflows/ci.yml, sous jobs:, au même niveau que les deux autres. Il fait trois choses : produire le SBOM, le publier comme artefact daté du commit, puis bloquer sur les CVE HIGH+ corrigeables.
      sca:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - name: sbom cyclonedx
            run: |
              docker run --rm -v "$PWD:/src" -w /src aquasec/trivy:0.53.0 \
                fs --format cyclonedx --output /src/sbom.cdx.json .
          - name: publier le sbom
            uses: actions/upload-artifact@v4
            with:
              name: sbom-cyclonedx
              path: sbom.cdx.json
          - name: scan bloquant des dependances
            run: |
              docker run --rm -v "$PWD:/src" -w /src aquasec/trivy:0.53.0 \
                fs --scanners vuln --severity HIGH,CRITICAL --ignore-unfixed \
                   --exit-code 1 .
  2. Lisez la dernière étape du job avant de committer. Les deux filtres --severity HIGH,CRITICAL et --ignore-unfixed sont vos deux décisions de seuil : vous ne bloquez ni sur les Medium ni sur l'incorrigeable. Sachez les défendre — ce sont exactement les questions de lundi.
  3. Activez la veille : créez .github/dependabot.yml. Dès qu'un correctif de sécurité existe pour l'une de vos dépendances épinglées, Dependabot ouvrira une PR — que votre pipeline testera avant toute fusion.
    # .github/dependabot.yml — surveillance des mises a jour de securite.
    # Dependabot ouvre une PR des qu'un correctif de securite existe pour une
    # dependance epinglee : c'est le complement obligatoire de l'epinglage.
    version: 2
    updates:
      - package-ecosystem: pip
        directory: /
        schedule:
          interval: weekly
        open-pull-requests-limit: 5
        commit-message:
          prefix: "fix(deps)"
        labels:
          - securite
          - dependances
  4. Committez, poussez, et regardez l'exécution du pipeline dans l'onglet Actions. Le job sca doit maintenant apparaître à côté de secrets et sast.
    git add .github/workflows/ci.yml .github/dependabot.yml
    git commit -m "feat(ci): job sca avec sbom en artefact et veille dependabot"
    git push
  5. Ouvrez l'exécution terminée et vérifiez deux choses : le job sca est vert (vos corrections du TP 8 sont passées), et le SBOM figure bien dans la section Artifacts de l'exécution, nommé d'après le commit. Téléchargez-le : c'est l'inventaire daté de cette version précise.
  6. Notez pour lundi : vous avez trois portes sur quatre. Il manque le DAST, et il manque la discussion d'ensemble des seuils. Écrivez la question que vous vous posez déjà — « pourquoi HIGH et pas MEDIUM ? » — vous y répondrez avec des chiffres.
Questions de réflexion — à préparer par écrit
  • Le SBOM est publié à chaque exécution. Quelle question, posée dans six mois sur une version précise livrée aujourd'hui, devient triviale grâce à cela ?
  • Dependabot ouvre des PR automatiquement. En quoi la règle de commits signés de main complique-t-elle la fusion de ces PR, et comment le résoudriez-vous ?
  • Votre job sca ne bloque pas sur les Medium. Décrivez le scénario dans lequel ce choix vous coûterait cher, et ce qui l'empêche.
Pour vous repérer

Le job publie le SBOM comme artefact de pipeline : c'est la bonne place, trouvée hier. Chaque exécution attache le SBOM au commit qui l'a produit — vous obtenez gratuitement l'inventaire daté de chaque version livrée.

Attention à l'ordre des filtres dans le job bloquant, il encode une décision : --severity HIGH,CRITICAL ne bloque pas sur les Medium (qui se traitent dans la fenêtre de maintenance), --ignore-unfixed ne bloque pas sur l'incorrigeable (qu'on compense au lieu de le subir). Ce sont les mêmes filtres qu'au TP 6 — vous les défendrez lundi, chiffres à l'appui.

Dependabot ne fonctionne que sur un dépôt GitHub : sans réseau, le fichier est correct mais rien ne s'ouvrira. Ce n'est pas une panne, c'est le repli hors-ligne du M1 qui prend le relais pour tout le reste.

✓ terminé
Travaux pratiques

TP 10 — Consigner la passe SCA

Jour 2 · SCA — la surface qu'on hérite · 45 min

Produire le rapport de composition, rejouable et daté, et le faire entrer dans le dépôt par une demande de fusion.

Le plan attendu

Cinq sections, alignées sur le rapport SAST d'hier : 1. périmètre et outillage (Trivy 0.53.0, Grype, versions, date) · 2. inventaire et quatre décomptes · 3. désaccord entre moteurs, une CVE commentée · 4. atteignabilité et décision par CVE HIGH+ · 5. exceptions datées et plan de mise à jour.

C'est la deuxième fois que vous écrivez ce plan. La troisième sera le rapport de pentest du M5 — vous le connaîtrez par cœur.

  1. Ouvrez rapports/sca-devsecops-lab.md et remplissez les sections 1 et 2 avec les versions exactes et les quatre décomptes du TP 6. Si un chiffre vous manque, relancez la commande — n'estimez pas.
  2. Complétez la section 3 avec les trois nombres du croisement Trivy/Grype et la CVE divergente que vous avez commentée. Rappelez la formule prudente : « connu de tel moteur, à telle date ».
  3. Rédigez la section 4, celle qui a le plus de valeur : pour chaque CVE HIGH+, le verdict (corriger / compenser / accepter) et l'argument d'atteignabilité qui le fonde. C'est ce raisonnement, et non la liste des CVE, qui distingue votre rapport d'une sortie d'outil.
  4. Complétez la section 5 : le contenu de .trivyignore.yaml avec la justification et l'échéance de chaque exception, et la mention de Dependabot comme mécanisme de veille. Une exception sans échéance dans ce document est une erreur à corriger avant de continuer.
  5. Committez le rapport et poussez la branche.
    git add rapports/sca-devsecops-lab.md
    git commit -m "docs(sca): rapport de composition du 7 aout"
    git push
  6. Ouvrez — ou mettez à jour — la demande de fusion, mais ne fusionnez pas. Le job sca est vert, mais sast reste rouge : VULN-03 et VULN-07 sont encore dans le code. La fusion consolidée aura lieu lundi, quand les quatre portes seront vertes ensemble.
    gh pr create --base main --head fix/vuln-10-deps \
      --title "SCA : correction VULN-10 et rapport de composition" \
      --body "Dependances montees hors versions a CVE, exceptions datees,
    SBOM en artefact, veille Dependabot. Job sca au VERT.
    Ne pas fusionner : sast reste rouge (VULN-03, VULN-07 a corriger lundi)."
  7. Regardez la PR : secrets vert, sca vert, sast rouge. La protection de main refuse la fusion tant qu'une vérification échoue — et c'est exactement son rôle. Vous fermez la journée SCA avec un dépôt aux dépendances propres, tracées et surveillées, et une porte de plus au pipeline. Lundi, le DAST rejoint la chaîne et vous corrigez les dernières failles de code.
Questions de réflexion — à préparer par écrit
  • Votre rapport SCA et votre rapport SAST suivent le même plan. Qu'est-ce que cette constance apporte à quelqu'un qui doit lire les deux à la suite ?
  • Le job sca est vert mais vous ne pouvez pas fusionner. Formulez la règle générale que vous en tirez sur ce qui peut entrer dans main.
✓ terminé
Point de contrôle

M2 · J2 — CVE tranchée par l'atteignabilité, VULN-10 corrigée, SBOM daté

Jour 2 · SCA — la surface qu'on hérite · 30 min

Vérifiez avant de continuer
Ce que vous présentez au point de 16 h 30

Votre encadrant vous fera rejouer le POC en direct et vous demandera de défendre le tri d'une CVE au hasard : atteignable ou pas, et qu'en avez-vous fait ? Puis il ouvrira votre .trivyignore.yaml et pointera une ligne : que couvre-t-elle, et quand expire-t-elle ?

Préparez le dépôt, Docker démarré, le volume de cache Trivy en place pour que le rescan soit instantané.

✓ terminé
Leçon

DAST : l'application attaquée pendant qu'elle tourne

Jour 3 · DAST et intégration des trois au pipeline · 25 min

Le SAST a lu votre code sans l'exécuter. La SCA a lu vos dépendances sans les exécuter. Le DAST fait l'inverse exact des deux : il ne lit rien, il attaque l'application pendant qu'elle tourne et regarde comment elle répond. Il ne sait pas où est le code, ni quelles bibliothèques vous utilisez. Il sait envoyer une requête et lire une réponse — comme un attaquant réel.

Trois temps, et une frontière de droit au milieu

1. Exploration spider : trouve les URL 2. Passif observe, n’attaque pas 3. Actif envoie des charges cible qui tourne sans risque en-têtes, cookies, bannières, VULN-09 modifie l’état XSS prouvé (VULN-02), injections — sur SA cible
Un scan DAST se déroule en trois temps. Le passif est sans risque et peut tourner partout ; l’actif envoie de vraies charges — d’où la règle de droit du premier jour.
  • L'exploration (spider) parcourt l'application et dresse la liste des URL qu'elle atteint. Tout le reste du scan se limite à cette liste : ce qui n'est pas découvert n'est pas testé.
  • Le scan passif observe les réponses sans jamais rien attaquer : en-têtes de sécurité absents, cookies sans attribut, bannières de version, message de débogage. Il est sans risque et peut tourner partout, à chaque build.
  • Le scan actif fabrique et envoie de vraies charges d'attaque — apostrophes d'injection, balises de script, chemins traversants — et observe si elles prennent. Il modifie l'état de la cible, prend du temps, et ne se lance que sur une cible qu'on a le droit d'attaquer.
La règle de droit, revue depuis l'outil

Le scan actif est une attaque, techniquement et juridiquement. Vous ne le lancez que sur vos deux applications locales — devsecops-lab et Juice Shop — qui tournent sur votre poste et que vous avez déployées vous-même. C'est la règle du premier jour, inchangée : localhost uniquement, jamais Internet, jamais une ressource de Caplogy, jamais le système d'un tiers.

Ce que le DAST trouve, et ce qu'il ne verra jamais

C'est le miroir exact du tableau du premier jour. Le DAST voit tout ce qui laisse une trace dans une réponse HTTP, et rien de ce qui n'en laisse pas.

VulnérabilitéDAST la trouve ?Pourquoi
VULN-02 XSS réfléchioui, avec preuvela charge injectée revient telle quelle dans la page — c'est visible
VULN-09 debug, en-têtesoui, en passifla bannière et les en-têtes manquants sont dans chaque réponse
VULN-08 SSRFconditionnelil faut un serveur d'écoute externe pour confirmer que la requête est partie — M5
VULN-05 secret en durnonun secret dans le code ne produit aucune réponse observable
VULN-06 hachage md5nonl'algorithme de hachage ne se lit pas de l'extérieur
VULN-10 dépendances à CVEnonla version d'une bibliothèque interne n'est pas dans la réponse
VULN-04 IDORnonil faudrait deux comptes et savoir que la fiche 2 n'est pas la vôtre — c'est humain

Mettez ce tableau en regard de celui du premier jour. Le SAST voyait VULN-05, VULN-06 et manquait la preuve du XSS ; le DAST prouve le XSS et ne voit ni le secret ni le md5. Aucun des deux n'est meilleur. Ils sont complémentaires, et leur intersection est vide sur la moitié des failles.

L'exploration ne suffit pas : le scan piloté par spécification

Le spider ne découvre que ce qui est relié. Une API n'a pas de liens : ses routes existent, mais aucune page n'y pointe. Pour les tester, on donne à l'outil la carte — une spécification OpenAPI — et il attaque chaque route décrite. C'est le zap-api-scan que vous emploierez tout à l'heure, et c'est aussi ce qui rendra le DAST utilisable dans le pipeline : un scan qui ne dépend pas de ce que le spider a bien voulu trouver est un scan reproductible.

ZAP, et les autres outils du même poste

OWASP ZAP est le DAST de ce parcours : libre, scriptable, intégrable au pipeline. Vous croiserez aussi mitmproxy — un proxy d'inspection en ligne de commande, pour lire et modifier une requête à la volée — et Burp Community, la version gratuite de Burp : excellente pour le travail manuel (Repeater, interception), mais son Intruder est bridé et elle ne s'automatise pas. Le programme citait Burp Suite Pro, payant : nous le remplaçons par ce trio libre — ZAP pour l'automatisation, mitmproxy pour l'inspection, Burp Community pour le geste manuel.

Pour vous repérer

La confusion à cet endroit : croire que le DAST « teste tout ». Il ne teste que ce qu'il trouve, et il ne trouve que ce que son explorateur (le spider) a découvert. Un endpoint sans lien vers lui, une page derrière un formulaire d'authentification, une route d'API non déclarée : invisibles. C'est pour cela que le scan d'API se pilote par une spécification, et non par exploration.

Second point, la règle de droit rappelée sous une forme technique : le scan passif ne fait qu'observer, vous pouvez le lancer sans conséquence ; le scan actif envoie de vraies charges d'attaque et modifie l'état de la cible. Vous ne le lancez donc que sur vos deux applications locales, jamais ailleurs — c'est la même frontière que le premier jour du parcours, vue depuis l'outil.

✓ terminé
Quiz

Quiz — DAST et complémentarité

Jour 3 · DAST et intégration des trois au pipeline · 10 min

1. Quelle est la différence entre le scan passif et le scan actif d'un DAST ?

2. Vous lancez un scan ZAP par exploration et /config n'est jamais testé. Pourquoi ?

3. Le SAST signale VULN-05 (secret en dur) mais pas le DAST. Pourquoi le DAST est-il muet ici ?

4. Vous voulez lancer un scan actif ZAP. Sur quelle cible est-ce légitime ?

5. Une équipe n'a mis en place que du DAST. Quelle affirmation est correcte ?

✓ terminé
Travaux pratiques

TP 11 — ZAP en passif sur vos deux cibles

Jour 3 · DAST et intégration des trois au pipeline · 1 h

Lancer un scan passif sur devsecops-lab puis sur Juice Shop, et distinguer ce que l'observation seule révèle — sur une application de 130 lignes et sur une application riche.

Les deux cibles tournent en local

devsecops-lab sur le port 5000 (votre appli), Juice Shop sur le port 3000 (la cible boîte noire du M5). Toutes deux sont sur dso-net et sur votre poste uniquement. ZAP les joint par leur nom de conteneur.

Le passif ne modifie rien : vous pouvez le lancer et le relancer sans conséquence. C'est le seul mode dont c'est vrai.

  1. Vérifiez que vos deux cibles répondent, et relancez celle qui dort. Ces images ont été construites au M1 ; on ne les recrée pas, on les redémarre.
    cd ~/devsecops-lab
    docker ps --filter name=devsecops-lab --filter name=juice-shop
    
    # Si l'un des deux ne tourne pas, relancez-le (images du M1) :
    docker start devsecops-lab 2>/dev/null || \
      docker run -d --name devsecops-lab --network dso-net -p 5000:5000 devsecops-lab:dev
    docker start juice-shop 2>/dev/null || \
      docker run -d --name juice-shop --network dso-net -p 3000:3000 \
        bkimminich/juice-shop:v17.3.0
    
    # Verifiez que les deux repondent depuis votre poste :
    curl -sf -o /dev/null -w "devsecops-lab %{http_code}\n" http://localhost:5000/
    curl -sf -o /dev/null -w "juice-shop   %{http_code}\n" http://localhost:3000/
  2. Lancez le scan passif sur votre application. ZAP explore, puis observe les réponses sans jamais attaquer. Le rapport HTML atterrit dans artefacts/.
    mkdir -p artefacts
    docker run --rm --network dso-net \
      -v "$(pwd)/artefacts":/zap/wrk:rw \
      ghcr.io/zaproxy/zaproxy:2.15.0 \
      zap-baseline.py -t http://devsecops-lab:5000 \
        -r zap-baseline-devsecops-2026-08-10.html \
        -J zap-baseline-devsecops.json
    echo "code de sortie ZAP : $?" 
  3. Ouvrez artefacts/zap-baseline-devsecops-2026-08-10.html dans votre navigateur. Repérez les alertes liées à VULN-09 : en-têtes de sécurité manquants (Content-Security-Policy, X-Content-Type-Options), et surtout la trace du mode débogage. C'est la même faille que Bandit signalait dans le code et que Trivy verra dans l'image au M3 — trois outils, trois angles, une seule cause.
  4. Lancez maintenant le même scan passif sur Juice Shop. Même commande, autre cible.
    docker run --rm --network dso-net \
      -v "$(pwd)/artefacts":/zap/wrk:rw \
      ghcr.io/zaproxy/zaproxy:2.15.0 \
      zap-baseline.py -t http://juice-shop:3000 \
        -r zap-baseline-juice-2026-08-10.html
  5. Comparez les deux rapports côte à côte. Votre application de 130 lignes déclenche une poignée d'alertes ; Juice Shop en déclenche beaucoup plus, parce qu'elle a une vraie surface : cookies applicatifs, scripts, stockage côté client, en-têtes multiples. Le passif n'a pourtant rien attaqué dans les deux cas — toute cette matière est simplement lue dans les réponses.
  6. Notez dans votre rapport DAST naissant (rapports/dast-devsecops-lab.md) deux choses : l'alerte de devsecops-lab qui correspond à VULN-09, et une phrase sur l'écart de surface entre les deux cibles. Le fond de Juice Shop — l'exploitation réelle — est réservé au M5 ; aujourd'hui, elle sert de point de comparaison.
Questions de réflexion — à préparer par écrit
  • Le scan passif n'a envoyé aucune charge d'attaque. Pourquoi peut-on alors le faire tourner en production, contrairement au scan actif ?
  • VULN-09 est vue par Bandit (code), par ZAP passif (réponse) et le sera par Trivy (image). Qu'apporte chacun des trois que les deux autres n'apportent pas ?
  • Juice Shop déclenche bien plus d'alertes passives que votre application. Cela signifie-t-il qu'elle est moins sûre ? Que compare-t-on réellement ?
Pour vous repérer

Le code de sortie de zap-baseline.py n'est pas 0 : c'est voulu. Il vaut le nombre de règles en avertissement, et une application vulnérable en déclenche forcément. Un code non nul ici n'est pas une panne — c'est le résultat. Vous apprendrez à en faire une porte au TP 13, en décidant quelles règles doivent vraiment bloquer.

Le rapport HTML se dépose dans artefacts/ parce que vous avez monté ce dossier sur /zap/wrk. Si le fichier n'apparaît pas, le montage est le premier suspect — ZAP écrit dans /zap/wrk, et nulle part ailleurs.

ZAP se joint aux cibles par leur nom sur dso-net (http://devsecops-lab:5000), pas par localhost : depuis l'intérieur du conteneur ZAP, localhost désigne ZAP lui-même.

✓ terminé
Travaux pratiques

TP 12 — Scan actif, scan d'API, inspection du trafic

Jour 3 · DAST et intégration des trois au pipeline · 1 h 10

Passer en actif sur devsecops-lab pour obtenir la preuve de VULN-02, scanner l'API par sa spécification, inspecter une requête au proxy, et vérifier la prédiction du premier jour.

Actif = attaque : votre cible locale, et elle seule

À partir de cette étape, ZAP envoie de vraies charges d'attaque. La règle du premier jour s'applique sans réserve : devsecops-lab et Juice Shop, sur votre poste, rien d'autre. Ce n'est pas une précaution de style, c'est la loi.

  1. Lancez le scan actif sur votre application. Il explore, puis attaque chaque point d'entrée trouvé. Comptez quelques minutes, et laissez-le aller au bout.
    docker run --rm --network dso-net \
      -v "$(pwd)/artefacts":/zap/wrk:rw \
      ghcr.io/zaproxy/zaproxy:2.15.0 \
      zap-full-scan.py -t http://devsecops-lab:5000 \
        -r zap-full-devsecops-2026-08-10.html \
        -J zap-full-devsecops.json
    echo "code de sortie ZAP : $?" 
  2. Ouvrez le rapport et trouvez Cross Site Scripting (Reflected) sur /search. Dépliez l'alerte : ZAP vous donne la charge exacte qu'il a envoyée et l'extrait de réponse où elle réapparaît. C'est VULN-02, et c'est une preuve, pas une suspicion — la différence exacte avec ce que votre règle Semgrep produisait hier. Vérifiez aussi, en creux, que la SQLi n'apparaît plus : VULN-01 a été corrigée au M1, et l'actif le confirme.
  3. Écrivez la carte de l'API dans openapi.yaml, à la racine du dépôt. Sans elle, /ping, /config et /login resteraient invisibles : aucune page ne pointe vers eux.
    # openapi.yaml — carte des routes de devsecops-lab pour zap-api-scan.
    # Sans elle, le spider ne trouverait ni /ping ni /config (aucune page n'y mene).
    openapi: 3.0.3
    info:
      title: devsecops-lab
      version: "1.0"
    servers:
      - url: http://devsecops-lab:5000
    paths:
      /search:
        get:
          parameters:
            - name: q
              in: query
              required: true
              schema: { type: string }
          responses:
            "200": { description: resultats }
      /ping:
        get:
          parameters:
            - name: host
              in: query
              schema: { type: string }
          responses:
            "200": { description: sortie de ping }
      /login:
        post:
          requestBody:
            content:
              application/x-www-form-urlencoded:
                schema:
                  type: object
                  properties:
                    user: { type: string }
                    pwd: { type: string }
          responses:
            "200": { description: authentifie }
            "401": { description: refuse }
  4. Lancez le scan piloté par la spécification. ZAP lit la carte, puis attaque chaque route décrite — y compris celles que le spider ne trouvait pas.
    docker run --rm --network dso-net \
      -v "$(pwd)":/zap/wrk:rw \
      ghcr.io/zaproxy/zaproxy:2.15.0 \
      zap-api-scan.py -t /zap/wrk/openapi.yaml -f openapi \
        -r artefacts/zap-api-devsecops-2026-08-10.html
  5. Comparez la couverture des deux scans actifs : le scan par exploration a testé ce qui était relié, le scan d'API a testé /ping et /login en plus. C'est la démonstration que dans le pipeline, on pilotera par spécification — c'est reproductible, là où l'exploration dépend de ce que le spider a bien voulu trouver ce jour-là.
  6. Inspectez enfin une requête au proxy. mitmproxy se place entre votre client et la cible et journalise tout en clair : la charge XSS que vous envoyez, et la réponse qui la renvoie.
    # Un proxy d'inspection, le temps d'une requete. On lit le trafic API en clair.
    docker run --rm -d --name dso-mitm --network dso-net -p 8080:8080 \
      mitmproxy/mitmproxy:10.3.1 mitmdump
    
    # Envoyez une requete A TRAVERS le proxy : la charge XSS traverse mitmproxy,
    # qui la journalise avec la reponse.
    curl -s -x http://localhost:8080 \
      "http://devsecops-lab:5000/search?q=<script>alert(1)</script>" -o /dev/null
    
    # Lisez ce que le proxy a intercepte, requete ET reponse :
    docker logs dso-mitm | tail -n 30
    
    # On coupe le proxy : c'etait une inspection, pas un service.
    docker rm -f dso-mitm
  7. Regardez ce que mitmproxy a capturé : votre charge <script> dans l'URL, et la réponse HTML qui la contient telle quelle. C'est le même XSS, vu au niveau du fil. Ce geste — intercepter, lire, modifier une requête — est exactement ce que vous feriez à la main dans Burp Community, en interface graphique. ZAP automatise, mitmproxy et Burp inspectent : trois outils, une même famille.
  8. Refermez la journée technique sur la vérification qui compte : reprenez le tableau de prédiction du premier jour et cochez chaque case avec ce que vous avez maintenant mesuré. Le DAST a-t-il trouvé VULN-02 ? Oui, avec preuve. VULN-05, VULN-06, VULN-10 ? Non, et vous savez dire pourquoi. VULN-04 ? Aucun des trois. Notez tout écart entre votre prédiction et la réalité : c'est là que vous avez appris quelque chose.
Questions de réflexion — à préparer par écrit
  • Le SAST « soupçonnait » le XSS, le DAST l'a « prouvé ». Rédigez les deux formulations telles qu'elles apparaîtraient dans un rapport, et dites laquelle convainc un développeur sceptique.
  • Le scan d'API a couvert des routes que l'exploration manquait. Quel est le prix à payer pour cette couverture, et qui doit le payer ?
  • Sur les dix vulnérabilités, laquelle n'est trouvée par aucune des trois familles ? Qu'est-ce que cela vous dit sur la place de l'outillage dans une démarche de sécurité ?
Pour vous repérer

Le scan actif envoie de vraies charges — c'est une attaque, sur votre cible locale et sur elle seule. Il est aussi bien plus long que le passif : quelques minutes sur une petite application, laissez-le finir.

Ce que vous cherchez précisément dans le rapport actif : Cross Site Scripting (Reflected) sur /search. C'est VULN-02, et le rapport vous en donne la preuve — la charge exacte envoyée et la réponse qui la renvoie. C'est ce que le SAST ne pouvait pas fournir : lui soupçonnait, ZAP démontre.

Test d'auto-vérification : si, à la fin du TP, vous pouvez cocher ligne par ligne le tableau de prédiction du premier jour — trouvée / pas trouvée, et pourquoi — la journée a atteint son but. Une seule case qui vous surprend encore mérite qu'on s'y arrête.

✓ terminé
Leçon

Trois portes, trois seuils : bloquer juste ce qu'il faut

Jour 3 · DAST et intégration des trois au pipeline · 25 min

Vous avez maintenant quatre portes possibles : secrets, sast, sca, dast. La question n'est plus comment les brancher — vous savez le faire — mais à quel seuil chacune doit refuser une fusion. C'est une décision d'architecture, et c'est celle qu'on vous demandera de défendre.

Le principe qui gouverne tous les seuils

Un seuil se juge à ce qu'il coûte quand il se trompe

Une porte trop stricte génère des blocages injustifiés. L'équipe, sous pression de livraison, finit par la désactiver — ou par fusionner en la contournant. Une porte désactivée est pire qu'une absence de porte : elle laisse croire qu'un contrôle existe. Une porte trop laxiste, à l'inverse, laisse tout passer et ne coûte rien à personne, donc ne protège personne. Le bon seuil est le plus haut qu'on puisse tenir dans la durée.

Gravité et confiance : deux axes, pas un

On ne bloque pas sur la seule gravité. On bloque sur le croisement de trois propriétés :

  • la gravité — combien coûte la faille si elle est exploitée ;
  • la confiance — la probabilité que l'alerte soit un vrai positif ;
  • la corrigeabilité — existe-t-il un correctif que l'équipe peut appliquer maintenant.

C'est exactement pourquoi votre XSS a voyagé d'un axe à l'autre en trois jours : côté SAST, le motif du gabarit dynamique était grave mais peu sûr (faux positifs sur les gabarits constants), donc en avertissement. Côté DAST, la preuve d'exécution lève le doute : grave et sûr, donc bloquant. La même faille, deux traitements, une seule logique.

secrets gitleaks sast semgrep sca trivy fs dast zap-api-scan merge autorisé zéro fuite ERROR seulement HIGH+, corrigeable alerte FAIL binaire haute confiance --ignore-unfixed API documentée Un seuil se justifie par ce qu’il coûte quand il se trompe bloquer trop → la porte est désactivée en trois semaines · bloquer trop peu → la porte ne sert à rien
Les quatre portes du pipeline à la fin du module, chacune avec son seuil et le motif de ce seuil.

Les quatre portes, leur seuil, et le motif de chaque seuil

PorteSeuil de blocageMotif du seuil
secrets (Gitleaks)toute fuite bloqueun secret est binaire — il fuit ou non ; aucun faux positif ne justifie de laisser passer une clé
sast (Semgrep)gravité ERROR seulementles règles ERROR sont à haute confiance ; les WARNING (dont le XSS) avertissent sans bloquer, pour ne pas noyer l'équipe
sca (Trivy)HIGH+ corrigeableon ne bloque pas une équipe sur une CVE sans correctif — on la compense ; --ignore-unfixed encode ce choix
dast (ZAP)une règle ciblée, à haute confiancel'actif a des faux positifs et modifie l'état ; on échoue sur le XSS réfléchi prouvé, pas sur chaque avertissement passif

Ce qui ne bloque pas ne disparaît pas

Bloquer n'est qu'une des actions possibles. Ce qui ne bloque pas doit encore être tracé : le SARIF remonte dans l'onglet Security, les avertissements restent visibles, les Medium se traitent en fenêtre de maintenance, les exceptions portent une date d'expiration. Une porte n'est pas un mur binaire ; c'est le sommet visible d'un dispositif qui, en dessous, garde trace de tout.

Le seuil n'est pas une constante universelle

Le tableau ci-dessus est celui de devsecops-lab, une application d'entraînement. Une banque bloquerait sur les Medium ; un site vitrine interne se contenterait peut-être des Critical. Le bon réflexe n'est pas de mémoriser des valeurs, c'est de savoir écrire et défendre celles qu'on a choisies pour un contexte donné.

Pour vous repérer

La question qui gouverne tout réglage de seuil n'est pas « quelle est la gravité ? » mais « que coûte cette porte quand elle se trompe ? ». Une porte qui bloque trop est désactivée par l'équipe en trois semaines — et une porte désactivée protège moins que pas de porte, car elle donne l'illusion d'un contrôle. Une porte qui bloque trop peu ne sert à rien. Le bon seuil est celui qu'on peut tenir sans céder.

Retenez la règle qui découle des trois jours : on bloque sur ce qui est à la fois grave, sûr (peu de faux positifs) et corrigeable. Le reste avertit, trace, mais ne bloque pas. C'est pourquoi votre XSS était en avertissement côté SAST — trop de faux positifs — et devient bloquant côté DAST — la preuve lève le doute.

✓ terminé
Travaux pratiques

TP 13 — DAST au pipeline : rouge sur le XSS, puis vert

Jour 3 · DAST et intégration des trois au pipeline · 1 h

Ajouter la quatrième porte au pipeline, la voir échouer sur VULN-02, corriger les dernières failles de code, lever les exceptions du module 1 à leur échéance, et amener les quatre portes au vert ensemble.

La quatrième porte, et la fin des failles de code

Votre pipeline a trois jobs : secrets, sast, sca. Vous ajoutez dast. Puis vous corrigez les quatre failles de code que les analyses ont trouvées et qui bloquaient encore : VULN-02 (XSS, prouvée hier par ZAP), VULN-03 (injection de commande), VULN-06 (md5), VULN-07 (désérialisation).

Ces quatre-là sont exactement celles que vous aviez couvertes, le 5 août, par une exception nosemgrep nommée et datée « échéance 10 août ». Nous y sommes : vous corrigez, puis vous levez les quatre annotations et vous fermez la trace au README et au journal. C'est le second geste, et c'est celui qu'on oublie.

Restent après aujourd'hui : VULN-04 et VULN-08 (IDOR, SSRF — exploitées et corrigées au M5) et VULN-09 (durcissement — M3). Chaque chose à son module.

  1. Reprenez la branche de correction et ajoutez le job dast à .github/workflows/ci.yml, sous jobs:. Il démarre la cible et ZAP sur un même réseau, puis lance le scan d'API en mode bloquant.
      dast:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - name: cible + zap sur un reseau dedie
            run: |
              docker network create dast-net
              docker build -t devsecops-lab:dev .
              docker run -d --name devsecops-lab --network dast-net devsecops-lab:dev
              # attendre que la cible reponde, depuis le meme reseau
              docker run --rm --network dast-net curlimages/curl:8.8.0 \
                sh -c 'for i in $(seq 1 30); do curl -sf http://devsecops-lab:5000/ && exit 0; sleep 2; done; exit 1'
          - name: zap api scan (actif, bloquant sur le XSS)
            run: |
              docker run --rm --network dast-net \
                -v "$PWD:/zap/wrk:rw" \
                ghcr.io/zaproxy/zaproxy:2.15.0 \
                zap-api-scan.py -t /zap/wrk/openapi.yaml -f openapi \
                  -c zap-regles.conf -r zap-dast.html
          - name: publier le rapport zap
            if: always()
            uses: actions/upload-artifact@v4
            with:
              name: rapport-dast
              path: zap-dast.html
  2. Ajoutez le fichier de règles zap-regles.conf à la racine. Attention : les colonnes sont séparées par des tabulations. C'est lui qui promeut le XSS réfléchi en FAIL et fait taire le bruit passif.
    # zap-regles.conf — action par regle pour zap-api-scan.py -c
    # format : <id de regle><TAB>WARN|FAIL|IGNORE<TAB>(commentaire)
    # On echoue le build sur le XSS reflechi, et rien d'autre : c'est la seule
    # regle a haute confiance dont la preuve a ete etablie au TP 12.
    40012	FAIL	(Cross Site Scripting (Reflected) : porte bloquante)
    10015	IGNORE	(Incomplete or No Cache-control : bruit passif, hors porte)
    10096	IGNORE	(Timestamp Disclosure : bruit)
    10027	IGNORE	(Information Disclosure - Suspicious Comments : bruit)
    
  3. Committez, poussez, et regardez le pipeline. Le job dast doit être rouge : VULN-02 est toujours dans le code, ZAP en fournit la preuve, et la règle 40012 échoue le build. C'est la boucle rouge→vert, sur la dernière porte.
    git add .github/workflows/ci.yml zap-regles.conf openapi.yaml
    git commit -m "feat(ci): porte dast (zap-api-scan) bloquante sur le XSS reflechi"
    git push
  4. Corrigez VULN-02 dans app.py. Un gabarit fixe, la valeur passée en variable : Jinja l'échappe automatiquement. C'est le geste que votre règle Semgrep recommandait mot pour mot.
    @app.route("/search")
    def search():
        # F-VULN-02 : gabarit FIXE, la valeur passe en variable et Jinja l'echappe.
        terme = request.args.get("q", "")
        return render_template_string("<h1>Resultats pour {{ terme }}</h1>", terme=terme)
  5. Corrigez du même élan VULN-03 : validez que l'entrée est bien une adresse IP, puis passez une liste d'arguments sans interpréteur de commandes. Ajoutez import ipaddress en tête de fichier.
    @app.route("/ping")
    def ping():
        # F-VULN-03 : on valide que l'entree EST une IP, puis liste d'arguments sans shell.
        hote = request.args.get("host", "127.0.0.1")
        ipaddress.ip_address(hote)  # leve ValueError si ce n'est pas une adresse
        out = subprocess.check_output(["ping", "-c1", hote])
        return out
  6. Puis VULN-06 et VULN-07 : sha-256 à la place de md5, safe_load à la place de load.
    @app.route("/hash")
    def weak_hash():
        # F-VULN-06 : sha-256 a la place de md5 pour un usage de securite.
        return hashlib.sha256(request.args.get("p", "").encode()).hexdigest()
    
    
    @app.route("/config", methods=["POST"])
    def load_config():
        # F-VULN-07 : safe_load ne construit que des types simples, pas d'objets Python.
        return str(yaml.safe_load(request.data))
  7. Levez les quatre exceptions du 5 août. Nous sommes le 10 : c'est l'échéance que vous aviez inscrite, et les quatre failles qu'elles couvraient viennent d'être corrigées. Une exception qui survit à sa cause est une dette cachée — supprimez les annotations et leurs trois lignes de commentaire, puis vérifiez qu'il n'en reste aucune.
    # Supprimez, dans app.py, les blocs :
    #   # nosemgrep: <identifiant>
    #   # VULN-0N — exception acceptee le 2026-08-05, echeance module 2 (2026-08-10).
    #   # Faille pedagogique deliberee. Suivi : README section 5, journal-incidents.
    
    grep -n 'nosemgrep' app.py
    echo "lignes restantes : $(grep -c nosemgrep app.py)"   # attendu : 0
  8. Fermez la trace au même endroit que vous l'aviez ouverte : la section 5 du README et le journal. Une exception se clôt par écrit, pas par oubli.
    # README.md, section 5 : passez les quatre lignes en statut LEVEE, avec la date du jour
    # et le commit de correction. Ne supprimez pas les lignes : l'historique de la dette
    # fait partie du dossier.
    
    # journal-incidents.md, une ligne :
    # | 2026-08-10 | 4 exceptions nosemgrep levees a echeance | VULN-02/03/06/07 corrigees | annotations retirees, README section 5 a jour | 0 |
  9. Relisez ce que vous venez de faire dans l'ordre : les exceptions ont été posées nommées et datées, elles n'ont jamais couvert plus que la règle qu'elles nommaient, elles ont tenu exactement jusqu'à leur échéance, et elles sont levées le jour dit avec la trace de leur fermeture. C'est le cycle complet d'une dette de sécurité gérée. La version dégradée — un nosemgrep nu posé un jeudi soir — donne le même vert au pipeline et aucune de ces garanties.
  10. Vérifiez localement, avant de pousser, que le XSS a disparu — relancez le scan d'API. Le job doit sortir en code 0 : plus de règle 40012.
    docker build -t devsecops-lab:dev .
    docker restart devsecops-lab
    docker run --rm --network dso-net -v "$(pwd)":/zap/wrk:rw \
      ghcr.io/zaproxy/zaproxy:2.15.0 \
      zap-api-scan.py -t /zap/wrk/openapi.yaml -f openapi -c zap-regles.conf
    echo "code de sortie ZAP : $?"
  11. Committez les corrections et poussez. Cette fois, regardez les quatre jobs : secrets, sast, sca, dast — tous verts. C'est la première fois que le pipeline complet passe.
    git add app.py README.md journal-incidents.md
    git commit -m "fix(VULN-02,03,06,07): gabarit fixe, ip validee, sha256, safe_load ;
    exceptions nosemgrep du 5 aout levees a echeance"
    git push
  12. Prenez la mesure de ce que vous venez de faire : une faille signalée par le SAST en avertissement, prouvée par le DAST, corrigée, et désormais gardée par une porte automatique qui échouera le build si elle revient. C'est cela, la sécurité comme code — et c'est la fin du parcours des trois familles.
Questions de réflexion — à préparer par écrit
  • La porte dast ne bloque que sur la règle 40012. Décrivez une faille réelle qui passerait cette porte, et ce qui la rattraperait ailleurs dans le dispositif.
  • Vous avez corrigé VULN-02 par un gabarit fixe plutôt que par un échappement manuel. Pourquoi ce choix est-il plus robuste face au prochain développeur ?
  • Le job dast fait tourner l'application à chaque exécution. Quel est le coût de cette porte comparé aux trois autres, et comment le justifieriez-vous ?
Pour vous repérer

La porte DAST échoue d'abord — c'est le but. zap-api-scan avec -c zap-regles.conf promeut le XSS réfléchi (règle 40012) en FAIL : tant que VULN-02 est dans le code, le job sort en erreur et le pipeline est rouge. C'est la boucle rouge→vert du premier module, appliquée à la dernière porte.

La correction de VULN-02 tient en une ligne : un gabarit fixe avec la valeur passée en variable. Jinja échappe alors automatiquement — c'est exactement ce que votre règle Semgrep recommandait dans son message. Vous corrigez du même geste VULN-03, VULN-06 et VULN-07, que le SAST tenait bloquées depuis jeudi.

Après correction, relancez ZAP en local avant de pousser : inutile d'attendre le pipeline pour savoir si le XSS a disparu. La preuve locale d'abord, la porte ensuite.

✓ terminé
Travaux pratiques

TP 14 — Synthèse des trois familles et fusion consolidée

Jour 3 · DAST et intégration des trois au pipeline · 25 min

Écrire la note qui relie les trois jours, fusionner les corrections sur une chaîne verte, et laisser un dépôt dont chaque porte a un seuil défendable.

Le livrable qui tient tout le module

Une note de deux pages, pas plus. Elle contient : le tableau vulnérabilité × famille (qui trouve quoi, d'après ce que vous avez mesuré), une phrase par angle mort, et le tableau des quatre portes avec leur seuil et le motif de chaque seuil.

C'est ce document que vous présenterez dans dix minutes à la revue de fin de module. Il doit se suffire à lui-même.

  1. Complétez rapports/dast-devsecops-lab.md : d'abord le tableau vulnérabilité × famille, une ligne par VULN, une colonne par famille (SAST / SCA / DAST), rempli avec ce que vous avez constaté sur trois jours — pas avec la prédiction du premier jour. Signalez toute case où la réalité a démenti la prédiction.
  2. Écrivez ensuite, sous le tableau, une phrase par angle mort : VULN-04 (aucune famille), les CVE que le SAST ne voit pas, les secrets et le md5 que le DAST ne voit pas. C'est la démonstration, en trois lignes, qu'aucune famille ne se suffit.
  3. Recopiez enfin le tableau des quatre portessecrets, sast, sca, dast — avec pour chacune son seuil et le motif du seuil. Le motif est ce qui distingue votre note d'une capture d'écran de configuration.
  4. Committez la note, puis fusionnez les corrections sur une chaîne verte. Vérifiez d'abord que les quatre checks passent, puis fusionnez la branche de correction.
    git add rapports/dast-devsecops-lab.md
    git commit -m "docs(dast): synthese des trois familles et seuils du pipeline"
    git push
    
    # apres verification des quatre checks verts sur la PR :
    gh pr merge feat/triage-sast --squash --delete-branch
  5. Vérifiez sur main que le pipeline est vert de bout en bout, quatre portes comprises. Vous laissez un dépôt où chaque faille détectable par une machine est gardée par une porte, chaque porte a un seuil écrit, et chaque exception a une date. C'est l'état dans lequel un module de sécurité doit rendre un dépôt.
Questions de réflexion — à préparer par écrit
  • Votre note tient en deux pages. Si vous ne deviez en garder qu'une phrase pour un décideur non technique, laquelle, et pourquoi celle-là ?
  • Vous avez fusionné après avoir laissé deux PR ouvertes plusieurs jours. Quelle discipline d'équipe cette expérience vous fait-elle recommander ?
✓ terminé
Point de contrôle

M2 · J3 — DAST intégré : ZAP échoue le build sur XSS, pipeline vert aux 4 portes

Jour 3 · DAST et intégration des trois au pipeline · 2 h

Vérifiez avant de continuer
La revue de fin de module

Elle dure deux heures et se tient en fin de journée. Votre encadrant partira de votre note de synthèse et vous fera démontrer le pipeline en direct : réintroduire le XSS, voir la porte échouer, corriger, voir le vert. Préparez le dépôt, Docker démarré, les volumes de cache en place.

Ce qui n'est pas attendu : une couverture parfaite. Un dépôt où chaque porte a un seuil que vous savez défendre vaut mieux qu'un dépôt qui bloque tout et que personne ne pourrait tenir. C'est cela qu'on validera.

Ce qui part au M3 : le durcissement de votre image Docker (elle tourne encore en root, embarque debug=True) et la fermeture de VULN-09. Vous y retrouverez Trivy, sur l'image cette fois.

✓ terminé

Mon avancement

Signature d'émargement

Demander de l'aide à votre encadrant

Étape concernée :

Avant d'envoyer

Cherchez seul pendant 30 minutes et notez ce que vous avez tenté. Ce que vous avez essayé fait partie de ce que vous présentez — c'est ce qui distingue l'autonomie de l'abandon.