hors suivi
Leçon

Ce que vous avez le droit d'attaquer — et vos deux cibles

Jour 1 · Poser le décor, le droit et le dépôt · 25 min

Ce que vous avez le droit d'attaquer — et rien d'autre

Ceci n'est pas une précaution de style, c'est une règle de droit. En France, accéder ou se maintenir dans un système de traitement automatisé de données sans autorisation est un délit (articles 323-1 et suivants du Code pénal), puni même en cas de simple intrusion sans dommage. Les outils que vous allez apprendre — sqlmap, ffuf, nuclei, ZAP — sont des outils d'attaque réels. Employés sur un système qui ne vous appartient pas et pour lequel vous n'avez pas d'autorisation écrite, ils constituent une infraction.

Pendant tout ce parcours, vous n'attaquez QUE des applications délibérément vulnérables que vous déployez vous-même, sur votre propre poste : devsecops-lab, que vous écrivez aujourd'hui, et Juice Shop, que vous lancez en conteneur local. Elles tournent sur localhost, isolées, et sur elles seules.

Trois interdits absolus, sans exception :

  • jamais sur Internet — pas de scan d'un site public, même « pour voir », même s'il « a l'air vulnérable » ;
  • jamais sur une ressource de Caplogy — pas de serveur, pas d'application, pas de réseau de l'entreprise, même en tant que salarié ;
  • jamais sur le système d'un tiers sans mandat écrit — un test d'intrusion réel repose toujours sur une autorisation formelle et un périmètre signé.

Un pentester professionnel travaille avec un contrat, un périmètre et une fenêtre de temps. Ici, votre « contrat » est ce paragraphe : votre périmètre est votre poste, votre fenêtre est la formation. Toute manipulation du parcours respecte ce cadre — si une page semble vous demander de viser autre chose que vos deux cibles locales, c'est la page qui a tort.

Pourquoi c'est la première leçon et pas une annexe

Un ingénieur qui apprend sqlmap apprend en même temps une capacité de nuisance. Ce qui sépare le professionnel du délinquant n'est ni la compétence ni l'intention : c'est l'autorisation écrite. C'est la seule différence, et elle est intégralement documentaire.

Deux conséquences pratiques, que vous retrouverez dans votre métier :

  • Un test d'intrusion commence toujours par un document — périmètre, adresses IP, fenêtre horaire, contacts d'escalade, ce qui est explicitement hors champ. Sans lui, on ne tape pas la première commande.
  • Un accès légitime n'est pas un mandat de test. Vous êtes salarié de Caplogy : vous avez le droit d'utiliser ses applications, pas de les attaquer. Ce sont deux autorisations distinctes, et la seconde ne se déduit jamais de la première.

Vos deux cibles

votre poste — et rien d'autre devsecops-lab votre code · votre dépôt Flask · 10 failles VULN-01..10 vous cassez, vous corrigez localhost:5000 Juice Shop boîte noire · image officielle v17.3.0 · vous n'avez pas le code vous attaquez, jamais vous ne corrigez localhost:3000 réseau Docker dso-net
Deux cibles, deux rôles. Celle de gauche vous apprend à construire sûr, celle de droite à casser. Les deux tournent sur votre poste, et sur lui seul.
Vos deux cibles

Vous travaillerez sur deux applications, et il ne faut jamais les confondre. devsecops-lab est la vôtre : vous l'écrivez aujourd'hui, elle contient dix failles délibérées, et tout le fil rouge consiste à les détecter automatiquement puis à les corriger — c'est votre dépôt, votre pipeline, votre responsabilité. Juice Shop est une cible d'entraînement que vous attaquez sans en posséder le code : vous la lancez sur votre poste, vous la cartographiez, vous l'exploitez, mais vous ne corrigez jamais son code source. La première vous apprend à construire sûr ; la seconde à casser. Les deux tournent sur votre machine, et sur elle seule.

<code>devsecops-lab</code>Juice Shop
NatureApplication Flask d'environ 130 lignes, que vous écrivezApplication Node.js complète, image officielle
Vous en avez le codeOui — c'est votre dépôt GitNon — boîte noire, vous ne le lisez pas
Versiondevsecops-lab:dev puis :hardenedbkimminich/juice-shop:v17.3.0
Adressehttp://localhost:5000http://localhost:3000
Ce que vous y faitescasser, détecter, corriger, durcir, déployerreconnaître, exploiter, rapporter — jamais corriger
ModulesM1 → M4 et le projet fil rougeM2 (DAST), M5 (pentest), projet fil rouge

La trace que vous laissez dès ce soir

Votre dépôt contiendra un fichier journal-incidents.md. Sa toute première ligne n'est pas un incident : c'est votre périmètre. Vous l'écrirez au TP 4 de cet après-midi, et elle ne bougera plus de quinze jours.

Perimetre autorise : localhost uniquement — devsecops-lab (5000) et juice-shop (3000).

Ce n'est pas une formalité. Le jour où vous rédigerez un vrai rapport de pentest, au module 5, la section « périmètre et méthodologie » sera la deuxième page du document — juste après le résumé exécutif, et avant le premier finding. Vous commencez à l'écrire aujourd'hui.

Pour vous repérer

La question qui vient toujours, et à laquelle la réponse est non : « je peux quand même lancer un scan passif sur un site public, je ne fais que regarder ? ». Non. Un scan est une série de requêtes que vous envoyez à un système qui ne vous a rien demandé. Le droit français ne distingue pas le regard de la main.

Deuxième piège, plus insidieux : Caplogy est votre employeur, vous y avez des accès légitimes. Un accès légitime pour travailler n'est pas une autorisation de test d'intrusion. Ce sont deux mandats différents, et seul le second s'écrit et se signe.

✓ terminé
Leçon

DevSecOps : pourquoi déplacer la sécurité vers la gauche

Jour 1 · Poser le décor, le droit et le dépôt · 30 min

Pendant vingt ans, la sécurité applicative a fonctionné comme un péage : on construisait, puis un auditeur passait, trois semaines avant la mise en production, et rendait un rapport de soixante pages. Le rapport arrivait au moment où plus personne ne pouvait rien changer sans décaler la date. On corrigeait les deux failles les plus voyantes, on notait le reste dans un tableau, et le tableau vivait sa vie.

Le DevSecOps est la réponse à ce constat, et elle tient en une phrase : le contrôle doit s'exécuter au moment où corriger ne coûte encore presque rien, et il doit avoir le pouvoir d'arrêter la chaîne.

j'écris le code c'est en production éditeur pre-commit M1 · jour 2 pipeline CI M1 · jour 3 le vrai verrou recette / DAST M2 incident M4 · réponse Hauteur de la barre = ce que coûte la correction Le contrôle ne change pas — c'est le moment où il s'exécute qui change tout
Décaler vers la gauche, ce n'est pas contrôler plus : c'est contrôler plus tôt, là où corriger ne coûte encore qu'une ligne.

Pourquoi le moment change tout

Une injection SQL trouvée dans votre éditeur, c'est une ligne à réécrire : vous avez encore le contexte en tête, personne d'autre n'a vu le code, aucune donnée n'existe. La même injection trouvée en production, c'est un correctif à écrire en urgence, une fenêtre de déploiement à négocier, des journaux à analyser pour savoir si elle a été exploitée, et peut-être une notification à la CNIL sous 72 heures.

Les chiffres qui circulent — un facteur 10, 30, 100 selon la phase — sont des ordres de grandeur, pas des lois. Ce qui compte est le mécanisme, et il est toujours le même :

  • le contexte se perd — dans six mois, plus personne ne saura pourquoi cette requête est construite ainsi ;
  • le correctif devient un projet — il faut le planifier, le tester, le déployer, coordonner ;
  • l'exposition est déjà réelle — les données sont en base, les clients sont connectés, l'incident a peut-être déjà eu lieu sans qu'on le sache.

Les trois boucles

« Shift left » ne veut pas dire « tout au plus tôt » : cela veut dire que chaque type de contrôle vit à l'endroit où il est le plus utile. Trois boucles se superposent, avec trois latences très différentes.

BoucleLatenceCe qu'elle attrapeCe qu'elle ne peut pas attraper
Localeéditeur, pre-commitquelques secondessecrets, motifs de code dangereux, gros fichierstout ce qui dépend de l'application en marche
Intégrationpipeline CI sur la pull requestquelques minutesSAST, secrets sur tout le dépôt, dépendances à CVE, imageles failles de logique métier, la configuration réelle
Exécutionrecette, préproduction, productioncontinuDAST, en-têtes, comportement réel, dérive de configurationce qui n'est jamais atteint par le trafic de test

Le module 1 construit les deux premières boucles : la locale au jour 2, l'intégration au jour 3. Le module 2 ajoutera la troisième avec ZAP, et le module 3 la portera sur l'image et le cluster.

Le verrou : un contrôle qui n'échoue pas n'est pas un contrôle

La distinction qui structure tout le parcours

Un outil de sécurité produit deux choses, qu'il ne faut jamais confondre :

  • un artefact — un rapport, un SARIF, un SBOM. Il informe, il s'archive, il se compare d'une semaine sur l'autre. Il ne bloque rien.
  • un verrou (gate) — un code de sortie non nul qui fait échouer le job, donc échouer la vérification obligatoire, donc interdire la fusion.

Semgrep sans --error produit un artefact. Semgrep avec --error produit un verrou. C'est le même outil, la même sortie, la même seconde de calcul — et deux dispositifs de sécurité qui n'ont rien à voir.

« Security as code » : la règle vit dans le dépôt

La deuxième idée du DevSecOps est moins spectaculaire mais tout aussi structurante : les règles de sécurité cessent d'être une procédure sur un intranet pour devenir des fichiers versionnés dans le dépôt du projet.

Vous en écrirez cinq pendant ce module : .gitignore, .pre-commit-config.yaml, .gitleaks.toml, .github/workflows/ci.yml, et le journal-incidents.md qui trace les exceptions. Ce qu'ils gagnent à vivre là :

  • ils se relisent en revue — un durcissement de règle passe par une pull request, comme le reste ;
  • ils se testent — on sait si la règle attrape ce qu'elle prétend attraper, parce qu'on lui donne un cas ;
  • leur historique répond à la question qui compte — qui a désactivé cette règle, quand, et avec quelle justification.

C'est exactement ce que ne permet pas une procédure PDF : on ne peut pas répondre à la troisième question.

Ce que le DevSecOps n'est pas

  • Ce n'est pas empiler des scanners. Trois outils qui trouvent la même chose et n'arrêtent rien valent moins qu'un seul qui fait échouer le build.
  • Ce n'est pas transférer la sécurité aux développeurs. Le développeur corrige parce qu'il est le seul à pouvoir le faire vite ; l'équipe sécurité outille, calibre les règles et arbitre les exceptions. La responsabilité se partage, elle ne se déplace pas.
  • Ce n'est pas le zéro faille. C'est un dispositif où l'on sait ce qu'on laisse passer, pourquoi, et jusqu'à quand. Vous en ferez l'expérience au jour 3 : votre pipeline passera au vert avec quatre failles encore ouvertes — datées, justifiées, assignées au module 2.
✓ terminé
Quiz

Quiz — Cadre légal, shift-left et security as code

Jour 1 · Poser le décor, le droit et le dépôt · 10 min

1. Un site public affiche une bannière de version manifestement obsolète. Vous lancez nuclei dessus pour confirmer, sans rien exploiter. Où en êtes-vous ?

2. Laquelle de ces phrases décrit correctement le partage entre vos deux cibles ?

3. Qu'est-ce qui distingue un contrôle « décalé à gauche » d'un audit de fin de projet ?

4. Votre pipeline exécute Semgrep, publie un rapport SARIF très lisible, et se termine toujours en succès. Que lui manque-t-il ?

5. Qu'apporte le fait d'écrire une règle de sécurité dans un fichier versionné du dépôt plutôt que dans une procédure interne ?

✓ terminé
Travaux pratiques

TP 1 — Le réseau dso-net et l'outillage du parcours

Jour 1 · Poser le décor, le droit et le dépôt · 40 min

Poser le décor Docker de quinze jours et tirer les images épinglées, une fois pour toutes.

Ce dont vous avez besoin

Docker en version 24 ou supérieure, Git 2.40 ou supérieur, Python 3.12 — installés au module d'accueil. Aucun matériel, aucun compte pour l'instant : le compte GitHub attendra demain.

Le dossier que vous créez maintenant est celui de tout le parcours, jusqu'à la soutenance du 21 août.

  1. Vérifiez votre outillage. Les trois commandes doivent répondre — si l'une échoue, réglez-la maintenant plutôt qu'au milieu d'un TP.
    docker --version      # attendu : Docker version 24.x ou superieur
    git --version         # attendu : git version 2.40 ou superieur
    python3 --version     # attendu : Python 3.12.x
  2. Créez le réseau Docker du parcours. Tous les conteneurs des cinq modules le rejoindront, pour se joindre par leur nom plutôt que par une adresse IP qui change à chaque redémarrage. C'est le tout premier geste, et il ne se refait jamais.
    docker network create dso-net
    docker network ls | grep dso-net
  3. Créez le dossier de travail. Il contiendra votre dépôt et, à côté, les sorties d'outils que vous ne versionnerez pas.
    mkdir -p ~/poei-s4
    cd ~/poei-s4
    pwd    # notez ce chemin, toutes les commandes en partent
  4. Tirez les trois images épinglées du module. Comptez plusieurs minutes : autant les avoir maintenant, pendant qu'aucun exercice n'attend. Les versions sont fixées pour tout le parcours — c'est ce qui rendra votre démonstration du 21 août identique à celle d'aujourd'hui.
    docker pull zricethezav/gitleaks:v8.18.4
    docker pull semgrep/semgrep:1.85.0
    docker pull bkimminich/juice-shop:v17.3.0
  5. Vérifiez que les trois sont bien là, avec leur étiquette. Une image tirée sans étiquette explicite serait latest — c'est-à-dire une version différente la semaine prochaine.
    docker images | grep -E 'gitleaks|semgrep|juice-shop'
  6. Éprouvez Gitleaks à vide, pour confirmer qu'il s'exécute. Retenez la forme de cette commande : on monte un répertoire dans le conteneur, jamais un fichier. Un montage sur un fichier qui n'existe pas encore fait créer par Docker un répertoire vide du même nom, et l'outil échoue sans rien expliquer.
    docker run --rm zricethezav/gitleaks:v8.18.4 version
  7. Vous avez maintenant un réseau, un dossier et trois outils. Rien de tout cela ne sera recréé : les jours suivants s'y ajoutent, ils ne le refont pas.
Questions de réflexion — à préparer par écrit
  • Épingler zricethezav/gitleaks:v8.18.4 plutôt que latest vous protège d'un incident, et vous en expose à un autre. Lesquels ?
  • Le réseau dso-net permet à un conteneur d'en joindre un autre par son nom. Quel intérêt cela aura-t-il quand un scanner tournera lui aussi en conteneur ?
Pour vous repérer

Cannot connect to the Docker daemon ne veut pas dire que votre commande est fausse : Docker Desktop n'est pas démarré. Sous macOS, lancez l'application et attendez que la baleine cesse de s'animer.

Le réseau dso-net se crée une seule fois pour les quinze jours. Si docker network create répond already exists, tout va bien : c'est qu'il est déjà là. Ne le supprimez jamais — les conteneurs qui y sont attachés perdraient la possibilité de se joindre par leur nom, et vous chercheriez la panne dans la mauvaise direction pendant une heure.

✓ terminé
Travaux pratiques

TP 2 — Juice Shop : lancer la cible et la reconnaître

Jour 1 · Poser le décor, le droit et le dépôt · 50 min

Déployer la cible boîte noire du parcours et pratiquer une première reconnaissance strictement passive.

Rappel de périmètre

Tout ce qui suit vise localhost:3000, un conteneur qui tourne sur votre poste. Aucune des commandes de ce TP ne doit jamais être pointée vers une adresse que vous n'hébergez pas vous-même.

  1. Lancez Juice Shop sur le réseau du parcours, en publiant son port sur votre poste.
    docker run -d --name juice-shop --network dso-net \
      -p 3000:3000 bkimminich/juice-shop:v17.3.0
  2. Suivez le démarrage dans un second terminal et attendez la ligne qui annonce l'écoute. Ne faites rien d'autre tant qu'elle n'est pas apparue.
    docker logs -f juice-shop
    # attendu, apres 20 a 40 secondes :
    #   info: Server listening on port 3000
  3. Vérifiez depuis votre poste — et non depuis l'intérieur du conteneur — que le port publié répond.
    curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3000/
  4. Ouvrez http://localhost:3000 dans votre navigateur. Parcourez l'application cinq minutes en simple utilisateur : créez un compte, ajoutez un article au panier, cherchez un produit. Vous n'attaquez rien — vous prenez la mesure de la surface.
    open http://localhost:3000          # macOS
    # xdg-open http://localhost:3000    # Linux
  5. Première reconnaissance passive : lisez les en-têtes de réponse. C'est le geste d'ouverture de tout test, et il ne coûte rien à la cible.
    curl -s -D - -o /dev/null http://localhost:3000/
  6. Notez ce que vous voyez, et surtout ce que vous ne voyez pas. Cherchez la présence ou l'absence de Content-Security-Policy, Strict-Transport-Security, X-Frame-Options, X-Content-Type-Options. Un en-tête absent ne se voit pas : il faut le chercher par son nom, c'est pour cela qu'on travaille avec une liste.
  7. Consignez votre observation dans un fichier. Ce sera votre premier artefact de reconnaissance, et le module 5 le reprendra.
    mkdir -p ~/poei-s4/artefacts
    curl -s -D - -o /dev/null http://localhost:3000/ \
      > ~/poei-s4/artefacts/recon-juice-shop-2026-08-03.txt
    
    # Puis ouvrez ~/poei-s4/artefacts/recon-juice-shop.md et ecrivez, en trois lignes :
    #  - la technologie que les en-tetes trahissent
    #  - les en-tetes de securite presents
    #  - ceux qui manquent
  8. Repérez le tableau de bord des défis, à l'adresse http://localhost:3000/#/score-board. Regardez-le, comptez les catégories, et refermez-le : vous n'en résolvez aucun aujourd'hui. Le module 5 y consacre trois jours avec une méthode ; les résoudre au hasard maintenant vous priverait de la matière et de la démonstration.
  9. Arrêtez proprement le conteneur pour la nuit, et retenez la paire de commandes que vous emploierez pendant quinze jours. docker rm n'en fait pas partie.
    docker stop juice-shop
    docker start juice-shop     # demain matin, et les jours suivants
Questions de réflexion — à préparer par écrit
  • Vous n'avez pas le code de Juice Shop. Qu'est-ce que cette contrainte vous oblige à faire, que la lecture du code vous aurait épargné ?
  • Un en-tête de sécurité absent n'apparaît nulle part dans une réponse HTTP. Comment s'assure-t-on de ne pas en oublier un ?
  • Recréer le conteneur remet les défis à zéro. Quelle conséquence cela a-t-il sur la façon dont vous devrez consigner vos preuves au module 5 ?
Pour vous repérer

Juice Shop met vingt à quarante secondes à démarrer. Pendant ce temps, curl répond Connection reset et l'on croit que le lancement a échoué. Regardez docker logs -f juice-shop et attendez la ligne Server listening on port 3000 avant de conclure quoi que ce soit.

Deuxième réflexe à prendre dès aujourd'hui : docker stop et docker start, jamais docker rm. Juice Shop garde son état en mémoire ; recréer le conteneur remet tous les défis à zéro. Ce n'est pas grave aujourd'hui — ce le sera au module 5, en pleine session d'exploitation.

✓ terminé
Leçon

Les dix failles de devsecops-lab : CWE, OWASP, et qui les trouvera

Jour 1 · Poser le décor, le droit et le dépôt · 25 min

Vous allez écrire, dans une heure, une application Flask d'environ cent trente lignes qui contient dix failles délibérées. Elles ne sont ni aléatoires ni décoratives : chacune est là pour être trouvée par un outil précis, à un moment précis du parcours, et refermée à un module précis.

N'anticipez aucune correction

La tentation sera forte, en tapant la requête SQL du /login, de la paramétrer au passage : vous savez le faire. Ne le faites pas. Un pipeline vert dès le premier jour ne démontre rien ; le jour 3 tient tout entier sur le fait que votre pipeline échoue d'abord.

Même consigne pour requirements.txt : ses versions sont choisies pour porter de vraies CVE, et c'est la matière de l'analyse de composition du module 2. Ne les montez pas.

La table des dix

IDFailleEmplacementCWEOWASP 2021Détectée parRefermée en
VULN-01Injection SQL/loginCWE-89A03Semgrep, puis sqlmapM1 J3
VULN-02XSS réfléchi/searchCWE-79A03Semgrep, ZAPM2
VULN-03Injection de commande/pingCWE-78A03Semgrep, BanditM2
VULN-04IDOR/profileCWE-639A01pentest manuelM5
VULN-05Secret en durapp.py, .envCWE-798A07GitleaksM1 J2
VULN-06Hachage faible (md5)/hashCWE-327A02Semgrep, BanditM2
VULN-07Désérialisation non sûre/configCWE-502A08Semgrep, BanditM2
VULN-08SSRF/fetchCWE-918A10Semgrep, ZAPM5
VULN-09debug=True, en-têtes absents__main__, DockerfileCWE-489A05Bandit, ZAP, TrivyM3
VULN-10Dépendances à CVErequirements.txtCWE-1104A06Trivy (SCA)M2

CWE et OWASP Top 10 : deux objets qu'on confond tout le temps

Les deux colonnes du milieu ne disent pas la même chose, et un rapport de pentest qui les mélange se fait reprendre.

CWEOWASP Top 10
Ce que c'estUne taxonomie des types de faiblesses logicielles, maintenue par le MITREUn classement des dix catégories de risque les plus répandues, publié tous les quelques années
TaillePlusieurs centaines d'entrées, très finesDix catégories larges, chacune regroupant de nombreuses CWE
Ce à quoi ça sertNommer précisément la faiblesse dans un findingCommuniquer une priorité, structurer un programme, parler à un décideur
ExempleCWE-89 — Improper Neutralization of Special Elements used in an SQL CommandA03:2021 — Injection, qui contient CWE-89, CWE-79, CWE-78 et bien d'autres

Concrètement : dans le rapport que vous rédigerez au module 5, chaque finding portera les deux. La CWE dit au développeur exactement quoi corriger ; la catégorie OWASP dit au responsable où se situe le risque dans le paysage.

Pourquoi les commentaires VULN-NN restent dans le code

Chaque faille porte en commentaire son identifiant. On pourrait croire qu'il faut les retirer pour « ne pas tricher ». C'est l'inverse :

  • ils vous permettent de vérifier ce que l'outil a manqué — le silence d'un scanner sur une faille que vous savez présente est l'information la plus utile du parcours ;
  • ils rendent le dépôt lisible par votre encadrant et par le jury, qui n'ont pas à jouer aux devinettes ;
  • ils donnent un identifiant stable à chaque correction : votre branche s'appellera fix/vuln-01-sqli, votre commit fix(VULN-01): requete SQL parametree, et votre journal parlera de VULN-01. Une même faille, un seul nom, du premier jour à la soutenance.
Un exercice de lecture avant le TP

Dans la table ci-dessus, repérez la seule catégorie du Top 10 OWASP 2021 qui n'apparaisse dans aucune ligne. Vous en trouverez la raison au module 4 : toutes les familles de risque ne se démontrent pas dans cent trente lignes de Python — certaines sont des propriétés de l'organisation, pas du code.

✓ terminé
Travaux pratiques

TP 3 — Écrire devsecops-lab : l'application, l'image, le conteneur

Jour 1 · Poser le décor, le droit et le dépôt · 1 h 45

Poser le fil rouge des quinze jours : l'application vulnérable, son image et son conteneur, et éprouver trois failles à la main.

Ce que vous construisez maintenant

Ce dossier est le fil rouge des quinze jours. Tous les modules s'y réfèrent : le M2 y corrige cinq failles, le M3 en durcit l'image et la déploie sur un cluster, le M4 en fait le modèle de menaces, le projet fil rouge en présente l'audit complet. Ne le déplacez pas, ne le renommez pas.

  1. Créez le dépôt et placez-vous dedans. Toutes les commandes de ce TP et des suivants partent de ce répertoire.
    cd ~/poei-s4
    mkdir -p devsecops-lab
    cd devsecops-lab
  2. Créez un environnement virtuel Python et activez-le. Sans lui, les versions à CVE que vous allez installer iraient polluer votre Python système — et les distributions récentes refusent purement et simplement l'installation.
    python3 -m venv .venv
    source .venv/bin/activate        # macOS et Linux
    # .venv\Scripts\activate       # Windows PowerShell
    
    python -c "import sys; print(sys.prefix)"   # doit pointer dans .venv
  3. Créez requirements.txt. Ces versions sont délibérément anciennes : elles portent de vraies vulnérabilités publiées, et c'est la matière de l'analyse de composition du module 2. Ne les montez pas.
    Flask==2.0.1
    Werkzeug==2.0.1
    Jinja2==2.11.3
    PyYAML==5.3.1
    requests==2.25.1
    urllib3==1.26.4
    
  4. Installez les dépendances. L'installateur peut vous avertir que certaines versions sont obsolètes : c'est exactement l'effet recherché, et vous relirez ces avertissements au module 2 avec Trivy.
    pip install -r requirements.txt
  5. Créez app.py et recopiez ce code à l'identique, commentaires compris. Chaque bloc porte l'identifiant de sa faille : ces commentaires sont la carte que les outils viendront confirmer, ils restent dans le fichier.
    #!/usr/bin/env python3
    """devsecops-lab — application deliberement vulnerable. NE JAMAIS exposer sur un reseau."""
    import hashlib
    import os
    import sqlite3
    import subprocess
    
    import requests
    import yaml
    from flask import Flask, request, render_template_string, g
    
    app = Flask(__name__)
    # VULN-05 : secret code en dur dans la source (a detecter par gitleaks).
    app.config["SECRET_KEY"] = "s3cr3t-dev-key-ne-pas-committer-42"
    API_KEY = "AKIAIOSFODNN7EXAMPLE"  # VULN-05 : faux identifiant AWS en dur
    
    DB = "lab.db"
    
    
    def db():
        if "db" not in g.__dict__:
            g.db = sqlite3.connect(DB)
        return g.db
    
    
    @app.route("/")
    def index():
        return "devsecops-lab — cible d'entrainement locale"
    
    
    @app.route("/login", methods=["POST"])
    def login():
        # VULN-01 : injection SQL — la requete est construite par concatenation.
        user = request.form.get("user", "")
        pwd = request.form.get("pwd", "")
        q = "SELECT * FROM users WHERE user='%s' AND pwd='%s'" % (user, pwd)
        row = db().execute(q).fetchone()
        return ("bienvenue" if row else "refuse"), (200 if row else 401)
    
    
    @app.route("/search")
    def search():
        # VULN-02 : XSS reflechi — l'entree utilisateur est rendue sans echappement.
        terme = request.args.get("q", "")
        return render_template_string("<h1>Resultats pour " + terme + "</h1>")
    
    
    @app.route("/ping")
    def ping():
        # VULN-03 : injection de commande — shell=True sur une entree utilisateur.
        hote = request.args.get("host", "127.0.0.1")
        out = subprocess.check_output("ping -c1 " + hote, shell=True)
        return out
    
    
    @app.route("/profile/<int:uid>")
    def profile(uid):
        # VULN-04 : IDOR — aucun controle d'autorisation sur uid.
        row = db().execute("SELECT user FROM users WHERE id=%d" % uid).fetchone()
        return {"id": uid, "user": row[0] if row else None}
    
    
    @app.route("/hash")
    def weak_hash():
        # VULN-06 : hachage faible (md5) pour un mot de passe.
        return hashlib.md5(request.args.get("p", "").encode()).hexdigest()
    
    
    @app.route("/config", methods=["POST"])
    def load_config():
        # VULN-07 : deserialisation non sure — yaml.load complet permet l'execution de code.
        return str(yaml.load(request.data, Loader=yaml.Loader))
    
    
    @app.route("/fetch")
    def fetch():
        # VULN-08 : SSRF — l'URL fournie par l'utilisateur est appelee cote serveur.
        return requests.get(request.args.get("url", ""), timeout=3).text
    
    
    def init_db():
        c = sqlite3.connect(DB)
        c.execute("CREATE TABLE IF NOT EXISTS users(id INTEGER PRIMARY KEY, user TEXT, pwd TEXT)")
        c.execute("INSERT INTO users(user, pwd) VALUES('admin', 'admin123')")
        c.commit()
        c.close()
    
    
    if __name__ == "__main__":
        if not os.path.exists(DB):
            init_db()
        # VULN-09 : debug=True expose la console Werkzeug ; host 0.0.0.0 sans en-tetes de securite.
        app.run(host="0.0.0.0", port=5000, debug=True)
    
  6. Lancez l'application et vérifiez qu'elle répond. Laissez ce terminal ouvert : il sera votre console pendant tout le TP.
    python app.py
    
    # Dans un SECOND terminal, depuis le meme dossier :
    curl -s http://localhost:5000/
  7. Première faille, à la main : VULN-01, l'injection SQL. Authentifiez-vous d'abord normalement, pour établir la référence, puis sans mot de passe valide. Notez les guillemets doubles autour de la charge : elle contient une apostrophe, et votre shell la supprimerait sinon.
    # 1. Authentification legitime — attendu : bienvenue, code 200
    curl -s -w ' [%{http_code}]\n' -X POST http://localhost:5000/login \
      --data "user=admin&pwd=admin123"
    
    # 2. Mot de passe faux — attendu : refuse, code 401
    curl -s -w ' [%{http_code}]\n' -X POST http://localhost:5000/login \
      --data "user=admin&pwd=nimportequoi"
    
    # 3. Injection — attendu : bienvenue, code 200, SANS mot de passe valide
    curl -s -w ' [%{http_code}]\n' -X POST http://localhost:5000/login \
      --data "user=admin'--&pwd=x"
  8. Arrêtez-vous et écrivez la requête sur papier. Le code construit SELECT * FROM users WHERE user='%s' AND pwd='%s' par concaténation. Remplacez le premier %s par admin'-- et lisez ce que la base reçoit réellement. Tant que vous ne l'avez pas écrit vous-même, vous connaissez le nom de l'attaque, pas son mécanisme — et c'est le mécanisme que le module 5 vous demandera de reproduire sur une cible dont vous n'aurez pas le code.
  9. Deuxième faille : VULN-02, le XSS réfléchi. Le terme de recherche est réinjecté dans la page sans échappement. Observez la réponse brute : la balise ressort telle quelle, ce qui signifie qu'un navigateur l'exécuterait.
    curl -s "http://localhost:5000/search?q=<script>alert(1)</script>"
    
    # attendu, mot pour mot :
    #   <h1>Resultats pour <script>alert(1)</script></h1>
  10. Troisième faille : VULN-03, l'injection de commande. Le paramètre est concaténé dans une commande shell. Le point-virgule termine le ping et ouvre la commande suivante.
    curl -s "http://localhost:5000/ping?host=127.0.0.1"
    
    # Puis, avec une seconde commande greffee :
    curl -s "http://localhost:5000/ping?host=127.0.0.1;id"
  11. Arrêtez l'application avec Ctrl+C, puis créez le Dockerfile. Cette version est mauvaise, et c'est voulu : elle tourne en root, part d'une image complète, copie tout le répertoire dans l'image. Le module 3 la remplacera par une version durcie, après que Trivy en aura fait la démonstration. Ne corrigez rien maintenant.
    FROM python:3.12
    WORKDIR /app
    COPY . .
    RUN pip install -r requirements.txt
    EXPOSE 5000
    CMD ["python", "app.py"]
    
  12. Construisez l'image et lancez le conteneur sur le réseau du parcours. Le . final est le contexte de construction : lancez la commande depuis devsecops-lab/, jamais ailleurs.
    docker build -t devsecops-lab:dev .
    
    docker run -d --name devsecops-lab --network dso-net \
      -p 5000:5000 devsecops-lab:dev
    
    docker ps | grep devsecops-lab
  13. Vérifiez que le conteneur répond depuis votre poste, puis rejouez l'injection SQL contre lui. Le comportement doit être identique à celui du lancement local : c'est ce qui prouve que l'image contient bien votre application.
    curl -s http://localhost:5000/
    
    curl -s -w ' [%{http_code}]\n' -X POST http://localhost:5000/login \
      --data "user=admin'--&pwd=x"
  14. Dernière observation du TP, et elle prépare le module 3 : demandez au conteneur sous quelle identité il exécute votre code.
    docker exec devsecops-lab whoami
    # attendu : root
    
    docker image inspect devsecops-lab:dev --format '{{.Size}}' \
      | awk '{print $1/1024/1024 " Mo"}'
  15. root, et une image de plusieurs centaines de mégaoctets. Retenez ces deux chiffres : le module 3 vous fera produire une image :hardened et vous demanderez de les comparer. Vous ne corrigez rien aujourd'hui — vous constatez, et vous notez.
Questions de réflexion — à préparer par écrit
  • Vous avez lancé l'application deux fois : en local, puis en conteneur. Qu'est-ce que la seconde exécution prouve, que la première ne prouvait pas ?
  • L'injection SQL vous a authentifié sans mot de passe. Quelle autre conséquence, plus grave que le contournement d'authentification, la même faille rend-elle possible ?
  • Le Dockerfile copie tout le répertoire dans l'image, y compris ce que vous n'avez pas encore créé. Quel fichier, que vous écrirez au TP suivant, ne devrait surtout pas s'y trouver ?
Pour vous repérer

Sous macOS, le port 5000 est souvent déjà pris par le service AirPlay Receiver. Symptôme : Flask démarre puis rend immédiatement la main, ou curl localhost:5000 renvoie une réponse qui n'est pas la vôtre. Vérifiez avec lsof -nP -iTCP:5000 | grep LISTEN ; si un ControlCenter apparaît, désactivez le récepteur AirPlay dans les Réglages Système, section Général puis Partage.

Deuxième point : la charge d'injection SQL contient une apostrophe. Elle doit rester entre guillemets doubles dans la ligne de commande — entre apostrophes, votre shell la mangerait et vous concluriez à tort que la faille n'existe pas.

Enfin, pip install hors environnement virtuel finit par un refus de l'installateur système sur les distributions récentes. Le venv de l'étape 2 n'est pas une préférence de style.

✓ terminé
Travaux pratiques

TP 4 — Le dépôt, le .gitignore, le README et le journal

Jour 1 · Poser le décor, le droit et le dépôt · 1 h

Poser les quatre artefacts documentaires du parcours et faire le premier commit, sans y embarquer ce qui ne doit pas s'y trouver.

  1. Avant tout autre geste, créez le .gitignore. Il doit exister avant le premier git add : un fichier déjà suivi ne s'ignore plus en ajoutant une ligne ici.
    # .gitignore
    .venv/
    __pycache__/
    *.pyc
    
    # La base de donnees locale : elle se recree toute seule au demarrage.
    lab.db
    
    # Les secrets d'execution. .env.exemple, lui, EST versionne.
    .env
    
    # Les sorties d'outils : volumineuses, regenerables, et parfois bavardes.
    artefacts/
    
  2. Créez les deux fichiers d'environnement. Le premier contient de fausses valeurs et n'est pas versionné ; le second contient les mêmes clés, vides, et l'est. C'est la convention qui permet à quelqu'un de reprendre votre projet sans jamais lire vos secrets.
    # ---------- .env  (ignore par git) ----------
    APP_SECRET_KEY=valeur-locale-de-developpement-a-remplacer
    APP_API_KEY=AKIAIOSFODNN7EXAMPLE
    
    # ---------- .env.exemple  (versionne) ----------
    # Copiez ce fichier en .env et renseignez les valeurs.
    # Aucune valeur reelle ne doit figurer ici.
    APP_SECRET_KEY=
    APP_API_KEY=
    
  3. Créez journal-incidents.md. Sa première ligne n'est pas un incident : c'est votre périmètre autorisé, recopié depuis la leçon de ce matin. Elle ne bougera plus de quinze jours.
    # Journal d'incidents — devsecops-lab
    
    Perimetre autorise : localhost uniquement — devsecops-lab (5000) et juice-shop (3000).
    Jamais Internet, jamais une ressource Caplogy, jamais un tiers sans mandat ecrit.
    
    ## Convention
    
    Une entree par incident ou par exception acceptee. Chaque entree porte :
    date, identifiant (INC-NN ou VULN-NN), constat, decision, echeance, verification.
    
    ## Entrees
    
    ### 2026-08-03 — Ouverture du journal
    Perimetre pose. Dix vulnerabilites delibrees introduites dans app.py (VULN-01..10),
    cartographiees dans le README. Aucune correction anticipee.
    
  4. Créez README.md. Ce squelette de six sections vous suivra jusqu'à la soutenance : chaque module en remplit une ou deux. Écrivez dès maintenant les sections 1 et 2, et laissez les autres en titres.
    # devsecops-lab
    
    Application Flask deliberement vulnerable, support du parcours S4 (POEI 2026).
    **Ne jamais exposer sur un reseau.** Perimetre : localhost uniquement.
    
    ## 1. Lancer l'application
    
        python3 -m venv .venv && source .venv/bin/activate
        pip install -r requirements.txt
        python app.py            # http://localhost:5000
    
    En conteneur :
    
        docker build -t devsecops-lab:dev .
        docker run -d --name devsecops-lab --network dso-net -p 5000:5000 devsecops-lab:dev
    
    ## 2. Les dix vulnerabilites
    
    | ID | Faille | Emplacement | CWE | OWASP | Refermee en |
    |----|--------|-------------|-----|-------|-------------|
    | VULN-01 | Injection SQL | /login | CWE-89 | A03 | M1 J3 |
    | ... | ... | ... | ... | ... | ... |
    
    ## 3. Secrets et configuration
    
    _(module 1, jour 2)_
    
    ## 4. Garde-fous locaux et pipeline
    
    _(module 1, jours 2 et 3)_
    
    ## 5. Exceptions acceptees
    
    _(module 1, jour 3)_
    
    ## 6. Limites connues
    
    _(a completer)_
    
  5. Complétez le tableau de la section 2 avec les dix lignes. Recopiez-les depuis la leçon précédente, mais vérifiez chaque emplacement dans votre app.py plutôt que de faire confiance au tableau : c'est votre fichier, et c'est vous qui répondrez des écarts.
  6. Initialisez le dépôt. Vérifiez le répertoire courant avant de taper la commande.
    pwd     # doit finir par /poei-s4/devsecops-lab
    git init -b main
  7. Regardez avant d'ajouter. C'est le geste que l'on saute, et c'est celui qui évite les mauvaises surprises. La sortie doit tenir en une dizaine de fichiers ; si elle en liste des centaines, vous n'êtes pas au bon niveau.
    git status --short
  8. Ajoutez et committez. Le message est à l'impératif, sur une ligne, et dit ce qui fonctionne — c'est la convention du parcours entier.
    git add .
    git commit -m "feat: application vulnerable devsecops-lab et son image de dev"
  9. Vérifiez ce qui est réellement suivi. Les trois commandes doivent ne rien renvoyer. Ce n'est pas la présence du .gitignore qui prouve quoi que ce soit : c'est cette sortie vide.
    git ls-files | grep -E '\.env$'      # doit etre vide
    git ls-files | grep -E '\.venv|lab\.db'   # doit etre vide
    git ls-files | grep -c ''            # le nombre de fichiers suivis : une dizaine
  10. Vérifiez enfin que .env.exemple, lui, est bien suivi. L'oublier est l'erreur symétrique, et elle rend le dépôt inutilisable pour un tiers.
    git ls-files | grep env
    # attendu : .env.exemple  — et lui seul
  11. Vous avez un dépôt, quatre documents et un commit. Vous n'avez pas encore de dépôt distant, pas de signature, pas de garde-fou : c'est la journée de demain. En l'état, votre historique contient déjà quelque chose que vous n'auriez pas dû committer — vous ne le savez pas encore, et c'est Gitleaks qui vous le dira demain matin.
Questions de réflexion — à préparer par écrit
  • Un fichier ajouté au .gitignore après avoir été commité reste suivi. Pourquoi Git se comporte-t-il ainsi, et que faut-il faire de plus ?
  • .env.exemple ne contient aucune valeur. À quoi sert-il, alors ?
  • Votre Dockerfile fait COPY . . et il n'y a pas de .dockerignore. Que contient donc votre image, en plus de votre code ?
Pour vous repérer

Tout ce TP tient dans un ordre : le .gitignore avant le premier git add. Un fichier ignoré après avoir été suivi reste suivi — Git ne revient pas en arrière tout seul. Si vous inversez les deux gestes, votre .venv et votre base lab.db entrent dans l'historique, et vous les y retrouverez le 21 août.

Deuxième point : vérifiez le niveau où vous lancez git init. S'il est lancé depuis ~/poei-s4 au lieu de ~/poei-s4/devsecops-lab, git status vous listera aussi les artefacts et les captures du parcours. Le signe qui ne trompe pas : des centaines de fichiers proposés au suivi.

✓ terminé
Point de contrôle

M1 · J1 — Cibles déployées et dépôt initialisé

Jour 1 · Poser le décor, le droit et le dépôt · 30 min

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

Votre encadrant vous demandera de montrer, pas de raconter : il vous fera lancer la charge d'injection devant lui. Préparez vos terminaux et votre conteneur.

Il vous demandera aussi les trois interdits, page fermée. C'est le seul critère du parcours qui ne se rattrape pas le lendemain.

Ouvrez le bouton Mon avancement, en haut à droite de cette page : il récapitule vos étapes, vos scores et vos points de contrôle. C'est ce que vous montrez en ouverture du point.

✓ terminé
Leçon

Comment un secret fuit — et pourquoi le supprimer ne suffit pas

Jour 2 · Git de confiance et garde-fous locaux · 30 min

Vous avez commité hier soir. Vous ne le savez pas encore, mais votre historique contient deux secrets. Ils y sont depuis dix-huit heures. Cette leçon explique par où ils sont entrés, ce que cela coûte, et pourquoi le réflexe naturel — supprimer la ligne — est le mauvais.

Par où passe une fuite

Le code source est la voie la plus connue, et loin d'être la seule. Un secret fuit par tous les endroits où l'on écrit sans y penser :

Voie de fuiteExemple typiqueCe qui la rend invisible
Le codeune clé d'API en constante, « le temps du développement »elle marche, donc plus personne ne la regarde
La configuration commitéeun .env ajouté par un git add . distraitun seul commit suffit, et il passe en revue
Les journaux de CIun echo de débogage, un set -x oubliéles journaux sont souvent lisibles plus largement que le code
Les couches d'imageun COPY . . qui embarque le .envl'image finale ne le montre pas, la couche intermédiaire le garde
L'historique de commandesun jeton passé en argument, capté par ~/.zsh_historypersonne ne pense à un fichier local
Les canaux d'équipeune clé collée dans un message pour dépanner un collèguela conversation est archivée et indexée pour des années

Retenez la quatrième ligne : elle vous concerne aujourd'hui. Votre Dockerfile fait COPY . . et vous n'avez pas de .dockerignore. Votre image devsecops-lab:dev contient donc votre .env, bien qu'il soit correctement exclu de Git. Deux mécanismes d'exclusion différents, deux fichiers différents — c'est le module 3 qui refermera celui-là.

Git est un journal, pas un classeur

Supprimer n'est pas retirer

Git ne modifie jamais un objet existant. Quand vous supprimez la ligne contenant la clé et que vous committez, vous ajoutez un commit qui contient une nouvelle version du fichier. L'ancienne version — le blob avec la clé — reste dans la base d'objets, référencée par le commit d'origine.

Autrement dit : git log -p la retrouve, git show <sha> l'affiche, et quiconque a cloné le dépôt en possède une copie complète sur son disque. La suppression a réglé le présent et rien d'autre.

C'est pour cela que la remédiation d'une fuite se fait dans un ordre précis, et que l'inverser rend le reste inutile.

Les quatre temps, dans l'ordre

  1. Révoquer. Dans la console du fournisseur, invalider la clé et en émettre une nouvelle. C'est le seul geste qui supprime réellement le risque : à partir de cette seconde, la clé fuitée ne vaut plus rien, où qu'elle se trouve.
  2. Mesurer l'exposition. Depuis quand ? Dépôt public ou privé ? Combien de clones, de forks, de miroirs ? La clé apparaît-elle dans des journaux de CI, dans une image publiée, dans un ticket ? Et surtout : a-t-elle servi à quelqu'un d'autre — que disent les journaux d'accès du fournisseur ?
  3. Réécrire l'historique. Purger le secret des commits passés. C'est du nettoyage, pas de la remédiation : cela réduit la surface pour l'avenir, cela n'annule rien du passé.
  4. Prévenir la récidive. Un hook local, un contrôle en CI, et une entrée dans le journal d'incidents qui dit ce qui s'est passé et ce qui a été mis en place.
Pourquoi cet ordre et pas un autre

Entre la fuite et la réécriture, il s'écoule des heures ou des jours. Pendant tout ce temps, la clé est lisible. Si vous commencez par réécrire, vous passez une heure à manipuler un historique pendant laquelle le secret reste valide — et vous finirez peut-être par croire l'incident clos sans avoir jamais révoqué.

La règle professionnelle : on considère qu'un secret exposé est un secret compromis, sans chercher à prouver qu'il a été utilisé. La preuve du contraire n'existe pas.

Comment un détecteur trouve un secret

Gitleaks, que vous lancerez dans quelques minutes, combine deux familles de règles. Elles ont des angles morts opposés, et c'est pour cela qu'on les emploie ensemble.

FamillePrincipeAttrape bienRate
Motifune expression régulière par type de secret : AKIA… pour AWS, ghp_… pour GitHub, -----BEGIN…KEY----- pour une clé privéeles secrets de fournisseurs connus, avec très peu de faux positifstout ce qui n'a pas de forme reconnaissable — un mot de passe de base, un jeton maison
Entropiemesure du désordre d'une chaîne : une suite aléatoire de 40 caractères a une entropie bien plus haute qu'un mot de la langueles secrets sans forme fixe, les jetons générésrien, mais elle produit des faux positifs — un hachage de commit, une empreinte, un identifiant encodé

Voici à quoi ressemble un finding Gitleaks. Retenez trois champs : RuleID dit quelle règle a mordu, Commit dit dans quel commit le secret vit, et Fingerprint est l'identifiant stable qui vous servira à déclarer un faux positif sans désactiver la règle entière.

[
  {
    "RuleID": "aws-access-token",
    "Description": "AWS Access Token",
    "File": "app.py",
    "StartLine": 16,
    "Secret": "AKIAIOSFODNN7EXAMPLE",
    "Commit": "8f2a1c4e9b7d3a5c2e8f0b6d4a1c9e7b2f5d8a3c",
    "Author": "Elie Zaraket",
    "Date": "2026-08-03T17:12:44Z",
    "Fingerprint": "8f2a1c4e:app.py:aws-access-token:16"
  }
]

Où un secret doit vivre

Type de secretOù il doit vivreCe qui prouve qu'il n'est pas dans le dépôt
Configuration locale de développementun .env ignoré par Git, décrit par un .env.exemple versionnégit ls-files | grep '\.env$' ne renvoie rien
Secret de pipelinele magasin de secrets de la forge, injecté en variable d'environnement au moment du jobaucune valeur en clair dans ci.yml, qui est versionné
Secret d'application en productionun gestionnaire de secrets, lu au démarrage ou monté par l'orchestrateurl'image ne contient aucune valeur — le module 3 le vérifiera avec Trivy
Clé privée (SSH, TLS)hors du dépôt, générée localement ; seule la clé publique se partagele hook detect-private-key refuse le commit

Vous appliquerez les deux premières lignes aujourd'hui même, et vous constaterez au passage que votre app.py viole allègrement la première.

Pour vous repérer

La phrase à retenir de cette leçon tient en six mots : supprimer n'est pas retirer. Git n'efface rien. Le commit qui « enlève » votre clé est un commit de plus, qui ajoute une version du fichier sans la clé — la version avec la clé reste dans l'historique, atteignable par n'importe qui a cloné le dépôt.

Conséquence directe sur votre réflexe : le premier geste devant un secret fuité n'est jamais git. C'est la console du fournisseur, pour révoquer.

✓ terminé
Quiz

Quiz — Secrets, historique Git et détection

Jour 2 · Git de confiance et garde-fous locaux · 10 min

1. Vous supprimez la ligne contenant la clé, vous committez, vous poussez. Que reste-t-il dans le dépôt ?

2. Dans quel ordre traiter une clé d'API poussée par erreur ?

3. Sur quelles familles de règles Gitleaks s'appuie-t-il ?

4. Vous installez pre-commit dans votre dépôt et vous poussez. Un collègue clone. Que se passe-t-il chez lui ?

5. Où doit vivre la clé d'API de production de votre application ?

✓ terminé
Travaux pratiques

TP 5 — Gitleaks trouve ce que vous avez commité hier

Jour 2 · Git de confiance et garde-fous locaux · 55 min

Détecter les secrets de votre propre historique, lire un rapport de findings, et déclarer un faux positif sans désactiver la règle.

Où l'on en est

Votre dépôt a un commit. Vous avez correctement exclu .env. Vous pouvez donc raisonnablement penser qu'aucun secret n'est versionné. Ce TP existe pour éprouver cette conviction, pas pour la confirmer.

  1. Placez-vous dans le dépôt et lancez Gitleaks sur l'historique. Remarquez l'absence de --no-git : c'est elle qui fait parcourir les commits plutôt que les seuls fichiers présents.
    cd ~/poei-s4/devsecops-lab
    
    docker run --rm -v "$(pwd)":/repo zricethezav/gitleaks:v8.18.4 \
      detect --source=/repo -v
  2. Relancez la même analyse en mode « fichiers », puis comparez les deux sorties. Ce sont deux questions différentes : « qu'y a-t-il dans mon historique » et « qu'y a-t-il dans mon arbre de travail ».
    docker run --rm -v "$(pwd)":/repo zricethezav/gitleaks:v8.18.4 \
      detect --source=/repo --no-git -v
  3. Regardez le code de sortie. C'est lui, et non le texte affiché, qui fera échouer votre pipeline demain. Retenez la convention : 1 quand des secrets sont trouvés, 0 quand il n'y en a aucun.
    docker run --rm -v "$(pwd)":/repo zricethezav/gitleaks:v8.18.4 \
      detect --source=/repo -v ; echo "code de sortie : $?"
  4. Lisez attentivement chaque finding et répondez par écrit à trois questions : combien de secrets sont remontés, dans quel commit chacun se trouve, et — la plus importante — est-ce que le compte est bon au regard de ce que vous avez écrit hier dans app.py ? Si un secret que vous savez présent n'apparaît pas, vous venez d'observer un faux négatif : notez-le, c'est un résultat, pas une panne.
  5. Produisez le rapport en JSON, dans le dossier des artefacts. Attention au chemin : --report-path doit désigner un emplacement à l'intérieur du volume monté, sinon le fichier meurt avec le conteneur.
    mkdir -p artefacts
    
    docker run --rm -v "$(pwd)":/repo zricethezav/gitleaks:v8.18.4 \
      detect --source=/repo \
      --report-format json \
      --report-path /repo/artefacts/gitleaks-2026-08-04.json
    
    ls -l artefacts/
  6. Ouvrez le rapport et isolez les champs qui comptent. Fingerprint est l'identifiant stable d'un finding : il ne change pas tant que le secret reste au même endroit, et c'est lui qu'on utilise pour déclarer un faux positif.
    python3 -m json.tool artefacts/gitleaks-2026-08-04.json | head -40
    
    # Ou, plus lisible, les seuls champs utiles :
    python3 -c "import json;d=json.load(open('artefacts/gitleaks-2026-08-04.json'));\
    [print(x['RuleID'],'|',x['File'],':',x['StartLine'],'|',x['Fingerprint']) for x in d]"
  7. Vérifiez que le rapport n'est pas versionné : artefacts/ figure dans votre .gitignore. Un rapport de secrets commité serait le comble — il contient les secrets en clair, dans le champ Secret.
  8. Créez .gitleaks.toml pour déclarer un faux positif. Cas d'école : la chaîne AKIAIOSFODNN7EXAMPLE est l'identifiant d'exemple publié par AWS dans sa propre documentation — il ne donne accès à rien. Un vrai faux positif, donc, et l'occasion d'apprendre le geste. Notez la structure : on hérite des règles par défaut, on n'en désactive aucune, on écarte une occurrence nommément.
    # .gitleaks.toml
    [extend]
    useDefault = true
    
    [[rules]]
      id = "aws-access-token"
      [rules.allowlist]
        description = """Identifiant d'exemple publie par AWS dans sa documentation.
        Ne donne acces a aucune ressource. Verifie le 2026-08-04 par E. Zaraket."""
        regexes = [ '''AKIAIOSFODNN7EXAMPLE''' ]
    
  9. Relancez l'analyse avec votre configuration et constatez la différence. Vérifiez surtout que les autres findings sont toujours là : une allowlist trop large est pire que pas d'allowlist du tout, parce qu'elle donne le sentiment d'un dépôt propre.
    docker run --rm -v "$(pwd)":/repo zricethezav/gitleaks:v8.18.4 \
      detect --source=/repo --config=/repo/.gitleaks.toml -v
    
    echo "code de sortie : $?"
  10. Consignez l'incident dans votre journal. C'est le temps n° 2 de la doctrine — mesurer l'exposition — et il s'écrit maintenant, pas ce soir.
    # A ajouter dans journal-incidents.md, section Entrees :
    
    ### 2026-08-04 — INC-01 : deux secrets en dur dans l'historique (VULN-05)
    
    **Constat.** gitleaks detect sur l'historique remonte des secrets introduits par le
    commit initial du 2026-08-03 : cle AWS d'exemple et SECRET_KEY Flask, dans app.py.
    
    **Exposition.** Depot local, aucun distant a ce stade, aucun clone. Cle AWS =
    identifiant d'exemple publie par AWS, sans acces. SECRET_KEY = valeur de
    developpement, jamais utilisee en production.
    
    **Rotation.** Sans objet ici (valeurs factices). En situation reelle : revocation
    immediate dans la console du fournisseur AVANT toute manipulation de l'historique.
    
    **Decision.** Correction et purge de l'historique planifiees ce jour (TP 8).
    Prevention par hook pre-commit (TP 7) puis par le pipeline (J3).
    
    **Faux positif ecarte.** AKIAIOSFODNN7EXAMPLE, via .gitleaks.toml, justification
    inscrite dans le fichier.
    
  11. Committez votre .gitleaks.toml et votre journal. Le secret, lui, est toujours dans app.py et toujours dans l'historique : on le traitera au TP 8, après avoir posé le garde-fou qui empêchera la récidive.
Questions de réflexion — à préparer par écrit
  • Un secret que vous savez présent n'a pas été détecté. Qu'est-ce que cela vous apprend sur la valeur d'un rapport vide ?
  • Le rapport JSON contient les secrets en clair. Quelles précautions cela impose-t-il sur sa conservation et sa transmission ?
  • Vous avez écarté un faux positif par une allowlist plutôt qu'en désactivant la règle. Qu'auriez-vous perdu en désactivant la règle ?
Pour vous repérer

Le piège de ce TP est une option d'une syllabe. gitleaks detect sans --no-git parcourt l'historique des commits ; avec --no-git, il traite le dossier comme un simple répertoire de fichiers et ignore complètement le passé. Les deux ont un usage, ils ne répondent pas à la même question — et vous croiserez les deux dans ce parcours, y compris demain dans votre pipeline.

Second point, propre à Docker : le rapport s'écrit dans le conteneur. Si le chemin de --report-path ne tombe pas dans le répertoire monté, votre fichier disparaît avec le conteneur et vous chercherez longtemps pourquoi il n'existe pas.

✓ terminé
Leçon

Commits signés et branche protégée : la confiance dans le dépôt

Jour 2 · Git de confiance et garde-fous locaux · 25 min

Ouvrez un dépôt quelconque et regardez la colonne des auteurs. Chaque commit affiche un nom et une adresse. La question à se poser, et que presque personne ne se pose : qui a vérifié cette information ?

Personne. C'est une déclaration.

user.name et user.email sont des variables de configuration que vous choisissez. Rien, dans Git, ne les vérifie. La commande suivante produit un commit parfaitement valide, attribué à quelqu'un d'autre :

git -c user.name="Linus Torvalds" -c user.email="torvalds@linux-foundation.org" \
    commit -m "un commit que je n'ai pas ecrit"

Ce n'est pas une faille de Git : c'est un choix de conception. Git est un système distribué où l'identité n'est pas centralisée. Le mécanisme qui rend l'auteur vérifiable est ajouté par-dessus, et il est facultatif : la signature cryptographique.

Ce que fait une signature

Signer un commit, c'est calculer une empreinte de son contenu et la chiffrer avec une clé privée que vous seul possédez. Quiconque dispose de la clé publique correspondante peut vérifier que le commit vient bien du détenteur de la clé privée, et qu'il n'a pas été modifié depuis.

GPGSSH
Maturité dans Githistorique, disponible depuis très longtempsplus récent — Git 2.34 et suivants
Ce qu'il faut gérerun trousseau, une expiration, éventuellement un serveur de clésun fichier de clé, comme pour se connecter en SSH
Vérification localepar le trousseau GPGpar un fichier allowed_signers que vous déclarez
Notre choix iciSSH : vous avez déjà l'outillage, rien à installer
Deux clés SSH, deux usages — ne les confondez pas

Vous allez manipuler deux clés dans le TP qui suit, et l'erreur classique est de n'en voir qu'une :

  • la clé d'authentification prouve à GitHub que vous avez le droit de pousser. Elle se dépose dans la section Authentication keys.
  • la clé de signature prouve que le commit vient de vous. Elle se dépose dans la section Signing keys.

Techniquement, une même clé peut jouer les deux rôles. Elle doit alors être déclarée deux fois chez GitHub, une fois par section. C'est la panne la plus silencieuse du TP suivant : tout fonctionne, la poussée passe, et la mention Verified n'apparaît jamais — parce que la clé n'a été déposée que comme clé d'authentification.

La protection de branche : le chemin obligé

La signature répond à « qui a écrit ce commit ? ». La protection de branche répond à une question différente : « par où ce code est-il passé avant d'arriver sur main ? ».

Sur une branche protégée, on interdit ce qui court-circuite la revue :

RègleCe qu'elle empêcheCe qu'elle n'empêche pas
Exiger une pull requestla poussée directe sur main, y compris la vôtre un vendredi soirqu'une pull request soit fusionnée sans être lue
Exiger des vérifications réussiesla fusion tant que le pipeline échoueque le pipeline soit mal écrit ou ne vérifie rien d'utile
Exiger des commits signésl'introduction d'un commit d'origine invérifiablequ'un compte légitime mais compromis signe du mauvais code
Interdire la poussée forcéela réécriture silencieuse de l'historique partagéune réécriture assumée, protection temporairement levée et tracée
Trois preuves distinctes, souvent confondues

La signature prouve l'origine : ce commit vient bien du détenteur de cette clé.

La protection de branche prouve le chemin : ce code est passé par une pull request et par les vérifications obligatoires.

Le pipeline prouve quelque chose sur le contenu : aucune règle de sécurité connue n'a mordu.

Aucune des trois ne remplace les deux autres, et aucune ne dit que le code est bon. Elles disent seulement qu'on sait d'où il vient, par où il est passé, et ce qui a été vérifié. C'est déjà considérable — et c'est exactement ce qu'on attend d'une chaîne d'approvisionnement logicielle.

Une remarque d'ordre pratique pour demain

Vous allez activer aujourd'hui deux règles : pull request obligatoire et commits signés. La troisième — exiger des vérifications réussies — ne pourra pas être activée avant demain, et pour une raison très concrète : GitHub ne propose de sélectionner une vérification qu'après l'avoir vue s'exécuter au moins une fois. Tant que votre pipeline n'a pas tourné, les cases secrets et sast n'existent pas dans l'interface. Ce n'est pas un bogue, et ce n'est pas la peine de la chercher.

✓ terminé
Travaux pratiques

TP 6 — Compte GitHub, clé de signature SSH, branche main protégée

Jour 2 · Git de confiance et garde-fous locaux · 55 min

Rendre l'origine de vos commits vérifiable et imposer un chemin obligé vers main.

Compte personnel, dépôt privé

Créez le compte avec une adresse personnelle, pas une adresse @caplogy : ce parcours ne doit dépendre d'aucune ressource d'entreprise. Et créez le dépôt privé — il contient une application délibérément vulnérable et, pour quelques heures encore, des secrets dans son historique.

  1. Créez un compte sur github.com avec une adresse personnelle, si vous n'en avez pas déjà un. Activez l'authentification à deux facteurs — elle est de toute façon exigée pour contribuer, et c'est le minimum pour un compte qui va porter une clé de signature.
  2. Créez un dépôt privé nommé devsecops-lab, sans README, sans .gitignore et sans licence : votre dépôt local existe déjà, et ces fichiers créeraient un historique divergent qu'il faudrait réconcilier pour rien.
  3. Générez la clé de signature. Elle est distincte de votre clé d'authentification habituelle — c'est un usage différent, et les mélanger rend le diagnostic pénible.
    ssh-keygen -t ed25519 -C "elie-devsecops" -f ~/.ssh/id_signing
    
    # Affichez la cle PUBLIQUE — c'est elle, et elle seule, qui se copie :
    cat ~/.ssh/id_signing.pub
  4. Sur GitHub : Settings → SSH and GPG keys → New SSH key. Collez le contenu de id_signing.pub et choisissez impérativement le type « Signing Key » dans le menu déroulant. C'est le seul geste du TP qui échoue sans rien dire : une clé déposée en « Authentication Key » ne fera jamais apparaître la mention Verified.
  5. Configurez Git pour signer. Les trois premières lignes activent la signature ; la quatrième et la cinquième permettent à votre poste de vérifier les signatures, ce qu'il ne sait pas faire sans un fichier de correspondance.
    git config --global gpg.format ssh
    git config --global user.signingkey ~/.ssh/id_signing.pub
    git config --global commit.gpgsign true
    
    # Pour verifier les signatures localement — sinon 'No signature' a l'affichage.
    # Remplacez l'adresse par celle de votre compte GitHub.
    echo "$(git config user.email) namespaces=\"git\" $(cat ~/.ssh/id_signing.pub)" \
      >> ~/.ssh/allowed_signers
    git config --global gpg.ssh.allowedSignersFile ~/.ssh/allowed_signers
  6. Vérifiez que l'adresse de vos commits est bien celle de votre compte GitHub. Une adresse différente produit des commits orphelins, non rattachés à votre profil.
    git config --global user.name "Elie Zaraket"
    git config --global user.email "<votre-adresse-github>"
    
    git config --global --list | grep -E 'user\.|gpg'
  7. Signez votre commit existant en le reprenant, puis vérifiez la signature localement. Vous devez lire Good "git" signature.
    cd ~/poei-s4/devsecops-lab
    git commit --amend --no-edit -S
    
    git log --show-signature -1
  8. Rattachez le dépôt distant et poussez. Employez l'URL SSH : en HTTPS, Git vous réclamerait un jeton d'accès personnel à chaque poussée.
    git remote add origin git@github.com:<votre-compte>/devsecops-lab.git
    git remote -v
    
    git push -u origin main
  9. Sur GitHub, ouvrez la liste des commits. Votre commit doit porter le badge Verified. S'il porte Unverified ou aucun badge, revenez à l'étape 4 : la clé a presque certainement été déposée dans la mauvaise section.
  10. Activez la protection de branche : Settings → Branches → Add branch protection rule, motif main. Cochez Require a pull request before merging, Require signed commits, et surtout Do not allow bypassing the above settings — sans cette dernière case, la règle ne s'applique pas à vous, propriétaire du dépôt, et la vérification qui suit ne prouverait rien. Laissez Require status checks de côté : les vérifications n'existeront qu'après le premier passage du pipeline, demain.
  11. Éprouvez la protection. Une poussée directe sur main doit être refusée par le serveur. C'est la preuve que la règle vous inclut.
    echo "" >> README.md
    git add README.md
    git commit -m "test: verifier que la protection de branche s'applique"
    git push origin main
    
    # attendu : remote: error: GH006: Protected branch update failed
    # Annulez ce commit de test :
    git reset --hard HEAD~1
Questions de réflexion — à préparer par écrit
  • La signature prouve l'origine du commit. Qu'est-ce qu'elle ne prouve pas, et qui resterait entièrement à vérifier ?
  • Vous avez coché « ne pas autoriser le contournement ». Qu'est-ce que cela change pour vous, propriétaire du dépôt, et pourquoi est-ce le bon réglage ?
  • Un attaquant obtient votre clé de signature. Que peut-il faire, et que ne peut-il toujours pas faire ?
Pour vous repérer

La panne silencieuse de ce TP : la clé déposée dans la mauvaise section de GitHub. Une clé de signature va dans Signing keys, pas dans Authentication keys. Si vous vous trompez, tout marche — la poussée réussit, le commit part — mais la mention Verified n'apparaît jamais et rien ne vous dit pourquoi.

Deuxième subtilité : git log --show-signature ne sait pas vérifier une signature SSH sans le fichier allowed_signers. Sans lui, vous verrez No signature ou une erreur, alors même que votre commit est correctement signé et que GitHub l'affiche comme vérifié. L'étape 5 règle ce point.

Enfin, si git push vous demande un mot de passe, vous poussez en HTTPS et non en SSH : GitHub n'accepte plus les mots de passe de compte depuis 2021. Vérifiez votre URL distante.

✓ terminé
Travaux pratiques

TP 7 — pre-commit : le garde-fou qui refuse le commit

Jour 2 · Git de confiance et garde-fous locaux · 1 h

Installer des contrôles qui s'exécutent avant chaque commit, et apprendre à traiter honnêtement ce qu'ils refusent.

  1. Installez pre-commit. La version est épinglée comme le reste de l'outillage.
    cd ~/poei-s4/devsecops-lab
    source .venv/bin/activate
    pip install pre-commit==3.7.1
    pre-commit --version
  2. Créez .pre-commit-config.yaml. Cinq contrôles : les secrets, le SAST Python, les clés privées, les fichiers trop gros, et deux vérifications d'hygiène.
    # .pre-commit-config.yaml — les garde-fous qui tournent AVANT le commit.
    repos:
      - repo: https://github.com/gitleaks/gitleaks
        rev: v8.18.4
        hooks:
          - id: gitleaks
    
      - repo: https://github.com/PyCQA/bandit
        rev: 1.7.9
        hooks:
          - id: bandit
            args: ["-c", ".bandit.yaml"]
    
      - repo: https://github.com/pre-commit/pre-commit-hooks
        rev: v4.6.0
        hooks:
          - id: detect-private-key
          - id: check-added-large-files
            args: ["--maxkb=512"]
          - id: check-yaml
          - id: end-of-file-fixer
    
  3. Créez la configuration de Bandit. Elle est volontairement vide de toute exclusion pour l'instant : on veut voir ce qu'il trouve avant de décider quoi que ce soit.
    # .bandit.yaml
    exclude_dirs:
      - ./.venv
      - ./artefacts
    
  4. Installez le hook dans votre dépôt. Cette commande écrit dans .git/hooks/pre-commit — un répertoire qui n'est pas versionné et qu'aucun clone ne recevra.
    pre-commit install
    ls -l .git/hooks/pre-commit
  5. Lancez tous les contrôles sur tout le dépôt. Le premier passage installe les environnements de chaque hook : comptez deux à quatre minutes, sans affichage. Attendez-vous à un échec bruyant — c'est le résultat attendu.
    pre-commit run --all-files
  6. Lisez la sortie et faites l'inventaire par écrit : quel hook a échoué, sur quel fichier, pour quelle règle. Vous devriez retrouver, sous des identifiants Bandit, plusieurs des vulnérabilités que vous avez introduites hier. Notez lesquelles ne sont pas remontées : c'est aussi instructif.
  7. Vous voilà devant le vrai sujet de ce TP : votre propre garde-fou refuse votre code. Il n'existe que deux réponses honnêtes. Corriger — mais VULN-02, 03, 06 et 07 sont réservées au module 2, les corriger maintenant priverait le module 2 de sa matière. Ou consigner une exception datée. C'est ce que nous faisons, et voici à quoi cela ressemble.
    # .bandit.yaml — version avec exceptions TRACEES
    exclude_dirs:
      - ./.venv
      - ./artefacts
    
    # EXCEPTIONS TEMPORAIRES — echeance : module 2 (2026-08-06 au 2026-08-10)
    # Chaque test ignore correspond a une vulnerabilite DELIBEREE du support de
    # formation, cartographiee dans README.md section 2 et suivie dans
    # journal-incidents.md. Aucune ne doit survivre au module 2.
    #   B602 -> VULN-03 injection de commande (subprocess shell=True)
    #   B324 -> VULN-06 hachage faible (md5)
    #   B506 -> VULN-07 deserialisation non sure (yaml.load)
    #   B201 -> VULN-09 debug=True (echeance module 3)
    skips:
      - B602
      - B324
      - B506
      - B201
    
  8. Ajoutez le tableau des exceptions à la section 5 de votre README.md. Une exception sans échéance n'est pas une exception, c'est un abandon — c'est la règle du parcours, et le jury la vérifiera.
    ## 5. Exceptions acceptees
    
    | Regle | VULN | Motif | Decidee le | Echeance | Levee le |
    |-------|------|-------|------------|----------|----------|
    | bandit B602 | VULN-03 | faille pedagogique deliberee | 2026-08-04 | M2 | |
    | bandit B324 | VULN-06 | faille pedagogique deliberee | 2026-08-04 | M2 | |
    | bandit B506 | VULN-07 | faille pedagogique deliberee | 2026-08-04 | M2 | |
    | bandit B201 | VULN-09 | faille pedagogique deliberee | 2026-08-04 | M3 | |
    | gitleaks aws-access-token | — | identifiant d'exemple AWS public | 2026-08-04 | aucune | |
    
  9. Relancez. Il doit rester des échecs — notamment sur les secrets d'app.py, que nous n'avons pas écartés et que nous allons vraiment corriger au TP suivant.
    pre-commit run --all-files
  10. Éprouvez le garde-fou pour de bon : tentez de committer votre .env, celui qui contient un faux secret. Le -f force l'ajout malgré le .gitignore — et c'est exactement le geste distrait que l'on veut voir échouer.
    git add -f .env
    git commit -m "test: le hook doit refuser ce commit"
    
    # attendu : le hook gitleaks echoue, le commit N'EST PAS cree.
    # Verifiez-le :
    git log --oneline -1
    
    # Puis retirez le fichier de l'index :
    git restore --staged .env
  11. Dernière démonstration, et c'est celle qui prépare demain. Le même commit, avec une option de plus.
    git add -f .env
    git commit --no-verify -m "test: contournement du hook"
    
    # Le commit passe. Le hook n'a rien vu, rien dit, rien empeche.
    git log --oneline -1
    
    # Annulez immediatement — ce commit contient un secret :
    git reset --hard HEAD~1
    git status --short
  12. Voilà pourquoi la journée de demain existe. Un hook local aide celui qui veut bien être aidé ; il ne protège pas un dépôt. Le contrôle qui arrête vit sur un serveur que vous ne pouvez pas contourner depuis votre poste. Committez votre configuration — signée, et sans --no-verify cette fois.
Questions de réflexion — à préparer par écrit
  • Le hook vit dans .git/hooks/, qui n'est pas versionné. Quelle conséquence cela a-t-il sur une équipe de dix personnes ?
  • Vous avez tracé quatre exceptions plutôt que de retirer quatre règles. Qu'est-ce que cela change, six mois plus tard, pour quelqu'un qui reprend le dépôt ?
  • --no-verify a un usage légitime. Lequel, et comment le distinguer d'un contournement ?
Pour vous repérer

Deux vérités à tenir ensemble sur ce TP, et elles ne se contredisent pas.

La première : le hook est le contrôle le plus rentable du parcours, parce qu'il vous répond en trois secondes pendant que vous avez encore le contexte en tête.

La seconde : il ne garantit rien. Il vit dans .git/hooks/, qui n'est jamais transmis par un clone, et git commit --no-verify le contourne d'un geste. C'est un confort, pas un verrou. Le verrou, c'est demain.

Attendez-vous à ce que le premier pre-commit run --all-files échoue bruyamment. C'est le résultat attendu, pas un problème d'installation.

✓ terminé
Leçon

Le secret est déjà dans l'historique : la procédure

Jour 2 · Git de confiance et garde-fous locaux · 25 min

Vous savez maintenant que votre historique contient des secrets, et vous avez posé le garde-fou qui empêchera d'en ajouter. Reste le passé. Cette leçon décrit l'opération, ses effets de bord, et la seule manière de vérifier qu'elle a fonctionné.

Rappel de l'ordre — parce que c'est là qu'on se trompe

La réécriture est du nettoyage, pas de la remédiation

Entre le moment où le secret a été poussé et celui où vous réécrivez, il a été lisible. Qui l'a lu ? Vous ne le saurez jamais. Un dépôt peut avoir été cloné, indexé par un robot, sauvegardé par un système de sauvegarde, dupliqué dans un fork.

Un secret exposé est un secret compromis. La réécriture réduit la surface pour l'avenir ; elle ne restaure rien. Si vous n'avez pas révoqué, vous n'avez rien fait.

Les outils, et lequel choisir

OutilCe qu'il vautVerdict
git filter-reporapide, sûr, maintenu ; c'est l'outil recommandé par le projet Git lui-même ; s'installe par pipx ou par le gestionnaire de paquetsnotre choix
BFG Repo-Cleanertrès rapide sur les gros dépôts, syntaxe simple ; demande une machine virtuelle Javaacceptable, second choix
git filter-branchintégré à Git, mais lent, difficile à écrire correctement, et Git affiche lui-même un avertissement invitant à ne plus l'employerà éviter
Supprimer et recréer le dépôtradical et efficace sur un dépôt de quelques jours ; perd tout l'historique et toutes les pull requestscas particulier

Ce que la réécriture casse — à savoir avant, pas après

  • Toutes les empreintes changent en aval du premier commit modifié. Un commit est identifié par un hachage de son contenu et de son parent : modifiez le premier, et toute la chaîne se renumérote.
  • Les clones existants deviennent incompatibles. Chaque personne ayant cloné doit recloner. Un simple git pull produirait une fusion monstrueuse qui réintroduirait les anciens commits — donc le secret.
  • Les pull requests ouvertes se cassent, les branches non traitées pointent encore sur l'ancien historique.
  • Les anciens objets survivent sur le serveur. GitHub conserve les commits devenus orphelins et les rend accessibles par leur empreinte, parfois longtemps ; et un fork en garde une copie que vous ne contrôlez pas. Sur un incident réel, on demande explicitement au support de purger.
  • La poussée forcée est refusée par une branche protégée — ce qui est le comportement voulu. Il faut lever la protection, forcer, la remettre, et écrire dans le journal qui a fait quoi et quand. C'est une procédure d'exception, elle se trace.

La séquence complète

  1. Révoquer le secret chez son fournisseur. Toujours en premier.
  2. Sauvegarder le dépôt avant de le réécrire : git clone --mirror dans un dossier à part. Une réécriture ratée sans sauvegarde, c'est un dépôt perdu.
  3. Corriger le code pour que le secret ne soit plus nécessaire : lecture depuis l'environnement, valeur documentée dans .env.exemple.
  4. Réécrire avec git filter-repo --replace-text, qui remplace chaque occurrence par un marqueur dans tous les commits.
  5. Vérifier, et c'est l'étape que l'on saute : git log --all -p -S'<fragment>' doit ne rien renvoyer, et un Gitleaks sur l'historique complet doit sortir en 0.
  6. Republier en poussée forcée, protection temporairement levée, puis remise.
  7. Prévenir : avertir ceux qui ont cloné, et consigner l'incident.
Un piège de git filter-repo qui surprend tout le monde

Par sécurité, git filter-repo supprime le dépôt distant origin de votre configuration après avoir réécrit l'historique. C'est délibéré : l'outil veut vous empêcher de pousser par réflexe un historique réécrit sans y avoir réfléchi.

Résultat : votre git push répondra 'origin' does not appear to be a git repository. Ce n'est pas une panne. Il faut refaire git remote add origin …, en connaissance de cause.

Pour vous repérer

Avant de lire cette leçon, faites une sauvegarde mentale de la règle : on révoque d'abord. Tout ce qui suit décrit le nettoyage, et le nettoyage vient en troisième position. Si vous ne devez retenir qu'une chose de la journée, c'est l'ordre — pas la commande.

Et une précision qui surprend toujours : réécrire l'historique ne fait pas disparaître les anciens objets du serveur distant. Ils restent atteignables par leur empreinte, parfois pendant des semaines, et dans les forks pour toujours.

✓ terminé
Travaux pratiques

TP 8 — Rotation, réécriture de l'historique, vérification

Jour 2 · Git de confiance et garde-fous locaux · 55 min

Fermer VULN-05 pour de bon : dans le code, dans l'historique, et dans le journal.

Opération destructive

Ce TP réécrit l'historique de votre dépôt. Fait correctement, il ne pose aucun problème. Fait sans sauvegarde et interrompu au mauvais moment, il vous coûte deux jours de travail. L'étape 2 n'est pas facultative.

  1. Temps n° 1 — révoquer. Nos deux secrets sont factices : l'identifiant AWS est celui de la documentation publique, la clé Flask n'a jamais servi. Il n'y a donc rien à révoquer, et c'est justement pourquoi il faut écrire ce que l'on aurait fait. Consignez-le dans le journal avant de toucher à quoi que ce soit.
    # A ajouter a l'entree INC-01 de journal-incidents.md :
    
    **Temps 1 — Rotation (2026-08-04).** Secrets factices, sans acces reel : aucune
    revocation possible ni necessaire. En situation reelle, la sequence aurait ete :
      1. console AWS -> IAM -> desactiver puis supprimer la cle d'acces ;
      2. emettre une nouvelle cle, la deposer dans le gestionnaire de secrets ;
      3. relire les journaux d'acces CloudTrail sur la periode d'exposition ;
      4. SEULEMENT ENSUITE, reecrire l'historique.
    
  2. Sauvegardez. Un miroir complet, à côté du dépôt. Si la réécriture tourne mal, c'est votre seule issue.
    cd ~/poei-s4/devsecops-lab
    git clone --mirror . ../devsecops-lab-avant-reecriture.git
    
    ls -d ../devsecops-lab-avant-reecriture.git   # doit exister
  3. Temps n° 3 — corriger le code. Sur une branche dédiée, faites lire les deux valeurs depuis l'environnement. Remarquez que SECRET_KEY n'a plus de valeur par défaut utilisable : une application qui démarre sans secret configuré doit s'arrêter, pas se rabattre sur une valeur connue.
    git switch -c fix/vuln-05-secret
    
    # Dans app.py, remplacez les deux lignes de VULN-05 par :
    
    # VULN-05 corrigee : les secrets viennent de l'environnement, jamais du code.
    app.config["SECRET_KEY"] = os.environ["APP_SECRET_KEY"]
    API_KEY = os.environ.get("APP_API_KEY", "")
    
  4. Vérifiez que l'application démarre avec l'environnement, et qu'elle refuse de démarrer sans. Le second essai est le plus important : il prouve qu'il ne reste aucune valeur de repli dans le code.
    source .venv/bin/activate
    
    # Avec l'environnement : demarre normalement.
    set -a && source .env && set +a
    python app.py &
    sleep 2 && curl -s http://localhost:5000/ && echo
    kill %1
    
    # Sans l'environnement : doit echouer sur KeyError APP_SECRET_KEY.
    env -u APP_SECRET_KEY python app.py
  5. Committez la correction. Le message porte l'identifiant de la vulnérabilité — c'est la convention qui permettra au jury de relier une ligne de journal à un commit.
    git add app.py
    git commit -m "fix(VULN-05): lire les secrets depuis l'environnement"
    
    git switch main
    git merge --no-ff fix/vuln-05-secret -m "merge: fix(VULN-05)"
  6. Temps n° 4 — réécrire. Installez l'outil, puis écrivez le fichier de remplacements : une ligne par secret à purger de tous les commits.
    pipx install git-filter-repo   # ou : brew install git-filter-repo
    git filter-repo --version
    
    # Creez ../replacements.txt — HORS du depot, pour ne pas le committer :
    cat > ../replacements.txt <<'FIN'
    AKIAIOSFODNN7EXAMPLE
    s3cr3t-dev-key-ne-pas-committer-42
    FIN
  7. Lancez la réécriture. Chaque occurrence est remplacée par ***REMOVED*** dans tous les commits de toutes les branches.
    git filter-repo --replace-text ../replacements.txt --force
    
    # Regardez le graphe : les empreintes ont toutes change.
    git log --oneline --all
  8. Temps n° 5 — vérifier. Deux commandes, et ni l'une ni l'autre ne regarde le fichier courant. La première fouille tout l'historique à la recherche du fragment ; la seconde relance le détecteur sur l'ensemble des commits.
    # 1. Aucune occurrence dans aucun commit — la sortie doit etre VIDE.
    git log --all -p -S 'AKIAIOSFODNN7EXAMPLE' | head
    git log --all -p -S 's3cr3t-dev-key' | head
    
    # 2. Gitleaks sur l'historique complet — code de sortie attendu : 0.
    docker run --rm -v "$(pwd)":/repo zricethezav/gitleaks:v8.18.4 \
      detect --source=/repo --config=/repo/.gitleaks.toml -v
    echo "code de sortie : $?"
  9. Remettez le dépôt distant : filter-repo l'a retiré exprès. Ce n'est pas une panne, c'est un garde-fou.
    git remote -v          # vide : c'est normal
    git remote add origin git@github.com:<votre-compte>/devsecops-lab.git
    git remote -v
  10. Temps n° 6 — republier. Votre poussée forcée sera refusée par la protection posée ce matin, et c'est la preuve qu'elle fonctionne. Procédure d'exception : sur GitHub, Settings → Branches, désactivez temporairement la règle, poussez, puis réactivez-la immédiatement. --force-with-lease plutôt que --force : il refuse si le distant a bougé depuis votre dernière synchronisation.
    git push --force-with-lease origin main
    
    # Si refuse : levez la protection sur GitHub, relancez, puis REMETTEZ-LA.
    # Verifiez ensuite dans l'interface que la regle est bien active.
  11. Temps n° 7 — consigner. Complétez INC-01 : la réécriture, sa vérification, et la levée temporaire de protection. Une procédure d'exception qui n'est pas tracée est indiscernable d'un contournement.
    **Temps 4-6 — Reecriture (2026-08-04, 15h40-16h05).**
    Sauvegarde miroir : ../devsecops-lab-avant-reecriture.git
    Outil : git filter-repo --replace-text (2 motifs).
    Verification : `git log --all -p -S` vide sur les deux motifs ; gitleaks sur
    l'historique complet en code 0.
    Protection de main levee de 16h02 a 16h05 par E. Zaraket pour la poussee forcee,
    puis retablie et verifiee dans l'interface.
    Aucun clone tiers a prevenir (depot prive, aucun collaborateur).
    
    **Statut VULN-05 : FERMEE** — code corrige, historique purge, prevention en place
    (hook pre-commit ; le pipeline prendra le relais au J3).
    
  12. Committez le journal, signé. VULN-05 est fermée : dans le code, dans l'historique, et dans la trace. C'est la première des dix, et la seule qui aura demandé une opération sur le passé.
Questions de réflexion — à préparer par écrit
  • Vous avez vérifié par deux commandes différentes. Qu'est-ce que la seconde apporte que la première ne donnait pas ?
  • La protection de branche a refusé votre poussée légitime. Était-ce un obstacle ou le comportement attendu — et qu'est-ce qui, dans votre procédure, fait la différence ?
  • Si ce dépôt avait été public pendant deux jours, qu'auriez-vous dû faire de plus, et qu'est-ce qui aurait été définitivement hors de votre contrôle ?
Pour vous repérer

Ne sautez pas l'étape de sauvegarde. Une réécriture ratée sans miroir, c'est un dépôt perdu — et le vôtre porte deux jours de travail.

Deux surprises vous attendent, toutes deux normales : git filter-repo supprime votre origin après coup, et votre poussée forcée sera refusée par la protection de branche que vous avez posée ce matin. Les deux sont des sécurités qui fonctionnent, pas des pannes. Les étapes le prévoient.

✓ terminé
Travaux pratiques

TP 9 — Rédiger INC-01 et la section « Secrets » du README

Jour 2 · Git de confiance et garde-fous locaux · 45 min

Produire les livrables écrits que le point de contrôle du soir va vérifier.

  1. Relisez votre journal-incidents.md en vous posant une seule question : quelqu'un qui n'était pas là comprend-il ce qui s'est passé et pourrait-il le rejouer ? Complétez ce qui manque. Un incident se relit un an plus tard, par une autre personne, dans un contexte oublié.
  2. Vérifiez que INC-01 contient les six temps, chacun avec sa trace. Voici la structure attendue — comparez-la à ce que vous avez écrit.
    ### 2026-08-04 — INC-01 : deux secrets en dur dans l'historique (VULN-05)
    
    **Constat**       quoi, trouve comment, quand, par quelle commande
    **Exposition**    depuis quand, depot public ou prive, clones connus, usage constate
    **Temps 1 — Rotation**     ce qui a ete revoque, ou ce qui l'aurait ete
    **Temps 4 — Reecriture**   outil, motifs, sauvegarde, horaires
    **Temps 5 — Verification** les DEUX commandes et leurs resultats
    **Temps 7 — Prevention**   hook en place, pipeline a venir, exceptions tracees
    **Statut**        FERMEE / OUVERTE / ACCEPTEE avec echeance
    
  3. Rédigez la section 3 du README, « Secrets et configuration ». Elle doit permettre à quelqu'un de faire tourner votre application sans jamais vous demander une valeur.
    ## 3. Secrets et configuration
    
    Aucun secret ne figure dans le code ni dans l'historique Git.
    
    ### Variables attendues
    
    | Variable | Role | Obligatoire | Ou la trouver |
    |----------|------|-------------|---------------|
    | APP_SECRET_KEY | signature des sessions Flask | oui, l'appli refuse de demarrer sans | generee localement |
    | APP_API_KEY | identifiant fournisseur (factice ici) | non | .env local |
    
    ### Mise en route
    
        cp .env.exemple .env      # puis renseignez les valeurs
        set -a && source .env && set +a
        python app.py
    
    ### Verifier qu'aucun secret n'est versionne
    
        git ls-files | grep -E '\.env$'        # doit etre vide
        docker run --rm -v "$(pwd)":/repo zricethezav/gitleaks:v8.18.4 \
          detect --source=/repo --config=/repo/.gitleaks.toml
        # code de sortie attendu : 0
    
  4. Rédigez la section 4, « Garde-fous locaux ». Décrivez ce que fait chaque hook, et surtout ce qu'il ne fait pas — c'est la partie que personne n'écrit et la seule qui évite les mauvaises surprises.
    ## 4. Garde-fous locaux et pipeline
    
    ### Hooks pre-commit (module 1, jour 2)
    
    | Hook | Ce qu'il verifie | Ce qu'il ne verifie pas |
    |------|------------------|-------------------------|
    | gitleaks | secrets dans les fichiers mis en scene | l'historique deja pousse |
    | bandit | motifs Python dangereux | la logique metier, l'appli en marche |
    | detect-private-key | cles privees | les cles publiques mal placees |
    | check-added-large-files | fichiers > 512 Ko | ce qui est deja versionne |
    
    **Limite structurelle.** Ces hooks vivent dans .git/hooks/, qui n'est pas versionne :
    un clone ne les recoit pas, et `git commit --no-verify` les contourne. Ils aident,
    ils ne garantissent rien. Le verrou reel est le pipeline (module 1, jour 3).
    
    ### Pipeline
    
    _(module 1, jour 3)_
    
  5. Vérifiez que votre section 5, le tableau des exceptions, porte une échéance datée sur chaque ligne. Relisez-la avec la question du jury : « qui a décidé ça, quand, et jusqu'à quand ? ». Si une ligne ne répond pas aux trois, complétez-la.
  6. Répondez par écrit aux questions de réflexion des TP 5 à 8 dans reponses-reflexion.md. C'est le document dont votre encadrant tirera ses questions au point du soir — pas de vos scripts. Deux réponses développées valent mieux que huit expédiées.
  7. Committez l'ensemble, signé, et poussez par une pull request : votre branche main est protégée, la poussée directe est refusée. C'est le premier aller-retour complet de votre chaîne, et vous le referez demain avec un pipeline au bout.
    git switch -c docs/journal-et-readme-j2
    git add journal-incidents.md README.md reponses-reflexion.md
    git commit -m "docs: INC-01, sections secrets et garde-fous du README"
    git push -u origin docs/journal-et-readme-j2
    
    # Puis ouvrez la pull request sur GitHub et fusionnez-la.
    git switch main && git pull
Questions de réflexion — à préparer par écrit
  • Votre journal décrit une opération que vous venez de faire. Qu'est-ce qui, dans votre rédaction, permettrait à un tiers de la rejouer à l'identique ?
  • Vous avez écrit ce que les hooks ne vérifient pas. En quoi cette section est-elle plus utile que la liste de ce qu'ils vérifient ?
✓ terminé
Point de contrôle

M1 · J2 — Commit signé et secret bloqué au pre-commit

Jour 2 · Git de confiance et garde-fous locaux · 30 min

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

Votre encadrant vous fera faire trois gestes : tenter le commit interdit, fouiller votre historique, et ouvrir la page de protection de branche sur GitHub. Ce dernier point est celui qu'on oublie après une poussée forcée — vérifiez-le avant le point.

Il vous demandera aussi l'ordre des quatre temps, page fermée. Si vous commencez par « réécrire », la journée est à reprendre.

✓ terminé
Leçon

Le pipeline qui refuse la faille

Jour 3 · Le pipeline qui refuse la faille · 30 min

Hier, vous avez installé un garde-fou local, puis vous l'avez contourné vous-même en ajoutant sept caractères à votre commande. C'était l'objet de la démonstration : un hook aide celui qui veut bien être aidé. Aujourd'hui, on place le contrôle là où votre poste ne peut plus l'atteindre.

pull request fix/vuln-01-sqli job secrets gitleaks v8.18.4 VULN-05 job sast semgrep 1.85.0 VULN-01 le verrou code de sortie merge sur main refusé 0 ≠ 0
Deux jobs, un verrou. Ce n'est pas le rapport qui bloque la fusion : c'est le code de sortie du job.

La chaîne de décision, en trois maillons

Un pipeline de sécurité n'empêche rien par lui-même. Ce qui empêche, c'est une chaîne de trois maillons — et il suffit qu'un seul manque pour que tout le dispositif devienne décoratif.

  1. L'outil sort en erreur. Semgrep avec --error, Gitleaks qui trouve un secret, Trivy avec --exit-code 1. Sans cela, le job réussit et le rapport part à la corbeille.
  2. Le job échoue donc, et il est déclaré obligatoire. Un job qui échoue mais n'est pas dans la liste des vérifications requises ne bloque rien : la pull request affiche une croix rouge que l'on peut fusionner par-dessus.
  3. La branche est protégée, et la protection s'applique à tout le monde, y compris au propriétaire du dépôt. Sans la case « ne pas autoriser le contournement », le verrou s'ouvre de l'intérieur.
Le test qui dit si votre pipeline sert à quelque chose

Introduisez volontairement une faille et ouvrez une pull request. Si vous parvenez à la fusionner, votre pipeline est un système d'information, pas un dispositif de sécurité.

C'est exactement ce que vous allez faire dans une heure, et dans cet ordre : d'abord prouver que ça bloque, ensuite corriger. Un pipeline que l'on n'a jamais vu échouer n'a jamais été éprouvé.

Le workflow de départ

Voici celui que vous allez poser. Deux jobs, indépendants, exécutés en parallèle : l'un cherche des secrets, l'autre analyse le code. Il grandira aux modules suivants — le module 2 y ajoutera l'analyse de composition et le DAST, le module 3 le scan d'image.

name: ci-securite
on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]
permissions:
  contents: read
  security-events: write
jobs:
  secrets:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: gitleaks
        run: |
          docker run --rm -v "$PWD:/repo" zricethezav/gitleaks:v8.18.4 \
            detect --source=/repo --no-git -v --report-path=/repo/gitleaks.json
  sast:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: semgrep
        run: |
          docker run --rm -v "$PWD:/src" semgrep/semgrep:1.85.0 \
            semgrep scan --config p/python --config p/flask --config p/security-audit \
            --error --sarif --output /src/semgrep.sarif /src

Ce qu'il faut savoir lire dans ces vingt-cinq lignes

LigneCe qu'elle faitPourquoi elle est là
on: pull_requestdéclenche le pipeline sur chaque pull request visant mainc'est le moment où le contrôle est utile : avant la fusion, pas après
permissions: contents: readrestreint le jeton fourni au jobpar défaut le jeton peut écrire ; un script compromis dans une dépendance en profiterait
fetch-depth: 0récupère tout l'historique et non le dernier commitsans lui, aucune analyse du passé n'est possible — voir l'encadré ci-dessous
docker run … zricethezav/gitleaks:v8.18.4exécute l'outil en conteneur épingléla commande est rigoureusement celle de votre poste : ce qui échoue ici se rejoue là-bas
--errorfait sortir Semgrep en code non nul dès qu'une règle correspondc'est le verrou — sans ce mot, tout le reste est de la documentation
--sarif --outputproduit un rapport au format standardun format commun à tous les outils, que la forge sait afficher
Une subtilité à remarquer dès aujourd'hui

Le job secrets demande tout l'historique par fetch-depth: 0, puis lance Gitleaks avec --no-git — qui, précisément, ignore l'historique et ne regarde que les fichiers présents.

Ce n'est pas une erreur : analyser tout l'historique à chaque pull request coûte cher, et vous l'avez déjà purgé hier. Mais le terrain est préparé : le jour où vous retirerez --no-git, l'historique sera là. Repérer ce genre de détail dans un workflow écrit par quelqu'un d'autre est exactement ce qu'on attend d'un ingénieur sécurité.

La sécurité du pipeline lui-même

Un workflow exécute du code que vous n'avez pas écrit, avec un jeton d'accès à votre dépôt. C'est une cible, et elle a été attaquée en conditions réelles à plusieurs reprises ces dernières années.

  • Le jeton au moindre privilège. permissions: en tête du workflow réduit ce qu'un script compromis peut faire. Sans cette déclaration, le jeton par défaut peut souvent écrire dans le dépôt.
  • L'épinglage des actions. actions/checkout@v4 désigne une étiquette, que le mainteneur peut déplacer vers un autre commit. L'épinglage strict se fait par empreinte complète — actions/checkout@<40 caractères> — ce qui fige exactement le code exécuté.
  • Les secrets ne vivent jamais dans le fichier. Ils vont dans le magasin de secrets de la forge et sont injectés à l'exécution. La forge masque leur valeur dans les journaux — mais seulement si elle la reconnaît : une clé transformée, découpée ou encodée ressort en clair.
  • Attention aux déclencheurs sur les contributions externes. Un workflow déclenché par une pull request venue d'un fork, s'il s'exécute avec les secrets du dépôt, donne ces secrets à quiconque ouvre une pull request. C'est la mauvaise configuration la plus classique du domaine.

Adopter un verrou sur un dépôt qui existe déjà

Votre dépôt a trois jours et dix failles connues. Un dépôt d'entreprise en a quinze ans et trois mille findings. Y activer un verrou absolu bloquerait toutes les équipes le premier matin, et le verrou serait désactivé avant midi. La pratique professionnelle est la ligne de base :

  • on mesure l'existant, et on l'enregistre comme référence acceptée ;
  • le verrou ne bloque que sur les nouveaux écarts — le code touché par la pull request en cours ;
  • la dette de référence est traitée par lots, avec des échéances, exactement comme votre tableau d'exceptions d'hier ;
  • la ligne de base ne peut que descendre : c'est la règle qui empêche le dispositif de devenir cosmétique.

Vous appliquerez ce principe cet après-midi à votre échelle : votre pipeline passera au vert avec quatre failles encore ouvertes — datées, justifiées, assignées au module 2. Ce n'est pas un compromis honteux, c'est la seule façon connue de faire vivre un verrou.

Si le réseau vous lâche

Le même fichier ci.yml s'exécute sur votre poste, sans GitHub, grâce à act, qui rejoue les jobs dans Docker : act -j sast. C'est le repli hors ligne du parcours — vous ne serez jamais bloqué par une coupure ou par un quota. La première exécution télécharge une image de runner assez lourde : lancez-la pendant une pause.

✓ terminé
Quiz

Quiz — Pipeline, verrous et exceptions

Jour 3 · Le pipeline qui refuse la faille · 10 min

1. Pourquoi déclarer permissions: contents: read en tête d'un workflow ?

2. Vous écrivez uses: actions/checkout@v4. Quel risque subsiste ?

3. Votre hook pre-commit refuse un commit. Que démontre git commit --no-verify ?

4. Semgrep signale une faille que vous ne corrigerez qu'au module suivant. Que faites-vous ?

5. Dans un diagramme de flux de données, où STRIDE demande-t-il de concentrer l'analyse ?

✓ terminé
Travaux pratiques

TP 10 — Le premier pipeline, et il est rouge

Jour 3 · Le pipeline qui refuse la faille · 55 min

Poser le workflow de sécurité et le voir refuser une pull request — avant de chercher à le faire passer.

  1. Créez le workflow. Attention au chemin : deux niveaux, et le point initial compte. Une erreur ici ne produit aucun message.
    cd ~/poei-s4/devsecops-lab
    git switch -c feat/pipeline-securite
    mkdir -p .github/workflows
  2. Écrivez .github/workflows/ci.yml. Recopiez-le tel quel : il grandira aux modules suivants, il ne se réinvente pas.
    name: ci-securite
    on:
      push:
        branches: [ main ]
      pull_request:
        branches: [ main ]
    permissions:
      contents: read
      security-events: write
    jobs:
      secrets:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
            with:
              fetch-depth: 0
          - name: gitleaks
            run: |
              docker run --rm -v "$PWD:/repo" zricethezav/gitleaks:v8.18.4 \
                detect --source=/repo --no-git -v --report-path=/repo/gitleaks.json
      sast:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - name: semgrep
            run: |
              docker run --rm -v "$PWD:/src" semgrep/semgrep:1.85.0 \
                semgrep scan --config p/python --config p/flask --config p/security-audit \
                --error --sarif --output /src/semgrep.sarif /src
    
  3. Ajoutez l'archivage des rapports. Le workflow tel quel écrit ses fichiers dans l'espace de travail du runner, qui est détruit à la fin du job : un artefact qu'on ne récupère pas n'existe pas. Insérez ces quatre lignes à la fin de chacun des deux jobs, au même niveau d'indentation que les autres étapes.
          - name: archiver le rapport
            if: always()
            uses: actions/upload-artifact@v4
            with:
              name: rapport-secrets      # ou rapport-sast dans le second job
              path: gitleaks.json        # ou semgrep.sarif dans le second job
    
  4. Notez le if: always() : sans lui, l'étape d'archivage serait sautée dès que le job échoue — c'est-à-dire précisément quand le rapport est intéressant. C'est une erreur de configuration extrêmement répandue.
  5. Rejouez la démonstration d'hier : remettez un secret dans le code et committez-le en contournant le hook. Vous savez que votre garde-fou local ne peut pas l'empêcher ; l'objet du TP est de voir qui l'arrêtera.
    # Dans app.py, ajoutez cette ligne apres les imports :
    #   TOKEN_TEMPORAIRE = "ghp_bV3xQ9pLmN2rT8sK1wY6zC4hJ7dF0aE5uI9o"
    
    git add app.py .github/workflows/ci.yml
    git commit --no-verify -m "feat: pipeline de securite (avec un secret oublie)"
    
    # Le hook n'a rien dit. Le commit existe.
    git log --oneline -1
  6. Poussez la branche et ouvrez une pull request vers main.
    git push -u origin feat/pipeline-securite
    
    # Puis, sur GitHub : Compare & pull request -> Create pull request
  7. Ouvrez l'onglet Actions et regardez les deux jobs tourner en parallèle. Comptez trois à cinq minutes au premier passage : Semgrep télécharge ses jeux de règles. Si l'onglet reste désespérément vide, votre fichier n'est pas au bon endroit — revenez à l'étape 1.
  8. Les deux jobs échouent. C'est le résultat attendu. Ouvrez le journal de chacun et répondez par écrit : quelle règle a mordu, sur quel fichier, à quelle ligne ? Pour secrets, vous devez retrouver le jeton que vous venez d'introduire. Pour sast, faites l'inventaire complet — vous en aurez besoin dans une heure.
  9. Revenez sur la pull request et regardez le bouton de fusion : il est indisponible. Essayez quand même. C'est ce que votre hook local ne pouvait pas faire, et c'est toute la différence entre un confort et un verrou.
  10. Récupérez les artefacts en bas de la page du workflow — section Artifacts — et rangez-les. Le SARIF vous servira au module 2 comme point de comparaison.
    # Telechargez depuis l'interface, puis :
    mv ~/Downloads/rapport-sast.zip ~/poei-s4/devsecops-lab/artefacts/
    cd ~/poei-s4/devsecops-lab/artefacts && unzip -o rapport-sast.zip && cd -
  11. Repli hors ligne. Le même fichier s'exécute sur votre poste sans GitHub. Lancez-le une fois pour savoir le faire le jour où le réseau tombera. La première exécution télécharge une image de runner volumineuse.
    brew install act        # ou : https://github.com/nektos/act
    
    act -j sast
    echo "code de sortie : $?"
Questions de réflexion — à préparer par écrit
  • Votre hook local et votre pipeline exécutent le même Gitleaks, avec la même version. Pourquoi l'un a-t-il laissé passer ce que l'autre a arrêté ?
  • Le workflow demande tout l'historique puis analyse les fichiers seuls. Dans quel cas faudrait-il retirer --no-git, et qu'est-ce que cela coûterait ?
  • Un pipeline qui prend vingt minutes finit par ne plus être attendu. En quoi la durée d'un pipeline est-elle un paramètre de sécurité ?
Pour vous repérer

L'objectif de ce TP est un échec. Si votre pipeline passe au vert du premier coup, quelque chose ne va pas : ou bien le workflow ne s'est pas déclenché, ou bien vous avez corrigé une faille par anticipation. Un pipeline qu'on n'a jamais vu échouer n'a jamais été éprouvé.

Le workflow doit vivre dans .github/workflows/ — deux niveaux, et le point initial compte. Une erreur de chemin ne produit aucun message : l'onglet Actions reste simplement vide, et l'on cherche du côté du contenu du fichier alors que le problème est son emplacement.

✓ terminé
Travaux pratiques

TP 11 — Du rouge au vert, sans rien cacher

Jour 3 · Le pipeline qui refuse la faille · 55 min

Corriger VULN-01 et VULN-05, tracer les quatre exceptions restantes, et rendre les deux vérifications obligatoires.

  1. D'abord le secret, puisqu'il est le plus grave et le plus simple. Retirez la ligne que vous avez introduite au TP précédent, et vérifiez localement avant de pousser quoi que ce soit.
    # Supprimez la ligne TOKEN_TEMPORAIRE d'app.py, puis :
    
    docker run --rm -v "$(pwd)":/repo zricethezav/gitleaks:v8.18.4 \
      detect --source=/repo --no-git --config=/repo/.gitleaks.toml -v
    echo "code de sortie : $?"   # attendu : 0
  2. VULN-01 : la requête paramétrée. Le pilote de base de données reçoit désormais la requête et les valeurs séparément : il n'y a plus de chaîne à interpréter, donc plus rien à injecter. C'est une séparation structurelle, pas un filtrage — et c'est pour cela qu'elle est complète, là où échapper les apostrophes à la main serait une liste noire toujours incomplète.
    # Dans app.py, remplacez le corps de la route /login :
    
    @app.route("/login", methods=["POST"])
    def login():
        # VULN-01 corrigee : requete parametree, les valeurs ne sont plus concatenees.
        user = request.form.get("user", "")
        pwd = request.form.get("pwd", "")
        row = db().execute(
            "SELECT * FROM users WHERE user=? AND pwd=?", (user, pwd)
        ).fetchone()
        return ("bienvenue" if row else "refuse"), (200 if row else 401)
    
  3. Prouvez la correction par le rejeu, pas par le silence de l'outil. Trois essais : l'authentification légitime marche toujours, le mauvais mot de passe échoue, et la charge d'injection échoue désormais aussi. Les trois comptent.
    set -a && source .env && set +a
    python app.py &
    sleep 2
    
    # 1. Legitime — attendu : bienvenue [200]
    curl -s -w ' [%{http_code}]\n' -X POST http://localhost:5000/login \
      --data "user=admin&pwd=admin123"
    
    # 2. Mauvais mot de passe — attendu : refuse [401]
    curl -s -w ' [%{http_code}]\n' -X POST http://localhost:5000/login \
      --data "user=admin&pwd=faux"
    
    # 3. Injection — attendu DESORMAIS : refuse [401]
    curl -s -w ' [%{http_code}]\n' -X POST http://localhost:5000/login \
      --data "user=admin'--&pwd=x"
    
    kill %1
  4. Notez le premier essai : il est le plus important des trois. Une correction qui casse l'authentification légitime n'est pas une correction, c'est une régression — et c'est l'erreur la plus fréquente quand on corrige une injection sous pression.
  5. Les quatre failles restantes. Elles appartiennent au module 2, avec une échéance inscrite hier. Ouvrez votre rapport de pipeline et relevez les identifiants de règle exacts que votre exécution a produits — ils dépendent de la version des jeux de règles, et une exception qui ne correspond à rien ne se voit jamais.
    # Depuis votre SARIF telecharge au TP precedent :
    python3 -c "import json;d=json.load(open('artefacts/semgrep.sarif'));\
    [print(r['ruleId'],'->',r['locations'][0]['physicalLocation']['region']['startLine']) \
     for r in d['runs'][0]['results']]"
  6. Écrivez les exceptions dans le code, sous leur forme nommée. Un nosemgrep nu désactiverait toutes les règles sur la ligne, y compris celles qui n'existent pas encore : c'est un blanc-seing, pas une exception. Remplacez les identifiants par ceux de votre propre rapport.
    # Forme a utiliser, une ligne au-dessus du code concerne :
    
    # nosemgrep: <identifiant-exact-de-votre-rapport>
    # VULN-02 — exception acceptee le 2026-08-05, echeance module 2 (2026-08-10).
    # Faille pedagogique deliberee. Suivi : README section 5, journal-incidents.
    return render_template_string("<h1>Resultats pour " + terme + "</h1>")
    
    # Faites de meme pour VULN-03 (/ping), VULN-06 (/hash), VULN-07 (/config).
    
  7. Mettez à jour le tableau des exceptions du README — il porte désormais les règles Semgrep en plus des règles Bandit — puis committez et poussez sur votre branche. Sans --no-verify cette fois : le hook doit passer.
    git add app.py README.md journal-incidents.md
    git commit -m "fix(VULN-01): requete SQL parametree ; exceptions M2 tracees"
    git push
  8. Retournez sur la pull request. Les deux jobs doivent être verts. S'il reste un échec sur sast, c'est presque toujours qu'un identifiant de règle ne correspond pas exactement à celui de votre rapport — relisez le journal du job, il vous donne la ligne.
  9. Fermez la chaîne. Les jobs passent, mais rien ne les rend encore obligatoires : une pull request en échec resterait fusionnable. Sur GitHub, Settings → Branches → main → Edit, cochez Require status checks to pass before merging et sélectionnez secrets et sast. Ils apparaissent enfin dans la liste : c'est parce qu'ils ont tourné au moins une fois, exactement comme annoncé hier.
  10. Fusionnez la pull request, puis vérifiez que main est verte et que votre historique est signé de bout en bout.
    git switch main && git pull
    
    git log --show-signature --oneline -5
    git log --oneline | head
  11. Votre pipeline est vert avec quatre failles ouvertes. Ce n'est pas une contradiction : vert signifie « aucune règle active n'a mordu », et quatre règles sont explicitement suspendues jusqu'au 10 août, nommément, dans le code et dans deux documents. C'est exactement ce que fait une équipe qui adopte un verrou sur du code existant — et c'est la différence entre une dette gérée et une dette cachée.
Questions de réflexion — à préparer par écrit
  • Vous avez prouvé la correction par un rejeu plutôt que par le silence de Semgrep. Pourquoi le silence d'un outil ne prouve-t-il rien ?
  • Les quatre exceptions expirent le 10 août. Que doit-il se passer ce jour-là, et qui s'en apercevra si rien n'est fait ?
  • Vous avez coché les vérifications obligatoires après que les jobs sont passés au vert. Quel risque y aurait-il eu à les cocher pendant qu'ils échouaient ?
Pour vous repérer

Ce TP se juge à une chose : ce que vous faites des failles que vous ne corrigez pas. Corriger l'injection SQL est facile. Ce qui est professionnel, c'est de traiter les quatre autres par une exception nommée, datée et signée plutôt que par une règle discrètement retirée. Le premier laisse une trace vérifiable, le second n'en laisse aucune.

N'écrivez pas les nosemgrep à partir de la liste d'un cours : utilisez votre sortie de pipeline, avec les identifiants de règle exacts que votre exécution a produits. Les identifiants varient selon la version des jeux de règles, et une exception qui ne correspond à rien ne se voit jamais.

✓ terminé
Leçon

STRIDE : six questions à poser à chaque flux

Jour 3 · Le pipeline qui refuse la faille · 25 min

Vos deux jours d'outillage ont un angle mort, et il est structurel. Semgrep trouve un motif de code dangereux. Gitleaks trouve une chaîne qui ressemble à un secret. Trivy trouve une version vulnérable. Tous les trois cherchent quelque chose qui est là.

Aucun d'eux ne trouvera jamais quelque chose qui manque — un contrôle d'autorisation absent, une donnée qui traverse une frontière sans être validée, un journal qu'on n'écrit pas. Ces défauts-là ne sont pas des motifs de code : ce sont des décisions d'architecture qu'on n'a pas prises. On les trouve en réfléchissant, pas en scannant.

Quatre questions, avant la méthode

Toute modélisation de menaces, quelle que soit la méthode, répond à quatre questions posées dans cet ordre :

  1. Qu'est-ce qu'on construit ? — le diagramme.
  2. Qu'est-ce qui peut mal tourner ? — les menaces. C'est ici que STRIDE sert.
  3. Qu'est-ce qu'on fait à ce sujet ? — les mesures, ou l'acceptation explicite du risque.
  4. A-t-on fait du bon travail ? — la revue, et le fait de recommencer au prochain changement d'architecture.

Le diagramme de flux de données

Cinq symboles suffisent, et le dernier est le plus important.

ÉlémentCe que c'estDans votre application
Entité externequelqu'un ou quelque chose hors de votre contrôlele navigateur, l'URL arbitraire appelée par /fetch
Processusdu code qui s'exécute et transforme la donnéel'application Flask
Magasin de donnéesun endroit où la donnée se reposelab.db, le fichier SQLite
Flux de donnéesun déplacement de donnée entre deux élémentsla requête HTTP, la requête SQL, l'appel système
Frontière de confiancela limite où la donnée change de zone de contrôleentre le navigateur et l'application — et c'est là que tout se joue
frontière de confiance navigateur entité externe app Flask processus · port 5000 lab.db magasin · SQLite shell du système ping · subprocess URL arbitraire entité externe F1 F2 F3 F4 F5
Le DFD de devsecops-lab : cinq flux, une frontière de confiance. C'est elle que STRIDE interroge en premier.
La frontière de confiance est l'élément qui fait tout le travail

Une donnée qui reste dans votre zone de contrôle ne pose pas de problème. Une donnée qui franchit une frontière cesse d'être maîtrisée et devient une hypothèse — quelque chose que vous croyez savoir sur une entrée que quelqu'un d'autre a fabriquée.

C'est pour cela qu'on commence toujours par tracer les frontières : les menaces s'agglutinent dessus. Un diagramme sans frontière de confiance est un schéma d'architecture, pas un modèle de menaces.

Les six lettres

Pour chaque flux qui traverse une frontière, on pose six questions. Chacune correspond à une propriété de sécurité qui pourrait être violée.

LettreMenacePropriété violéeSur votre application
SSpoofing — usurpation d'identitéauthentificationl'injection SQL du /login permettait de devenir admin
TTampering — altération de donnéesintégritél'injection de commande de /ping permet d'écrire sur le système
RRepudiation — nier une actionnon-répudiationaucun journal applicatif : rien ne trace qui a fait quoi
IInformation disclosure — divulgationconfidentialitédebug=True expose la trace d'exécution et le code source
DDenial of service — indisponibilitédisponibilité/fetch sans limite : une URL lente immobilise un processus
EElevation of privilege — élévationautorisation/profile/<uid> : aucun contrôle, on lit le profil de n'importe qui

Ce que STRIDE trouve et que vos outils ne trouveront jamais

Regardez la dernière ligne. VULN-04, l'IDOR, est la seule des dix qui n'apparaîtra dans aucun rapport de votre pipeline — ni aujourd'hui, ni au module 2, ni au module 3. Aucun scanner ne peut la voir, pour une raison simple et définitive :

le code est parfaitement correct. Il lit un enregistrement par son identifiant, ce qui est exactement ce qu'on attend d'une fonction de ce nom. Ce qui manque, c'est une ligne qui n'a jamais été écrite — la vérification que l'utilisateur connecté a le droit de consulter ce profil-là. Un outil ne détecte pas l'absence d'une intention.

Il faudra un module 5 entier, et un humain qui se demande « et si je changeais ce chiffre ? », pour la trouver. C'est la meilleure justification de la modélisation des menaces que vous rencontrerez dans ce parcours, et elle est dans votre propre code.

Les limites de STRIDE, à connaître dès maintenant

STRIDE énumère, il ne priorise pas. Il produit une liste de menaces, pas un ordre de traitement. La priorisation viendra au module 4, avec CVSS et DREAD.

Il traite mal certaines familles : le déni de service distribué, les risques organisationnels, la chaîne d'approvisionnement logicielle s'y logent mal. Le module 4 introduira PASTA et les arbres d'attaque pour les couvrir.

Il dépend entièrement de la qualité du diagramme. Un flux oublié est une menace jamais posée — et c'est de très loin la première cause de modèle de menaces inutile.

Ce que vous produisez cet après-midi est un brouillon de travail. Le module 4 le reprendra pour en faire rapports/threat-model.md, avec le registre de risques et les scores. Aujourd'hui, on installe le geste.

✓ terminé
Travaux pratiques

TP 12 — STRIDE sur devsecops-lab : le diagramme et la table

Jour 3 · Le pipeline qui refuse la faille · 55 min

Produire un modèle de menaces de votre application, et confronter ce qu'il trouve à ce que vos outils ont trouvé.

  1. Créez le fichier. Attention au nom : threat-model.md tout court est réservé au livrable du module 4, qui reprendra votre brouillon. Ne l'écrasez pas d'avance.
    cd ~/poei-s4/devsecops-lab
    mkdir -p rapports
    touch rapports/stride-devsecops-lab-m1.md
  2. Dessinez le diagramme avant tout. En texte, c'est parfait — il doit être versionnable et relisible, pas beau. Le seul élément non négociable est la frontière de confiance.
    # Modele de menaces — devsecops-lab (brouillon M1)
    
    ## 1. Diagramme de flux de donnees
    
    ```
            ZONE NON FIABLE          |          ZONE APPLICATIVE
                                     |
      [navigateur]  --- F1 --->  ( app Flask )  --- F3 --->  [[ lab.db ]]
      entite externe <-- F2 ---   port 5000     --- F4 --->  [[ shell OS ]]
                                     |          --- F5 --->  [url arbitraire]
                                     |
                        frontiere de confiance
    ```
    
    | Flux | De | Vers | Donnee transportee | Traverse la frontiere |
    |------|----|----- |--------------------|------------------------|
    | F1 | navigateur | app Flask | parametres HTTP (user, pwd, q, host, uid, url) | **oui** |
    | F2 | app Flask | navigateur | reponses HTML, JSON, traces d'erreur | **oui** |
    | F3 | app Flask | lab.db | requetes SQL construites | non |
    | F4 | app Flask | shell OS | ligne de commande construite | non |
    | F5 | app Flask | URL arbitraire | requete HTTP sortante | **oui** |
    
  3. Remarquez ce que le tableau vous apprend déjà, avant toute analyse : trois flux sur cinq traversent la frontière, et F5 la traverse vers l'extérieur, ce qui est le cas le moins intuitif. Une application qui appelle une adresse fournie par l'utilisateur transforme votre serveur en client de l'attaquant. Vous y reviendrez au module 5.
  4. Passez chaque flux au crible des six lettres. Toutes les cases ne se remplissent pas — cinq flux fois six lettres font trente cases, et beaucoup sont sans objet. Une case vide assumée vaut mieux qu'une menace inventée. Voici la structure et deux lignes pour donner le ton.
    ## 2. Menaces par flux (STRIDE)
    
    | Flux | Lettre | Menace | Mesure existante | Verdict |
    |------|--------|--------|------------------|---------|
    | F1 | S | Un anonyme se fait passer pour l'administrateur en manipulant les parametres du formulaire | requete parametree depuis le 2026-08-05 (VULN-01) | **couverte** |
    | F1 | E | Un utilisateur authentifie consulte le profil d'un autre en changeant l'identifiant dans l'URL | **aucune** — aucun controle d'autorisation | **ouverte (VULN-04)** |
    | F1 | T | ... | | |
    | F2 | I | ... | | |
    | F1 | R | ... | | |
    | F5 | I | ... | | |
    
  5. Complétez la table. Insistez particulièrement sur le R, la répudiation : c'est la lettre que tout le monde bâcle, parce qu'elle ne porte pas sur ce qui est écrit mais sur ce qui ne l'est pas. Posez-vous la question franchement : si quelqu'un s'authentifiait frauduleusement sur votre application ce soir, quelle trace en resterait-il demain matin ? Écrivez la réponse, même si elle est « aucune ».
  6. Le croisement — c'est le cœur du TP. Confrontez votre table à la liste des dix VULN et remplissez les trois cases. La troisième est celle qui compte.
    ## 3. Croisement avec les dix vulnerabilites connues
    
    ### 3.1 Menaces confirmees par mes outils
    _(STRIDE et le pipeline disent la meme chose — le recouvrement est rassurant)_
    
    ### 3.2 VULN que STRIDE ne fait pas ressortir
    _(exemple : VULN-10, dependances a CVE. Ce n'est pas un defaut de conception
     du systeme que je dessine, c'est une propriete de sa chaine d'approvisionnement.
     Les deux approches ne se remplacent pas.)_
    
    ### 3.3 Menaces qu'AUCUN outil du parcours ne trouvera
    _(la section la plus importante. Pour chacune : la menace, et POURQUOI aucun
     scanner ne peut la voir.)_
    
  7. Pour la section 3.3, trouvez-en au moins deux. VULN-04 en est une, et la leçon vous a donné le motif : le code est correct, c'est une ligne jamais écrite qui manque, et un outil ne détecte pas l'absence d'une intention. Cherchez-en une seconde — regardez du côté du R, et du côté de ce qui se passerait si votre application recevait dix mille requêtes par seconde.
  8. Terminez par les limites de votre propre modèle. C'est la section qui distingue un professionnel : savoir ce que son analyse ne couvre pas.
    ## 4. Limites de ce modele
    
    - Etabli le 2026-08-05 sur l'architecture du jour ; a rejouer a chaque changement
      d'architecture (conteneurisation K8s au module 3, notamment).
    - STRIDE enumere, il ne priorise pas : aucun score ici. La priorisation par CVSS et
      DREAD est l'objet du module 4.
    - Perimetre : l'application seule. Ni le poste, ni le reseau, ni la chaine de
      construction (le pipeline lui-meme est une surface d'attaque non modelisee ici).
    - Le deni de service distribue et les risques organisationnels se logent mal dans
      STRIDE ; le module 4 introduira PASTA et les arbres d'attaque.
    
  9. Committez et poussez par une pull request. Votre pipeline doit rester vert : un fichier Markdown ne déclenche aucune règle, mais l'aller-retour complet est devenu le geste normal.
    git switch -c docs/stride-m1
    git add rapports/stride-devsecops-lab-m1.md
    git commit -m "docs: modele de menaces STRIDE (brouillon M1)"
    git push -u origin docs/stride-m1
Questions de réflexion — à préparer par écrit
  • Votre section 3.3 contient au moins deux menaces qu'aucun outil ne trouvera. Qu'est-ce que cela dit du rapport entre outillage et conception ?
  • Le flux F5 traverse la frontière vers l'extérieur. En quoi ce sens est-il plus difficile à raisonner que le sens entrant ?
  • Vous avez trouvé des VULN que STRIDE ne fait pas ressortir. Est-ce une faiblesse de la méthode, ou autre chose ?
Pour vous repérer

La faute qui vide l'exercice de son sens : passer directement à la liste des menaces sans dessiner le diagramme. On produit alors une liste générique — « il pourrait y avoir une injection », « il pourrait y avoir une fuite » — vraie pour n'importe quelle application, donc utile pour aucune. Le diagramme d'abord, et surtout la frontière de confiance.

Deuxième point : ne cherchez pas des menaces déjà connues. L'intérêt de l'étape 5 est précisément d'en trouver au moins une qui ne figure dans aucune de vos dix VULN — c'est elle qui justifie la méthode.

✓ terminé
Travaux pratiques

TP 13 — La note de synthèse du module

Jour 3 · Le pipeline qui refuse la faille · 40 min

Rassembler les livrables du module et écrire ce que vous défendrez devant votre encadrant.

  1. Complétez la section 4 du README : elle porte maintenant le pipeline en plus des garde-fous locaux.
    ### Pipeline (module 1, jour 3)
    
    `.github/workflows/ci.yml` — deux jobs sur chaque pull request vers `main` :
    
    | Job | Outil | Verrou | Artefact |
    |-----|-------|--------|----------|
    | secrets | gitleaks v8.18.4 | code de sortie non nul si un secret est trouve | gitleaks.json |
    | sast | semgrep 1.85.0 (`--error`) | code de sortie non nul si une regle correspond | semgrep.sarif |
    
    Les deux sont des **verifications obligatoires** sur `main` : une pull request dont un
    job echoue ne peut pas etre fusionnee, y compris par le proprietaire du depot.
    
    Repli hors ligne : `act -j sast`.
    
  2. Complétez la section 6, « Limites connues ». C'est la section que personne n'écrit et la première que lit un auditeur.
    ## 6. Limites connues
    
    - Six vulnerabilites restent ouvertes a la fin du module 1 : VULN-02, 03, 06, 07, 10
      (echeance module 2) et VULN-04, 08, 09 (modules 3 et 5). Toutes sont tracees.
    - Le pipeline ne couvre que le code source : ni les dependances (module 2), ni
      l'image (module 3), ni l'application en execution (module 2, DAST).
    - Gitleaks analyse l'arbre de travail (`--no-git`) et non l'historique a chaque
      passage. L'historique a ete purge le 2026-08-04 et verifie a cette date.
    - Les hooks pre-commit ne sont pas garantis : ils vivent hors du depot versionne et
      se contournent. Le verrou reel est le pipeline.
    - Le modele de menaces est un brouillon etabli sur l'architecture du 2026-08-05.
    
  3. Écrivez la note de synthèse. Une page, pas deux. Ce qui ne tient pas en une page n'est pas encore compris. La partie qui compte est la deuxième : trois arbitrages que vous défendez, pas trois choses que vous avez faites.
    # Note de synthese — Module 1, DevSecOps : fondations
    Elie Zaraket · 3 au 5 aout 2026
    
    ## Ce que j'ai construit
    _(cinq lignes : le depot, la chaine de confiance, le pipeline, le modele de menaces)_
    
    ## Les trois arbitrages que je defends
    
    **1. Le verrou vit en CI, pas dans le hook local.**
    _(pourquoi : je l'ai contourne moi-meme avec --no-verify le 4 aout ; le hook aide,
     il ne garantit pas)_
    
    **2. Quatre failles restent ouvertes derriere une exception datee, plutot qu'une
    regle retiree.**
    _(pourquoi : l'exception laisse une trace verifiable et une echeance ; la regle
     retiree ne laisse rien, et un pipeline rouge en permanence cesse d'etre lu)_
    
    **3. La rotation precede la reecriture de l'historique.**
    _(pourquoi : la reecriture ne desexpose rien ; un secret expose est compromis, et
     la preuve du contraire n'existe pas)_
    
    ## Ce qui manque encore
    _(trois lignes, en pointant vers les modules qui s'en chargent)_
    
  4. Vérifiez que vos questions de réflexion des treize TP sont répondues dans reponses-reflexion.md. C'est le document dont votre encadrant tirera ses questions demain — pas de vos scripts. Deux réponses développées valent mieux que trente expédiées.
  5. Faites l'inventaire de vos livrables. Les huit doivent exister et être à jour.
    cd ~/poei-s4/devsecops-lab
    ls -1 README.md journal-incidents.md reponses-reflexion.md \
          note-synthese-m1.md .pre-commit-config.yaml .gitleaks.toml \
          .github/workflows/ci.yml rapports/stride-devsecops-lab-m1.md
  6. Commit final du module, signé, fusionné par pull request avec un pipeline vert. Puis posez une étiquette : elle marquera l'état exact du dépôt à la fin du module 1, et vous pourrez y revenir depuis n'importe quel module ultérieur.
    git add -A
    git commit -m "docs: note de synthese et limites connues du module 1"
    git push
    
    # Apres fusion de la pull request :
    git switch main && git pull
    git tag -a m1-fin -m "Fin du module 1 : depot, chaine de confiance, pipeline"
    git push origin m1-fin
  7. Dernière vérification, et c'est celle que le bilan reprendra : votre dépôt est-il rejouable par quelqu'un d'autre ? Ne le vérifiez pas en relisant votre README — clonez-le dans un dossier neuf et suivez vos propres instructions à la lettre.
    git clone git@github.com:<votre-compte>/devsecops-lab.git /tmp/verif-m1
    cd /tmp/verif-m1
    
    # Suivez VOTRE section 1, puis VOTRE section 3, sans rien deviner.
    # Si une valeur vous manque, c'est votre README qui est incomplet — corrigez-le.
    cat .env.exemple
    
    cd - && rm -rf /tmp/verif-m1
Questions de réflexion — à préparer par écrit
  • Vous avez cloné votre dépôt dans un dossier neuf. Qu'avez-vous découvert que la relecture du README ne vous avait pas montré ?
  • Vos trois arbitrages sont défendables. Lequel serait le plus difficile à faire accepter à une équipe qui livre tous les jours, et comment le présenteriez-vous ?
✓ terminé
Point de contrôle

M1 · J3 — Pipeline rouge puis vert sur VULN-01 et VULN-05

Jour 3 · Le pipeline qui refuse la faille · 2 h

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

Elle a lieu mercredi à 15 h 00 et dure deux heures — c'est la durée portée par ce point de contrôle. Votre encadrant passera en revue vos livrables, vous fera expliquer les notions, et validera — ou non — le module.

Elle s'ouvre par une démonstration en direct : vous ouvrez une pull request avec une faille, le pipeline la refuse, vous corrigez, elle passe. Préparez la branche à l'avance, mais faites la manipulation devant lui.

Il partira ensuite de votre reponses-reflexion.md et de votre note de synthèse : ce sont les deux documents qui montrent ce que vous avez compris, et non ce que vous avez exécuté.

Ce qui n'est pas attendu : un dépôt sans faille. Six vulnérabilités sont encore ouvertes et c'est le programme. Ce qui est attendu, c'est que vous sachiez lesquelles, pourquoi, et jusqu'à quand.

✓ terminé

Mon avancement

Signature d'émargement

Demander de l'aide à votre encadrant

Étape concernée :

Avant d'envoyer

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