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.
Ce que chacune fait, en une phrase
| Famille | Nom complet | Entrée | Question qu'elle pose |
|---|---|---|---|
| SAST | Static Application Security Testing | le code source, sans l'exécuter | « ce que ce code peut faire est-il dangereux ? » |
| SCA | Software Composition Analysis | la liste des dépendances et leurs versions | « quelqu'un a-t-il déjà publié une faille dans ce que j'importe ? » |
| DAST | Dynamic Application Security Testing | l'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
emplacement — app.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.
| Faille | SAST | SCA | DAST | |
|---|---|---|---|---|
VULN-01 | Injection SQL sur /login | oui — concaténation vers un curseur | non | oui — la réponse change |
VULN-02 | XSS réfléchi sur /search | oui — gabarit construit dynamiquement | non | oui — la charge revient dans la page |
VULN-03 | Injection de commande sur /ping | oui — shell=True sur une entrée | non | oui, mais bruyant |
VULN-04 | IDOR sur /profile | non | non | non — il faut deux comptes pour le voir |
VULN-05 | Secret en dur | oui — c'est Gitleaks | non | non — invisible de l'extérieur |
VULN-06 | Hachage md5 d'un mot de passe | oui | non | non |
VULN-07 | yaml.load non sûr | oui | partiellement — via une CVE de PyYAML | difficilement |
VULN-08 | SSRF sur /fetch | oui — par motif | non | seulement avec un serveur d'écoute externe |
VULN-09 | debug=True, en-têtes absents | oui — Bandit | non | oui — bannière et en-têtes |
VULN-10 | Dépendances à CVE | non | oui — c'est sa seule raison d'être | non |
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.
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.
Intention — poser la thèse du module en première étape, et sous une forme falsifiable : le tableau VULN-01..10 × famille est une prédiction qu'Elie va vérifier lui-même en trois jours. Tout le reste du module consiste à cocher ou à corriger ce tableau. Placée avant toute manipulation, parce qu'un outil découvert sans grille de lecture devient « l'outil qui trouve les failles », au singulier.
Ce qui coince
- La hiérarchie implicite : « le DAST est plus fort puisqu'il prouve ». Signe : il propose de sauter le SAST au jour 3. Le contre-exemple à sortir est VULN-05 (secret en dur) — invisible de l'extérieur, quel que soit le scanner.
- « Angle mort » compris comme « bug de l'outil ». C'est une propriété de la méthode, pas un défaut d'implémentation : aucune version future de Semgrep ne trouvera une CVE dans urllib3, et aucune version future de Trivy ne lira votre
app.py. - Ingénieur venant du développement : il connaît les linters et range le SAST à côté de
flake8. Le distinguo à faire passer est la notion de puits — un linter juge la forme, un moteur SAST suit une donnée.
Questions à anticiper
- « Pourquoi trois outils et pas un produit qui fait tout ? » — les produits « plateforme » existent (Snyk, Veracode) ; ils assemblent exactement ces trois moteurs sous une facture unique. Répondre par l'assemblage, pas par l'idéologie du libre.
- « IAST, RASP, ça existe aussi ? » — oui, une phrase suffit : instrumenter le runtime, hors périmètre reproductible sur un poste. Ne pas ouvrir le sujet.
Ce qu'il faut exiger — qu'il annonce, avant le TP 1, laquelle des dix VULN Semgrep ne trouvera pas, et pourquoi. Une prédiction fausse est un excellent point de départ ; l'absence de prédiction rend le TP 2 sans objet.
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ègle | Ce qu'il attrape | Ce 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ès | subprocess.run(["ping", h]) |
hashlib.md5(...) | tout usage de md5, y compris légitime | hashlib.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.
Sur VULN-03, cela donne exactement trois éléments :
- source —
request.args.get("host", "127.0.0.1"): la valeur vient du client, elle est teintée ; - puits —
subprocess.check_output(..., shell=True): le premier argument est interprété par un interpréteur de commandes ; - assainisseur —
shlex.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
| Limite | Pourquoi | Conséquence concrète |
|---|---|---|
| La logique métier | « cet utilisateur a-t-il le droit de lire cette fiche ? » n'est écrit nulle part | VULN-04 ne sortira jamais, quel que soit le jeu de règles |
| Le code appelé dynamiquement | getattr(module, nom)() : le nom n'est connu qu'à l'exécution | un chemin d'attaque réel reste invisible |
| L'inter-fichiers | Semgrep OSS analyse fichier par fichier ; l'inter-procédural inter-fichiers est payant | une teinte qui traverse un import se perd |
| La configuration effective | la valeur réelle d'une variable d'environnement n'existe qu'au déploiement | un DEBUG lu depuis l'environnement ne se juge pas statiquement |
| Les dépendances | il analyse votre code, pas les 40 Mo de site-packages | VULN-10 est hors de portée — c'est le sujet de demain |
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 (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.
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.
Intention — installer trois objets techniques qui servent tout le jour : l'arbre syntaxique, la métavariable, et le triplet source / assainisseur / puits. Sans eux, le TP 3 (écriture d'une règle) devient de la recopie de YAML.
Ce qui coince
- Le suivi de teinte confondu avec « il regarde si la variable vient de
request». Signe : il écrit un motifrequest.args.get(...)et s'étonne qu'il ne remonte pas jusqu'ausubprocesstrois lignes plus bas. Le motif décrit un point ; la teinte décrit un chemin. - L'ellipse
...lue comme « n'importe quel texte ». C'est « n'importe quelle suite d'arguments ou d'instructions », au sens de l'arbre — pas une regex. - L'analyse inter-fichiers supposée acquise. Semgrep OSS analyse fichier par fichier ; l'inter-fichiers est une fonction payante. Le dire franchement évite qu'il conclue à un bug quand une teinte ne traverse pas un import.
Questions à anticiper
- « Pourquoi ne pas utiliser une regex ? » — faire écrire au tableau une regex qui attrape
execute("SELECT ... " + x)mais pasexecute("SELECT ... ?", (x,)), sur trois lignes avec un retour à la ligne. Trente secondes suffisent à emporter la conviction. - « SARIF, encore un format ? » — oui, et le seul qui soit lu à la fois par GitHub, GitLab, VS Code et les agrégateurs. C'est la raison pour laquelle on ne parse jamais la sortie texte d'un outil.
Ce qu'il faut exiger — qu'il nomme la source, l'assainisseur possible et le puits de VULN-03 dans le code réel, sans regarder le schéma. Ces trois mots sont les trois clés YAML du TP 3 : s'ils ne sont pas posés ici, l'après-midi dérape.
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 ?
Intention — vérifier quatre acquis distincts avant de lâcher sur les TP : la frontière motif / teinte (q1, q4), la définition d'un faux négatif (q2), le périmètre réel du SAST (q3), et le rôle de SARIF (q5). Le score global ne renseigne sur rien ; c'est la ventilation qui décide.
Ce qui coince
- q1 ratée : le triplet source / puits / assainisseur n'est pas posé, le TP 3 sera une recopie. À reprendre avant de le laisser continuer — dix minutes ici en économisent quarante cet après-midi.
- q3 ratée dans le sens « le SAST voit les CVE » : c'est la confusion SAST/SCA, elle empoisonne toute la journée de demain. Reprendre le tableau de M2L1.
- q2 ratée : il inverse faux positif et faux négatif. Fréquent et sans gravité si c'est un lapsus ; grave s'il en tire « mieux vaut trop d'alertes ».
Questions à anticiper
- « La 4, c'est un piège ? » — non : c'est exactement le faux positif qu'il va rencontrer dans une heure au TP 2. Le dire, cela installe l'attente.
- « On peut donc tout régler avec des règles de teinte ? » — non, et le tableau des limites de M2L2 répond : la logique métier n'a ni source ni puits.
Ce qu'un échec révèle — 3/5 avec q1 et q3 justes : laissez partir, les TP corrigeront. 3/5 avec q1 fausse : reprenez le schéma de teinte cinq minutes.
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.
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.
- 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 \ . - 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.
- 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 . - 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 - 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 - Ouvrez
app.pyà côté de cette liste et faites la confrontation, vulnérabilité par vulnérabilité. Pour chacune des dixVULN-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. - Dernière étape, et c'est celle qui compte pour le pipeline : relancez en mode bloquant.
--errorfait sortir Semgrep en code 1 dès qu'une règle correspond, et--severity ERRORrestreint 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 : $?" - 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.
- 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-auditest 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..10Semgrep 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 ?
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.
Intention — produire l'inventaire brut avant toute discussion de tri. L'ordre est délibéré : voir cinquante résultats sortir d'une application de cent trente lignes est ce qui rend le TP 2 nécessaire, et aucune leçon ne remplace ce choc.
Ce qui coince
- Docker Desktop non démarré :
Cannot connect to the Docker daemon. Lu comme une erreur de syntaxe, cela coûte dix minutes à qui découvre. - Analyse lancée depuis le parent du dépôt. Symptôme trompeur : ça marche, c'est long, et rien ne concerne
app.py. - Le SARIF ouvert dans un éditeur et lu à l'œil. Trois mille lignes. C'est le moment d'imposer le script Python de l'étape e — un ingénieur qui lit du JSON à l'œil n'automatisera jamais son triage.
- L'écart entre le nombre de résultats et le nombre de règles distinctes n'est pas vu. C'est pourtant le premier indicateur de bruit.
Questions à anticiper
- « Pourquoi
--metrics=off? » — Semgrep remonte par défaut des statistiques d'usage anonymes. Sur du code client, ce réflexe se prend le premier jour. Ne pas en faire un procès à l'éditeur : c'est une question d'hygiène contractuelle. - «
p/security-auditsort trop de choses, on l'enlève ? » — pas maintenant. C'est le sujet du TP 2, et la décision doit être argumentée, pas réflexe.
Ce qu'il faut exiger — le fichier artefacts/semgrep-2026-08-06.sarif existe, le décompte par règle a été produit par script, et il sait annoncer de tête combien de résultats pour combien de règles. Refuser un « il y en a beaucoup » : le chiffre est le point de départ du tri.
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.
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.
- 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 - 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) - Analysez ce seul fichier. Semgrep signale l'usage de md5, exactement comme il le signale sur
/hashdansapp.py.docker run --rm -v "$(pwd)":/src -w /src semgrep/semgrep:1.85.0 \ semgrep scan --metrics=off --config p/python outils/cache.py - Arrêtez-vous une minute sur ce résultat. La même règle, deux verdicts opposés : sur
app.pyc'estVULN-06, un mot de passe haché avec un algorithme cassé ; suroutils/cache.pyc'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. - Corrigez le faux positif de la bonne manière : en rendant l'intention explicite dans le code.
usedforsecurity=Falseexiste 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() - 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 - Posez maintenant le
.semgrepignoredu 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/ - Reprenez maintenant la liste complète du TP 1 et rendez un verdict pour chaque ligne. Écrivez-la dans
rapports/sast-devsecops-lab.mdsous 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. - É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.
- 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"
- 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
nosemgrepnu 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é ?
La tentation, à cet endroit, est de faire taire l'outil. Retenez la hiérarchie des trois gestes, du meilleur au pire :
- 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. - Annoter avec l'identifiant de la règle —
nosemgrep: la.regle.exactesuivi d'un motif écrit. L'alerte disparaît, les autres règles continuent de s'appliquer sur cette ligne. - Annoter sans identifiant — un
nosemgrepnu 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.
Intention — faire vivre le faux positif au lieu d'en parler. Le fichier outils/cache.py est construit pour cela : la même règle md5 se déclenche sur un vrai positif (VULN-06, un mot de passe) et sur un faux positif (une clé de cache). Deux verdicts opposés, une seule règle : c'est la démonstration que le tri ne peut pas être délégué à l'outil.
Ce qui coince
- Le réflexe
# nosemgrepnu, systématiquement, dès la deuxième alerte. C'est le moment de l'arrêter net : montrer qu'unnosemgrepnu posé aujourd'hui masquera aussi la règle d'injection écrite l'an prochain sur la même ligne. usedforsecurity=Falsepris pour une astuce de contournement. C'est l'inverse : c'est un paramètre du langage (Python 3.9+, appuyé sur OpenSSL) qui déclare l'intention. Bandit et Semgrep le respectent parce que le code dit enfin la vérité.- Le verdict rendu sans justification écrite. Un tri non écrit sera refait, à l'identique, dans trois mois — et par quelqu'un d'autre.
- La liste des angles morts bâclée : elle est pourtant le vrai livrable du TP, et le critère qui reviendra lundi.
Questions à anticiper
- « Combien de faux positifs est-ce acceptable ? » — pas de chiffre absolu. Le seuil opérationnel est celui à partir duquel on cesse de lire les alertes ; en pratique, dans une porte bloquante, on vise très bas et on accepte des faux négatifs, que le DAST et la revue rattrapent. C'est l'arbitrage de M2L1, il faut qu'il le reformule.
- « Pourquoi ne pas simplement passer md5 en sha256 dans le cache ? » — on peut, et ce serait acceptable ; faire remarquer que cela résout l'alerte sans jamais poser la question du besoin, et que le jour où le motif sera « performance », la réponse aura été perdue.
Ce qu'il faut exiger — un tableau à quatre colonnes (règle, emplacement, verdict, justification) couvrant tous les résultats, sans exception « évidente », et la liste des angles morts avec la cause de chacun. Ne pas valider un tableau où la colonne justification contient « faux positif ».
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 |
|---|---|---|
id | identifiant unique, stable | il apparaît dans le SARIF, dans les annotations nosemgrep et dans vos rapports : le changer casse tout ce qui le référence |
severity | INFO, WARNING ou ERROR | c'est ce que filtre --severity dans le pipeline ; réservez ERROR à ce qui doit bloquer |
message | ce que lit le développeur | il doit dire quoi faire. « Usage dangereux détecté » ne corrige rien |
metadata | CWE, OWASP, confiance, référence interne | champ libre ; c'est là que vous rattachez vos VULN-NN |
patterns | liste combinée en ET | toutes les entrées doivent correspondre au même endroit |
pattern-either | liste combinée en OU | une seule entrée suffit |
pattern-not | exclusion | retire les correspondances qui satisfont aussi ce motif |
fix | remplacement 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.
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-idavant une ligne — la règle doit la signaler ;# ok: mon-idavant 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.
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.
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.
Intention — faire de la règle un objet de code versionné, relu et testé, et non un réglage d'outil. C'est le pivot du module : à partir d'ici, la sécurité s'écrit dans le dépôt. La fonction --test est le point central de la leçon, pas une annexe.
Ce qui coince
- YAML : indentation à deux espaces, tabulations refusées, et un
pattern:multiligne mal indenté produit une erreur d'analyse à trente lignes de l'endroit fautif. S'il perd plus de cinq minutes là-dessus, faire coller le fichier dans un validateur YAML plutôt que de relire. - Les métavariables comprises comme des variables du langage.
$Xcapture un sous-arbre, et la même métavariable employée deux fois dans une règle impose que ce soit le même sous-arbre — c'est ce qui permet d'écrire « la valeur assignée ici est celle utilisée là ». --testlancé sans fixture, ou avec une fixture dont le nom de base ne correspond pas au fichier de règles. L'appariement se fait par nom (flask-regles.yml↔flask-regles.py) : c'est la panne numéro un du TP suivant.
Questions à anticiper
- « Pourquoi écrire des règles alors qu'il en existe des milliers ? » — parce que les règles publiques ne connaissent ni vos conventions internes, ni vos fonctions maison, ni vos interdits d'entreprise. Donner l'exemple concret : une équipe qui interdit
requestsau profit d'un client HTTP interne instrumenté. - « Le champ
fix:peut-il corriger tout seul en CI ? » — techniquement oui (--autofix) ; y répondre par le risque, pas par la faisabilité : une correction automatique poussée sans revue est un changement non relu dansmain.
Ce qu'il faut exiger — qu'il explique la différence entre patterns: et pattern-either: avec ses mots, et qu'il annonce le contenu de sa fixture avant d'écrire la règle. Écrire le test d'abord n'est pas une coquetterie ici : c'est la seule façon de savoir qu'une règle silencieuse est fausse plutôt que satisfaite.
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.
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.
- Créez l'arborescence des règles et de leurs tests.
mkdir -p .semgrep/tests - É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) - É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) - 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/ - Cassez volontairement une règle et relancez le test. Retirez par exemple la ligne
- pattern-not: render_template_string("...", ...): le casgabarit_okdoit maintenant échouer. C'est la preuve que votre test teste quelque chose. Remettez la ligne, relancez, revérifiez le vert. - Appliquez enfin les règles au code réel. Vous devez voir remonter
VULN-02,VULN-03etVULN-07, et rien d'autre — trois résultats pour trois règles. Notez les gravités :VULN-03etVULN-07sortent enERROR,VULN-02enWARNING. 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 : $?" - Regardez ce que propose le champ
fix:, sans l'appliquer.--dryrunaffiche le correctif que--autofixécrirait. Vous corrigerezVULN-07demain, à 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 - Branchez les règles maison au pipeline. Remplacez le job
sastde.github/workflows/ci.ymlpar 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 - Notez le changement de doctrine dans ce job : on charge
p/pythonetp/flask, mais plusp/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. - Committez, poussez, et vérifiez que le pipeline devient rouge — c'est attendu :
VULN-03etVULN-07sont toujours dansapp.pyet vos règles viennent de les déclarerERROR, 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 - Regardez au passage ce que vos annotations du 5 août n'ont pas fait. Les lignes
/pinget/configportent 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 dep/security-audit, et vos règles s'appellentcaplogy-*. 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. Unnosemgrepnu, 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. - 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 |
- 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
WARNINGet les deux autres enERROR. Défendez ce choix : pourquoi ne pas tout bloquer, et qu'est-ce qui rattrapera le XSS ? - La règle
caplogy-flask-gabarit-dynamiquesignaleraitrender_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.
É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.ymlexigeflask-regles.py, pastest-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.
Intention — trois règles, trois mécanismes délibérément différents : un motif avec exclusion (VULN-02), une règle de teinte complète (VULN-03), une règle avec fix: (VULN-07). C'est le livrable technique du jour et le premier critère du point de contrôle.
Ce qui coince
- L'appariement
--test. Panne numéro un, et son symptôme — « aucun test trouvé » — ressemble à un succès pour qui lit vite. Exiger qu'il montre le nombre de cas exécutés. - La règle de teinte écrite avec source et puits dans deux fonctions différentes de la fixture. Elle ne remonte rien, et il conclut que le mode teinte ne marche pas.
- Le
pattern-not: render_template_string("...", ...)oublié : la règle remonte alors aussi le gabarit fixe et paramétré, qui est justement la bonne pratique qu'on veut encourager. Excellent moment pour montrer qu'une règle mal écrite pénalise le code correct. - Le YAML : deux espaces, jamais de tabulation, et
>-pour un message multiligne. Les erreurs d'analyse pointent souvent loin de la faute.
Questions à anticiper
- « Ma règle remonte aussi
outils/cache.py, c'est normal ? » — non, et c'est le signe d'un motif trop large. Le faire restreindre plutôt qu'ignorer. - « Je peux mettre
--autofixdans le pipeline ? » — répondre par le risque : un correctif non relu poussé dansmain. En local avant commit, oui ; en CI, jamais. - « Pourquoi
--severity ERRORdans le job et pas dans mon scan local ? » — parce que le local sert à voir, la CI à décider. Deux usages, deux réglages, et c'est la préfiguration des seuils de lundi.
Ce qu'il faut exiger — semgrep --test qui affiche un compte de cas non nul et tout au vert, les trois règles qui remontent les trois VULN attendues dans app.py, et le job sast du pipeline qui charge /src/.semgrep. Un test vert sans cas exécuté ne vaut rien : demander le chiffre.
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.
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.
- 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 - Ouvrez
http://localhost:9000. Identifiants initiauxadmin/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. - Lancez le scanner. Notez le
sonar.host.url: il désigne le conteneur par son nom surdso-net, paslocalhost— depuis l'intérieur d'un conteneur,localhostc'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 - 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.
- 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.
- 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 ».
- É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 - Dernier réflexe, celui qui distingue une découverte d'une accumulation :
docker ps -ane 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.
- 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 ?
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.
Intention — le programme déposé cite SonarQube ; il faut donc qu'Elie l'ait manipulé, et qu'il sache argumenter pourquoi le parcours ne s'appuie pas dessus. L'objectif pédagogique est la comparaison, pas l'outil : une plateforme à serveur apporte l'historique, la tendance et le quality gate partagé ; elle coûte un service à maintenir. Semgrep apporte la règle versionnée et le zéro-infrastructure.
Ce qui coince
- Sous Linux,
vm.max_map_count. Le conteneur sort en boucle de redémarrage et le message est enterré dans les journaux d'Elasticsearch. Dix minutes perdues si on ne le sait pas. - L'impatience : le navigateur ouvert à trente secondes, un 503, et la conclusion que l'image est cassée. Exiger d'attendre « SonarQube is operational ».
sonar.host.urlpointé surlocalhostdepuis le conteneur du scanner. Il faut le nom du service surdso-net: c'est exactement la règle du § 5 du référentiel, et c'est un excellent rappel involontaire de réseau Docker.- Le conteneur laissé allumé. Le vérifier soi-même au point de contrôle par
docker ps: c'est un critère.
Questions à anticiper
- « Pourquoi ne pas le garder, puisqu'il trouve des choses ? » — répondre en coût total : un serveur à sauvegarder, à mettre à jour, à sécuriser (il contient tout votre code), plus 2 Go de RAM sur le poste. Puis renvoyer à la vraie question : qu'apporte-t-il que Semgrep + Bandit n'apportent pas, sur ce projet ?
- « Le quality gate ne bloque pas mon pipeline ? » — pas sans
sonar-scanner --waitet une intégration dédiée. Une porte est déjà en place ; en ajouter une seconde, redondante et fragile, est une mauvaise architecture.
Ce qu'il faut exiger — trois différences concrètes écrites dans le rapport (pas « c'est plus visuel »), au moins un security hotspot commenté avec le raisonnement de tri, et docker ps qui ne montre plus SonarQube.
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.
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.
- Relisez
rapports/sast-devsecops-lab.mdet 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. - 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é
ERRORsurp/pythonetp/flaskseuls. Ces trois nombres sont l'argumentaire de votre futur seuil de blocage. - 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.
- 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 - 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." - 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.
- 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 ?
Intention — transformer une journée de manipulations en livrable opposable. Le contenu est déjà écrit au fil de l'eau ; cette demi-heure sert à le structurer, à le relire et à le faire passer en revue. C'est aussi la première fois qu'Elie ouvre une pull request sur un contenu documentaire — le réflexe se prend maintenant.
Ce qui coince
- Le rapport rédigé maintenant, de mémoire, parce que rien n'a été noté au fil de l'eau. Se voit immédiatement : les chiffres manquent. Si c'est le cas, faire relancer les commandes plutôt que d'accepter des approximations.
- La section « ce que le SAST n'a pas vu » réduite à une phrase. C'est pourtant elle qui prépare les deux journées suivantes.
- La PR ouverte sur une branche non signée : la protection de
mainla refuse, et le message de GitHub n'est pas explicite. Rappeler lecommit.gpgsigndu M1 J2.
Questions à anticiper
- « Je fusionne moi-même ma propre PR ? » — oui ici, il est seul ; lui faire écrire dans la description ce qu'un relecteur devrait vérifier. C'est ce qui rend l'exercice utile en équipe.
- « Le pipeline est rouge, je peux fusionner quand même ? » — non, et c'est le moment d'expliquer pourquoi cette PR restera ouverte jusqu'à demain : les corrections de code viennent au J2. Une PR qui attend un correctif est une situation normale, pas un échec.
Ce qu'il faut exiger — les cinq sections présentes, les chiffres réels du jour (nombre de résultats, nombre de règles, écart entre le scan large et le scan bloquant), et une PR ouverte dont la description dit ce qui reste à faire.
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
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.
Intention — vérifier deux acquis et deux seulement : Elie sait écrire une règle testée, et il sait trier un résultat plutôt que le faire taire. Trente minutes, c'est le point quotidien de 16 h 30, pas une revue de module.
Ce qu'il faut exiger — trois gestes, faits par lui devant vous, dans cet ordre :
semgrep --testlancé en direct : exiger le nombre de cas affiché, pas le mot « passed ». Puis lui faire retirer une ligne de règle au hasard et prédire le test qui va tomber, avant de relancer.- La confrontation md5 : ouvrir côte à côte
app.py/hashetoutils/cache.py, et lui faire dire pourquoi la même règle donne deux verdicts. S'il répond « parce que l'un est un mot de passe », demander la généralisation : ce que la valeur protège. docker ps -a— aucune trace de SonarQube.
Ce qui coince
- Le critère bâclé neuf fois sur dix : la section « ce que le SAST n'a pas vu ». Si elle tient en deux lignes, la journée a été exécutée et non comprise. Le dire ce soir : demain est plus dense, et lundi s'appuie dessus.
- Un test vert avec zéro cas exécuté. Ça ressemble à une réussite. Demander le chiffre systématiquement.
- Le tableau de triage avec des justifications creuses (« faux positif », « pas grave »). Refuser : une justification dit ce que la valeur protège et pourquoi le risque n'existe pas ici.
Questions à anticiper
- « Mon pipeline est rouge, c'est un échec ? » — non, c'est le résultat attendu et c'est écrit dans sa PR. Le féliciter là-dessus : un pipeline qu'on rend rouge volontairement est un pipeline auquel on croit.
- « Je peux rattraper le rapport demain ? » — non : le TP 10 de demain suppose la structure déjà en place, et vendredi est la journée la plus dense du module.
Ne pas valider si — la fixture ne contient aucun cas ok:, ou si le faux positif md5 a été traité par un nosemgrep nu. Dans les deux cas, l'idée centrale du jour n'est pas passée, et elle ne se rattrape pas en marchant : reprendre vingt minutes demain matin.
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.
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é
| Terme | Ce que c'est | Ce que ce n'est pas |
|---|---|---|
| CVE | un identifiant mondial unique, CVE-AAAA-NNNNN | ni une gravité, ni un verdict sur votre application |
| Advisory | l'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 |
| CVSS | un score de gravité intrinsèque, de 0 à 10 | une probabilité d'exploitation, ni un niveau de risque pour vous |
| PURL | l'adresse normalisée d'un paquet : pkg:pypi/pyyaml@5.3.1 | un nom local — c'est ce qui permet de croiser deux outils sans ambiguïté |
| SBOM | l'inventaire complet et daté de ce que contient une livraison | un 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.
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-dateutilcontredateutil). 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.
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.
Intention — faire admettre que la surface d'attaque déborde très largement les fichiers versionnés, et donner le vocabulaire exact (direct/transitif, advisory, PURL, SBOM) avant les manipulations. Le SBOM n'est pas un formulaire administratif : c'est la condition pour répondre en une heure à « sommes-nous exposés ? » le jour où une faille majeure sort.
Ce qui coince
- « Transitif » compris mais pas ressenti. Le contre-argument qui porte : les dépendances transitives s'exécutent avec exactement les mêmes droits que le code d'Elie, sans avoir été choisies par personne.
- La confusion CVE / advisory / avis d'éditeur. Une CVE est un identifiant, pas un verdict ; c'est l'advisory (GHSA, OSV) qui dit quelles versions sont touchées, et deux bases ne s'accordent pas toujours. Cela prépare le désaccord Trivy/Grype du TP 7.
- Le SBOM pris pour de la paperasse réglementaire. Le retourner en question opérationnelle : « Log4Shell, un vendredi soir — combien de temps pour savoir si vous êtes concerné ? » Sans inventaire, la réponse se compte en jours.
Questions à anticiper
- « Pourquoi épingler avec
==plutôt que>=? » — reproductibilité d'abord ; puis nuancer honnêtement : épingler fige aussi les failles, et c'est exactement pour cela qu'on ajoute Dependabot en fin de journée. - « Un lockfile, c'est mieux ? » — oui, il fige l'arbre transitif entier.
requirements.txtne fige que le premier niveau. Une phrase surpip-compile, sans ouvrir le sujet. - « Le typosquatting, ça arrive vraiment ? » — oui, régulièrement, sur PyPI comme sur npm. Rester factuel, ne pas dramatiser.
Ce qu'il faut exiger — qu'il cite de mémoire deux dépendances transitives de son application, c'est-à-dire absentes de son requirements.txt. S'il n'y arrive pas, il ne mesure pas encore ce que la SCA va lui apprendre.
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étrique | Valeurs | La question qu'elle pose |
|---|---|---|
AV — Attack Vector | Network, Adjacent, Local, Physical | d'où l'attaquant doit-il opérer ? |
AC — Attack Complexity | Low, High | faut-il des conditions particulières, hors du contrôle de l'attaquant ? |
PR — Privileges Required | None, Low, High | faut-il déjà un compte, et lequel ? |
UI — User Interaction | None, Required | faut-il qu'une victime clique ? |
S — Scope | Unchanged, Changed | l'impact déborde-t-il du composant vulnérable ? |
C/I/A | None, Low, High | confidentialité, 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
| Bande | Score CVSS v3.1 | Délai de correction attendu |
|---|---|---|
| Critical | 9.0 – 10.0 | immédiat |
| High | 7.0 – 8.9 | sous 7 jours |
| Medium | 4.0 – 6.9 | sous 30 jours |
| Low | 0.1 – 3.9 | planifié |
| Info | 0.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
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
- 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.
- 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.
- 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.
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.
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.
Intention — casser l'équation « score élevé = à corriger d'abord », et introduire l'atteignabilité comme critère de tri. C'est la préparation directe du TP 8, qui est le cœur de la journée. Le vecteur CVSS est aussi le format imposé par le § 8 du référentiel pour tous les findings du M5 et du PFR : il s'apprend ici, une fois.
Ce qui coince
- Le vecteur lu comme une décoration. Faire décoder
AV:N/AC:L/PR:N/UI:Nà voix haute, métrique par métrique, jusqu'à ce que ce soit une phrase française. - « Atteignable » confondu avec « installé ». Un paquet installé n'est pas un paquet appelé, et une fonction appelée n'est pas forcément la fonction vulnérable.
- EPSS confondu avec CVSS. Deux échelles, deux questions : gravité intrinsèque contre probabilité d'exploitation observée. Un 9.8 avec un EPSS à 0,1 % et un 7.5 avec un EPSS à 40 % ne se traitent pas dans le même ordre.
- L'acceptation du risque vécue comme un aveu de faiblesse. C'est au contraire l'acte professionnel du jour — à condition qu'elle soit écrite, datée et signée.
Questions à anticiper
- « Qui décide d'accepter un risque ? » — pas l'ingénieur seul. L'ingénieur documente et propose ; l'acceptation appartient au responsable du service. C'est la distinction que le M4 formalisera dans le registre de risques.
- « CVSS v4 existe, pourquoi v3.1 ? » — parce que l'immense majorité des advisories en circulation sont notées en v3.1, et que le référentiel du parcours l'impose pour que tous les rapports soient comparables. Une phrase, pas un débat.
Ce qu'il faut exiger — qu'il traduise un vecteur complet en français, sans aide, et qu'il énonce les trois décisions possibles face à une CVE : corriger, compenser, accepter avec justification datée. Ces trois mots structurent le TP 8.
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 ?
Intention — vérifier que le tri par score seul est bien mort (q1, q4), que le vecteur se lit (q2), que le SBOM a un usage opérationnel (q3) et que l'épinglage est compris dans ses deux effets (q5). q1 et q4 sont les deux qui commandent le TP 8.
Ce qui coince
- q1 ratée : il trie encore par score. Le TP 8 sera vécu comme un exercice de style. Reprendre l'encadré atteignabilité avant de le laisser partir.
- q2 ratée sur
PR:N: confusion fréquente entre « aucun privilège requis » et « aucun privilège obtenu ». Le vecteur décrit ce qu'il faut avant, l'impact ce qu'on obtient après. - q5 ratée dans le sens « épingler protège » : l'idée que le figement est une mesure de sécurité en soi. Elle est tenace et elle mène à des dépôts qui pourrissent.
Questions à anticiper
- « La 4 est ambiguë, on corrige ou pas ? » — non, elle ne l'est pas : quand le coût est nul, on corrige sans débattre de l'atteignabilité. L'analyse d'atteignabilité sert à trier ce qui coûte, pas à justifier de ne rien faire.
Ce qu'un échec révèle — 3/5 avec q1 et q4 justes : laissez partir. q1 ou q4 fausses : dix minutes de reprise, sinon la journée entière rate sa cible.
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.
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.
- 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 . - 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'estFixed Version. Vide, il n'existe pas de correctif et vous devrez compenser. - 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 . - 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. - 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 . - 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 - 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. - Appliquez enfin les filtres du futur seuil de blocage, et regardez le code de sortie.
--severity HIGH,CRITICALrestreint la gravité,--ignore-unfixedécarte ce qui n'a pas de correctif,--exit-code 1fait é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 : $?" - É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.
- 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.
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.
Intention — produire l'inventaire et, surtout, faire manipuler les filtres. La compétence visée n'est pas « savoir lancer Trivy » mais « savoir quel décompte on regarde et pourquoi il change ». Chaque filtre correspond à une décision de seuil qui sera défendue lundi.
Ce qui coince
- Le cache oublié : trois minutes de téléchargement à chaque commande, et une conclusion erronée sur la lenteur de l'outil.
- L'écart entre le décompte brut et le décompte filtré non observé. C'est pourtant toute la matière de la journée : lui faire écrire les quatre nombres côte à côte.
- Sans réseau, Trivy échoue sur le téléchargement de la base et le message parle de
OCI registry— lu comme un problème d'image Docker. Le dire avant. --dependency-treeignoré parce que la sortie est large. C'est pourtant le seul endroit où la notion de transitif devient visible.
Questions à anticiper
- « Pourquoi Trivy voit-il des paquets absents de mon
requirements.txt? » — excellente question, et exactement le point de la leçon : il résout l'arbre. Le faire verbaliser. - « Les scores diffèrent de ceux du NVD » — Trivy affiche plusieurs sources (nvd, redhat, ghsa) ; le champ retenu dépend de l'écosystème. Bon moment pour dire que l'accord entre bases n'est pas acquis, et pour annoncer Grype au TP suivant.
Ce qu'il faut exiger — les quatre décomptes écrits : brut, HIGH+CRITICAL, HIGH+CRITICAL sans les non-corrigeables, et le code de sortie de la commande bloquante. Et qu'il nomme deux paquets remontés qui ne figurent pas dans son requirements.txt.
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.
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.
- 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 . - 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 leurpurl.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 - 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. - 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 - 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 - 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 - 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.
- É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.
- 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 ?
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.
Intention — deux acquis distincts. Le SBOM comme artefact de livraison d'abord : daté, versionné, interrogeable. Le désaccord entre moteurs ensuite, qui est la vraie leçon d'humilité de la journée et qui prépare la formulation prudente exigée dans tous les rapports du parcours.
Ce qui coince
- Le SBOM produit puis jamais ouvert. Imposer l'étape de lecture : s'il ne sait pas dire ce qu'est un
purl, l'artefact n'est qu'un fichier de plus. - Le désaccord interprété comme « l'un des deux se trompe ». Il faut au contraire qu'il aille lire une des CVE divergentes et constate que les deux avis sont défendables sur des sources différentes.
- Grype qui télécharge sa base à chaque appel faute de volume : même piège que Trivy, d'où le volume
grype-db. - Le SBOM committé dans le dépôt. Non :
artefacts/est ignoré, un SBOM se régénère et se joint à une release, il ne vit pas dans l'historique.
Questions à anticiper
- « Lequel des deux a raison ? » — la bonne réponse est « les deux, sur leurs sources ». En pratique on retient un moteur de référence pour la porte, et le second sert d'audit périodique. Dire lequel et pourquoi est une décision d'architecte.
- « Faut-il committer le SBOM ? » — non dans
artefacts/, oui attaché à une release ou publié dans un registre. La distinction compte. - « CycloneDX ou SPDX ? » — CycloneDX ici, orienté sécurité ; SPDX domine sur les licences. Une phrase suffit.
Ce qu'il faut exiger — le nombre de composants du SBOM, comparé au nombre de lignes de requirements.txt ; les trois nombres du croisement (Trivy seul, Grype seul, communs) ; et une CVE divergente commentée, advisory lu.
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é.
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.
- Écrivez le script de démonstration dans
demo/pyyaml-poc.pyet lancez-le. Il montre côte à côte l'exécution de code paryaml.loadet le refus paryaml.safe_load.mkdir -p demo # Collez le contenu ci-dessous dans demo/pyyaml-poc.py, puis : python3 demo/pyyaml-poc.py - 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() - 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) etVULN-07(votre/configappelleyaml.loadsur le corps de la requête). L'une sans l'autre serait inoffensive. - Établissez l'atteignabilité, à la main, pour les paquets remontés en HIGH et CRITICAL. Ce
grepest 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 - É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.
- Corrigez
VULN-10pour 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 : $?" - Voici les versions corrigées de
requirements.txt. Remplacez le contenu, puis relancez la construction et le scan de l'étape précédente. Siapp.pyne 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 - 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 - 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. - 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"
- Vous avez prouvé l'exécution de code par
yaml.load. En quoi cette preuve change-t-elle la priorité deVULN-07par 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.yamla une échéance. Que se passe-t-il exactement le lendemain de cette date, et pourquoi est-ce une bonne chose ?
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.
Intention — c'est le cœur du module SCA et l'étape la plus formatrice. Trois gestes indissociables : prouver l'exploitabilité (la CVE PyYAML rendue concrète), décider par l'atteignabilité, corriger pour de vrai en acceptant le risque de régression. La correction de VULN-10 vit ici et nulle part ailleurs.
Ce qui coince
- La montée de version qui casse l'application. C'est le moment pédagogique du module : ne pas le désamorcer.
Flask 3retire des imports,Werkzeug 3déplace des symboles. S'il panique, le ramener à la méthode : lire la trace, une erreur à la fois. - Le POC YAML lancé « pour voir » contre un service distant. Rappel de droit sec et immédiat : localhost, son dépôt, rien d'autre.
- L'atteignabilité bâclée : un
greplancé, aucune conclusion écrite. Exiger un verdict par CVE HIGH+, pas un tableau degrep. - Le
.trivyignore.yamlsansexpired_at. C'est précisément ce que la leçon interdit : une exception sans date. Refuser.
Questions à anticiper
- « Flask 3 casse tout, je reviens à 2.0.1 ? » — non : on corrige la compat, c'est le métier. L'accompagner sur la première erreur, le laisser sur les suivantes. S'il manque de temps, VULN-10 peut se limiter à PyYAML+requests+urllib3 et documenter Flask comme montée planifiée — mais alors c'est écrit et daté.
- « Le POC prouve la RCE, dois-je corriger VULN-07 aussi ? » — oui, c'est le TP suivant côté code (
yaml.safe_load). Ici on prouve, demain… non, aujourd'hui même, au fil des corrections de code. Bien distinguer la faille de dépendance (VULN-10) de la faille de code (VULN-07).
Ce qu'il faut exiger — la charge YAML exécutée en local avec sa preuve à l'écran, un verdict écrit par CVE HIGH+ (corriger / compenser / accepter), le scan bloquant qui passe au vert après montée de version, et un .trivyignore.yaml où toute entrée porte une expired_at.
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é.
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:.
- Reprenez la branche de correction et ajoutez le job
scaà.github/workflows/ci.yml, sousjobs:, 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 . - Lisez la dernière étape du job avant de committer. Les deux filtres
--severity HIGH,CRITICALet--ignore-unfixedsont 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. - 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 - Committez, poussez, et regardez l'exécution du pipeline dans l'onglet Actions. Le job
scadoit maintenant apparaître à côté desecretsetsast.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 - Ouvrez l'exécution terminée et vérifiez deux choses : le job
scaest 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. - 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.
- 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
maincomplique-t-elle la fusion de ces PR, et comment le résoudriez-vous ? - Votre job
scane bloque pas sur les Medium. Décrivez le scénario dans lequel ce choix vous coûterait cher, et ce qui l'empêche.
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.
Intention — fermer la boucle SCA : détection (Trivy), inventaire (SBOM en artefact) et remédiation continue (Dependabot). Le message de fond : épingler sans surveiller est une dette ; Dependabot est la contrepartie de l'épinglage, pas un gadget.
Ce qui coince
- Le job
scamal indenté dans le YAML — il s'insère au même niveau quesecretsetsast, sousjobs:. Une erreur d'indentation ici et le workflow entier est invalide. - Le SBOM publié en artefact confondu avec un commit. Ce sont deux choses : l'un s'attache à l'exécution, l'autre polluerait l'historique.
- Dependabot activé sans comprendre qu'il ouvrira des PR non signées par Elie. Discuter la branch protection : les PR de bot et la règle de commits signés se conjuguent mal, c'est un sujet réel d'équipe.
Questions à anticiper
- « Pourquoi ne pas bloquer sur les Medium ? » — parce qu'une porte qui bloque tout est désactivée en trois semaines. Le seuil est un arbitrage, pas un maximalisme. C'est la thèse de lundi, il faut qu'il commence à la formuler.
- « Dependabot va-t-il casser
main? » — non : il ouvre des PR, la porte les teste, rien n'entre sans passer le pipeline. C'est même la démonstration que la porte sert à quelque chose.
Ce qu'il faut exiger — le workflow valide avec ses trois jobs (secrets, sast, sca), le SBOM visible dans les artefacts de l'exécution, et .github/dependabot.yml en place. Qu'il explique l'ordre des deux filtres du job bloquant.
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.
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.
- Ouvrez
rapports/sca-devsecops-lab.mdet 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. - 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 ».
- 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.
- Complétez la section 5 : le contenu de
.trivyignore.yamlavec 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. - 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 - Ouvrez — ou mettez à jour — la demande de fusion, mais ne fusionnez pas. Le job
scaest vert, maissastreste rouge :VULN-03etVULN-07sont 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)." - Regardez la PR :
secretsvert,scavert,sastrouge. La protection demainrefuse 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.
- 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
scaest vert mais vous ne pouvez pas fusionner. Formulez la règle générale que vous en tirez sur ce qui peut entrer dansmain.
Intention — même exigence qu'au TP 5, sur la matière SCA : un livrable opposable, rejouable, versionné. Le rapport de composition est aussi le lieu où la décision d'atteignabilité et les exceptions datées sont tracées — c'est ce qui distingue un tri professionnel d'un « j'ai monté les versions ».
Ce qui coince
- Les chiffres du jour non consignés au fil de l'eau : les quatre décomptes de Trivy, le croisement Trivy/Grype, le nombre de composants du SBOM. Sans eux, le rapport n'est pas rejouable. Faire relancer plutôt qu'estimer.
- La section atteignabilité absente ou réduite au
grepcollé sans conclusion. C'est pourtant le raisonnement qui fait la valeur du rapport. - La tentation de fusionner tout de suite parce que « la SCA est finie ». Non : le job
sastreste rouge (VULN-03 et VULN-07 sont toujours dans le code, déclaréesERRORhier). La fusion consolidée est pour lundi, quand les dernières failles de code seront corrigées et que les quatre portes seront vertes ensemble.
Questions à anticiper
- « Le job
scaest vert, pourquoi ne pas fusionner ? » — parce que la protection demainexige toutes les vérifications au vert, etsastne l'est pas. C'est exactement ce à quoi sert une porte : elle n'attend pas qu'on soit d'accord.
Ce qu'il faut exiger — le rapport sca-devsecops-lab.md avec ses sections, les chiffres réels, la décision par CVE HIGH+, les exceptions datées, et la branche fix/vuln-10-deps poussée avec le job sca au vert, fusion en attente des corrections de code.
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
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é.
Intention — vérifier que le tri par atteignabilité est acquis (et pas seulement récité) et que la correction de VULN-10 est réelle et rejouable. Trente minutes, point quotidien.
Ce qu'il faut exiger — trois gestes, faits devant vous :
- Il rejoue le POC YAML en local : la commande s'exécute à l'écran. Puis il explique en une phrase pourquoi
VULN-10etVULN-07sont la même histoire. - Il prend une CVE HIGH+ de sa liste et défend sa décision par l'atteignabilité, sans regarder son rapport. S'il dit « je l'ai montée parce que c'est un 9 », creuser : atteignable ou pas ?
trivy fs --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1en direct : code de sortie0, et il sait dire ce que chaque exception du.trivyignore.yamlcouvre et quand elle expire.
Ce qui coince
- Le « je monte tout » déguisé en tri. Signe : aucune CVE acceptée, aucune compensée, tout corrigé. Sur cette application c'est possible, mais alors il doit pouvoir dire pour chacune qu'elle était atteignable OU que le correctif était gratuit — pas « par principe ».
- Une exception sans
expired_at. Rédhibitoire : c'est l'inverse exact de la leçon. - La formule « zéro vulnérabilité » sans qualificateur d'outil et de date. La faire reformuler : c'est un réflexe de langage qui protège en audit.
Questions à anticiper
- « Flask 3 a cassé des choses, c'est grave ? » — non, c'est la leçon ; ce qui compte est qu'il ait diagnostiqué et corrigé, ou documenté une montée planifiée et datée.
Ne pas valider si — le POC n'a jamais été exécuté (donc l'exploitabilité reste une croyance), ou si le .trivyignore.yaml contient une entrée sans échéance. Reprendre vingt minutes le lendemain matin.
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
- 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.
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échi | oui, avec preuve | la charge injectée revient telle quelle dans la page — c'est visible |
VULN-09 debug, en-têtes | oui, en passif | la bannière et les en-têtes manquants sont dans chaque réponse |
VULN-08 SSRF | conditionnel | il faut un serveur d'écoute externe pour confirmer que la requête est partie — M5 |
VULN-05 secret en dur | non | un secret dans le code ne produit aucune réponse observable |
VULN-06 hachage md5 | non | l'algorithme de hachage ne se lit pas de l'extérieur |
VULN-10 dépendances à CVE | non | la version d'une bibliothèque interne n'est pas dans la réponse |
VULN-04 IDOR | non | il 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.
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.
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.
Intention — poser le DAST comme le troisième sommet du triangle et, surtout, faire toucher du doigt sa dépendance à l'exploration. Le message qui doit rester : le DAST apporte la preuve (la charge, la réponse) là où le SAST donnait un emplacement, et il est aveugle exactement là où le SAST voyait tout — le secret en dur, le md5, la CVE de dépendance.
Ce qui coince
- Passif et actif confondus. Tant que la distinction n'est pas nette, le rappel légal reste abstrait. La rendre concrète : le passif lit les réponses, l'actif fabrique des requêtes hostiles.
- Le spider pris pour exhaustif. Il ne l'est jamais. Signe : « ZAP n'a pas testé
/config». Bien sûr — rien ne pointe vers lui. D'où l'import d'une spécification OpenAPI. - La croyance que le DAST rend le SAST inutile puisqu'il « prouve ». Ressortir la ligne
VULN-05: un secret en dur ne produit aucune réponse HTTP observable.
Questions à anticiper
- « Pourquoi ne pas tout faire en actif, c'est plus complet ? » — parce que l'actif modifie l'état, prend du temps, et se lance sous conditions légales strictes. Le passif tourne à chaque build ; l'actif, sur autorisation et périmètre.
- « ZAP contre Juice Shop, c'est autorisé ? » — oui : Juice Shop tourne sur son poste, en local, c'est une cible délibérément vulnérable qu'il a lancée lui-même. C'est exactement le cadre du premier jour, à re-verbaliser.
Ce qu'il faut exiger — qu'il annonce, avant le TP, deux vulnérabilités que le DAST trouvera (VULN-02, VULN-09) et deux qu'il ne trouvera pas (VULN-05, VULN-10), en justifiant par « observable ou non depuis l'extérieur ». C'est la vérification directe de la prédiction du premier jour.
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 ?
Intention — vérifier que passif/actif est net (q1), que la dépendance à l'exploration est comprise (q2), que la complémentarité des trois familles est acquise et non récitée (q3, q5), et que le rappel légal tient sous l'angle de l'outil (q4). q3 et q5 sont celles qui portent la thèse du module.
Ce qui coince
- q3 ratée : la complémentarité n'est pas acquise, et le TP de synthèse de fin de journée n'aura pas de socle. À reprendre avec les deux tableaux côte à côte.
- q1 ratée : passif/actif encore flous, donc le rappel légal reste abstrait. Redanger : il pourrait lancer un actif là où il ne faut pas.
- q2 ratée : il croit le spider exhaustif, il ne comprendra pas pourquoi on importe une OpenAPI au TP suivant.
Questions à anticiper
- « La 5 est un piège ? » — non, c'est le cœur du module : une seule famille ne couvre pas un tiers du risque, elle couvre tout d'un tiers et rien du reste.
Ce qu'un échec révèle — 3/5 avec q3 juste : laissez partir. q3 fausse : dix minutes sur les deux tableaux avant le TP, sinon la synthèse de ce soir sera creuse.
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.
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.
- 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/ - 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 : $?" - Ouvrez
artefacts/zap-baseline-devsecops-2026-08-10.htmldans 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. - 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 - 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.
- Notez dans votre rapport DAST naissant (
rapports/dast-devsecops-lab.md) deux choses : l'alerte dedevsecops-labqui 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.
- Le scan passif n'a envoyé aucune charge d'attaque. Pourquoi peut-on alors le faire tourner en production, contrairement au scan actif ?
VULN-09est 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 ?
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.
Intention — installer le passif comme un scan sans risque et sans conséquence, et faire sentir l'écart de richesse entre une appli de 130 lignes et une appli réelle. Le passif sur devsecops-lab confirme surtout VULN-09 (bannière Werkzeug, en-têtes de sécurité absents) — la jonction avec ce que le SAST et Trivy diront de la même faille.
Ce qui coince
- Le code de sortie non nul lu comme un échec. C'est LE malentendu du jour : le désamorcer tde suite.
- Le montage
/zap/wrkoublié ou mal orthographié : le scan tourne, aucun rapport n'apparaît, et il conclut que ZAP est cassé. localhostà la place du nom de service.Connection refuseddepuis le conteneur ZAP.- Le passif sur Juice Shop pris pour un scan complet : il ne l'est pas, et c'est justement l'occasion de dire que le fond de Juice Shop, c'est le M5.
Questions à anticiper
- « Pourquoi tant d'alertes sur Juice Shop et si peu sur la mienne ? » — parce que Juice Shop a une vraie surface : cookies, en-têtes, scripts tiers, stockage local. C'est le bon moment pour annoncer M5.
- « Le passif a-t-il attaqué ? » — non, aucune charge envoyée. C'est ce qui le rend lançable partout, y compris en production.
Ce qu'il faut exiger — les deux rapports HTML dans artefacts/, et qu'il pointe dans celui de devsecops-lab l'alerte qui correspond à VULN-09. Pas « il y a des alertes » : laquelle est VULN-09, et pourquoi le passif la voit.
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.
À 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.
- 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 : $?" - 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'estVULN-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-01a été corrigée au M1, et l'actif le confirme. - Écrivez la carte de l'API dans
openapi.yaml, à la racine du dépôt. Sans elle,/ping,/configet/loginresteraient 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 } - 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 - Comparez la couverture des deux scans actifs : le scan par exploration a testé ce qui était relié, le scan d'API a testé
/pinget/loginen 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à. - 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 - 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. - 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.
- 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é ?
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.
Intention — c'est l'étape qui matérialise la thèse du module. Trois gestes : l'actif prouve VULN-02 (là où le SAST se contentait de soupçonner) ; le scan d'API montre qu'on pilote par spécification et non par exploration ; mitmproxy fait voir le trafic en clair. Le tout se referme sur la vérification de la prédiction du premier jour — le moment où la complémentarité cesse d'être un discours.
Ce qui coince
- L'impatience sur l'actif : il l'interrompt et conclut qu'il ne trouve rien. Le laisser finir.
- Le scan d'API lancé sans avoir écrit
openapi.yaml, ou avec unservers:pointant surlocalhostau lieu du nom de service. Rien n'est scanné, aucune erreur claire. - mitmproxy : le
-xde curl oublié, la requête part en direct sans traverser le proxy, et les journaux sont vides. Vérifier que l'URL du proxy est bienlocalhost:8080(port publié) et la cible le nom de service. - La preuve du XSS survolée. Exiger qu'il montre la charge et la réponse, pas juste le nom de l'alerte.
Questions à anticiper
- « L'actif a-t-il trouvé
VULN-01(SQLi) ? » — non, elle est corrigée depuis le M1 : c'est la preuve, en creux, que la correction tient. Excellent point à faire verbaliser. - « Pourquoi mitmproxy alors que ZAP est déjà un proxy ? » — pour le geste manuel d'inspection et de modification à la volée, plus léger et scriptable. Et pour introduire l'idée que Burp Community occupe la même niche côté interface graphique.
- « ZAP n'a pas trouvé
VULN-04? » — non, et aucun outil ne le fera. C'est la ligne à retenir de tout le module.
Ce qu'il faut exiger — la preuve de VULN-02 montrée (charge + réponse), le scan d'API qui a bien couvert /ping et /login (non atteignables au spider), une capture d'inspection mitmproxy, et le tableau de prédiction du premier jour coché en entier, écarts commentés.
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
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.
Les quatre portes, leur seuil, et le motif de chaque seuil
| Porte | Seuil de blocage | Motif du seuil |
|---|---|---|
secrets (Gitleaks) | toute fuite bloque | un secret est binaire — il fuit ou non ; aucun faux positif ne justifie de laisser passer une clé |
sast (Semgrep) | gravité ERROR seulement | les règles ERROR sont à haute confiance ; les WARNING (dont le XSS) avertissent sans bloquer, pour ne pas noyer l'équipe |
sca (Trivy) | HIGH+ corrigeable | on 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 confiance | l'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 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é.
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.
Intention — transformer trois jours d'outillage en une doctrine de blocage défendable. C'est la leçon la plus « architecte » du module : chaque seuil est un arbitrage écrit, pas un maximalisme. Elle prépare directement le TP 13 et le point de contrôle de fin de module.
Ce qui coince
- Le maximalisme : « on bloque tout ce qui est HIGH partout ». Le contre-argument est empirique et imparable : la porte trop stricte est contournée, et une porte contournée ne protège plus rien tout en rassurant.
- La confusion entre gravité (propriété de la faille) et confiance (propriété de la détection). On bloque sur le croisement des deux, plus la corrigeabilité — trois conditions, pas une.
- Le DAST bloquant sur tout : l'actif a des faux positifs et modifie l'état. Dans le pipeline, on cible une règle à haute confiance (le XSS réfléchi) plutôt que d'échouer sur chaque avertissement passif.
Questions à anticiper
- « Qui décide du seuil ? » — l'équipe, écrit, révisable. Ce n'est pas une constante universelle : une banque et un site vitrine n'ont pas le même. La bonne réponse est « ça se décide et ça s'écrit », pas un chiffre.
- « Et les Medium, on les ignore ? » — non : ils avertissent, ils sont tracés, ils se traitent en fenêtre de maintenance. Bloquer n'est pas la seule action possible.
Ce qu'il faut exiger — qu'il énonce pour chacune des quatre portes le seuil et le motif du seuil, en une phrase, et qu'il distingue explicitement gravité et confiance. Sans le motif, ce n'est qu'une valeur recopiée.
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.
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.
- Reprenez la branche de correction et ajoutez le job
dastà.github/workflows/ci.yml, sousjobs:. 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 - 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 enFAILet 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) - Committez, poussez, et regardez le pipeline. Le job
dastdoit être rouge :VULN-02est 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 - Corrigez
VULN-02dansapp.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) - 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. Ajoutezimport ipaddressen 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 - Puis
VULN-06etVULN-07: sha-256 à la place de md5,safe_loadà la place deload.@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)) - 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 - 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 | - 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
nosemgrepnu posé un jeudi soir — donne le même vert au pipeline et aucune de ces garanties. - 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 : $?" - 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 - 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.
- La porte
dastne 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-02par un gabarit fixe plutôt que par un échappement manuel. Pourquoi ce choix est-il plus robuste face au prochain développeur ? - Le job
dastfait tourner l'application à chaque exécution. Quel est le coût de cette porte comparé aux trois autres, et comment le justifieriez-vous ?
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.
Intention — refermer les trois jours par la boucle rouge→vert sur la porte DAST, et corriger les dernières failles de code que les trois familles ont trouvées (VULN-02/03/06/07). C'est le moment où le pipeline complet — quatre portes — passe au vert pour la première fois. La fusion consolidée vient au TP 14.
Ce qui coince
- L'indentation du job
dastdans le YAML : même niveau que les trois autres, sousjobs:. - Le
zap-regles.confmal formaté : ce sont des tabulations entre les colonnes, pas des espaces. Un fichier à espaces est ignoré en silence et la porte ne bloque sur rien. - La correction du XSS par échappement manuel (
markupsafe.escapepartout) plutôt que par gabarit fixe. Ça marche, mais le bon geste est de laisser Jinja faire son travail : gabarit constant, valeur en variable. - Corriger VULN-02 mais oublier que le job
sastreste rouge tant que VULN-03/07 sont là. Les quatre portes doivent être vertes ensemble.
Questions à anticiper
- « Pourquoi ne bloquer que sur 40012 et pas sur tout ? » — c'est la leçon L7 : l'actif a des faux positifs, on bloque sur la seule règle dont la preuve est établie. Le reste avertit.
- « Faire tourner l'appli dans la CI, ce n'est pas lourd ? » — si, un peu ; c'est le prix du DAST, et c'est pourquoi on le cible (api-scan sur une spec) plutôt que de lancer un full-scan à chaque commit.
Ce qu'il faut exiger — le pipeline observé rouge sur le job dast avant correction, puis vert sur les quatre portes après ; la correction de VULN-02 faite par gabarit fixe ; et la vérification locale du XSS (disparu) avant le push.
Point bloquant, à vérifier vous-même — grep -c nosemgrep app.py doit répondre 0, et la section 5 du README doit porter les quatre exceptions en statut levée avec la date du jour. C'est le geste que le module 1 lui avait promis pour le 10 août ; s'il ne le fait pas, son pipeline est vert avec quatre suppressions mortes dans le code, et il ne le sait pas. Ne pas valider la journée sans ce contrôle.
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.
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.
- 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. - É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. - Recopiez enfin le tableau des quatre portes —
secrets,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. - 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 - Vérifiez sur
mainque 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.
- 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 ?
Intention — cristalliser la thèse du module en un livrable court et opposable, et fermer la boucle Git par la fusion consolidée sur les quatre portes vertes. C'est aussi le matériau que l'encadrant utilisera à la revue de fin de module qui suit immédiatement.
Ce qui coince
- La note de synthèse réduite à un résumé de commandes. Ce qu'on attend, c'est le raisonnement : où chaque famille est aveugle, pourquoi on cumule les trois, et quel seuil pour quelle porte avec quel motif.
- Le tableau des seuils recopié de la leçon sans le motif. Le motif est le livrable.
- La fusion faite sans vérifier que les quatre checks sont verts, ou l'ordre des deux branches (feat/triage-sast, fix/vuln-10-deps) qui produit un conflit sur
ci.yml. Bon exercice de rebase, à accompagner s'il bloque.
Ce qu'il faut exiger — la note rapports/dast-devsecops-lab.md avec le tableau récapitulatif VULN × famille (coché d'après ses mesures, pas d'après la prédiction), les quatre seuils motivés, et main à jour avec un pipeline vert de bout en bout.
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
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.
Intention — c'est la revue de fin de module, deux heures, pas le point quotidien. On valide que la thèse (trois familles, trois angles morts) est comprise et défendable, que le pipeline complet tient, et que les livrables sont opposables. Partir de la note de synthèse et du dépôt, pas d'un discours.
Déroulé proposé (2 h)
- 20 min — il présente sa note
dast-devsecops-lab.mdet défend le tableau VULN × famille. Question pivot : « quelle faille aucune famille ne trouve, et qu'en concluez-vous sur l'outillage ? » La bonne réponse mentionne VULN-04 et la place irréductible de la revue humaine. - 30 min — démonstration live du pipeline. Il réintroduit le XSS (
git revertou édition), pousse, montre le jobdastrouge, puis recorrige et montre le vert aux quatre portes. C'est le cœur de la validation. - 30 min — il défend ses quatre seuils, motif par motif, et distingue gravité et confiance sur l'exemple du XSS (WARNING au SAST, FAIL au DAST). Puis il rejoue une règle Semgrep
--testet un tri d'atteignabilité au hasard. - 25 min — revue des trois rapports (SAST, SCA, DAST) : rejouabilité, chiffres, exceptions datées. Vérifier une
.trivyignore.yamlligne par ligne. - 15 min — bilan, ce qui part au M3 (durcissement image, VULN-09), verdict.
Ce qui distingue un module acquis d'un module exécuté — il sait dire, sans notes, pourquoi on cumule trois familles (angles morts complémentaires), pourquoi un seuil trop strict est contre-productif, et pourquoi une exception sans date est un oubli. S'il récite les commandes mais bute sur ces trois « pourquoi », le module est à consolider avant le M3.
Ne pas valider si — le pipeline ne passe pas au vert aux quatre portes en direct, ou si l'un des trois rapports n'est pas rejouable (versions ou chiffres manquants). Ce sont les deux livrables non négociables du module.