🧪 TP SLAM — Audit de code automatisé · 2 séances · 3h

Bloc B3 Option SLAM Semgrep + docker-compose 3h
🎬 Mode présentation (slides) 📄 Export PDF
PublicBTS SIO 2e année, option SLAM
DuréeSéance 1 (1h) + Séance 2 (2h) = 3h
LienFait suite au cours théorique 2h — Sécurité proactive
CompétencesB3.3 Assurer la cybersécurité d'une solution applicative
PrérequisGit + GitHub, bases YAML, Docker + docker-compose, CLI Linux

0. Contexte pédagogique — on intègre Semgrep dans un projet conteneurisé

Vous reprenez un projet web de Mediaschool déjà dockerisé : un docker-compose.yml qui orchestre un front Node.js et une API Python derrière un reverse proxy. Le code est hébergé sur un repo privé GitHub. Le CTO a signé une PSSI qui impose désormais :

  • Aucun code en main ne peut contenir de finding OWASP de sévérité ERROR.
  • Les images Docker et le docker-compose.yml doivent être scannés au même titre que le code applicatif (supply chain OWASP A03:2025).
  • Chaque PR doit produire un rapport d'audit téléchargeable par le reviewer.
  • Le runner CI tourne sur l'infra Mediaschool — pas de runner cloud, pas de code qui sort du périmètre. Le runner lui-même est un service du docker-compose.yml.

Votre mission : livrer le runner GitHub auto-hébergé comme service compose, écrire un workflow qui lance Semgrep dans un container sur chaque PR, bloquer les merges en cas de finding critique et produire un audit Markdown. Tout en conteneurisé, rien en natif sur l'hôte.

1. Objectifs pédagogiques

  • Déployer un runner GitHub Actions auto-hébergé comme service docker-compose (pas d'install native).
  • Intégrer Semgrep dans la CI via l'image officielle returntocorp/semgrep (scan en container).
  • Scanner le Dockerfile + docker-compose.yml avec les rulesets Semgrep dédiés (p/dockerfile, p/docker-compose).
  • Configurer des règles Semgrep bloquantes (mode Block / ERROR).
  • Générer un audit au format Markdown + SARIF, l'uploader en artefact et vers l'onglet Security de GitHub.
  • Protéger la branche main pour forcer le passage du check.

2. Prérequis matériels

ÉlémentDétail
Compte GitHubUn par binôme, un repo privé par binôme
Accès serveur IRISSSH avec clé, utilisateur runner-<binome> membre du groupe docker
Docker engineVersion ≥ 24.0 avec plugin docker compose v2
Stack outilsgit, curl, jq — pas besoin de pipx ni de binaires natifs
RéseauHTTPS sortant vers github.com, ghcr.io, docker.io, semgrep.dev

Séance 1 — Tutoriel : auto-héberger un runner GitHub avec Docker + premier scan (1h)

Étape 1.1 — Fork du repo TP et découverte du projet (5 min)

Fork du repo fourni par le formateur : iris-formations/tp-semgrep-compose-sample. Cloner en local :

git clone git@github.com:<votre_user>/tp-semgrep-compose-sample.git
cd tp-semgrep-compose-sample
tree -L 2

Structure du projet :

.
├── docker-compose.yml       # stack front + api + reverse-proxy
├── front/                   # Node.js (Express + EJS)
│   ├── Dockerfile
│   └── src/
├── api/                     # Python (FastAPI)
│   ├── Dockerfile
│   └── app/
├── .github/workflows/       # vide — à compléter en séance 2
└── README.md

Le repo contient volontairement 6 bugs plantés : SQLi, XSS, secret hardcodé, path traversal, + 1 anti-pattern dans le Dockerfile (image :latest + run en root) + 1 dans le docker-compose.yml (port 3306 exposé publiquement). Vous devez tous les attraper.

Étape 1.2 — Auto-héberger un runner GitHub avec Docker — tutoriel complet (30 min)

💡 C'est quoi un runner auto-hébergé ?

Par défaut, GitHub Actions exécute les workflows sur ses propres machines (runners cloud). Un runner auto-hébergé est une machine que vous contrôlez qui écoute les demandes de GitHub et exécute les jobs. Cas d'usage : code confidentiel, secrets internes, accès à une infra privée, contrôle des coûts, matériel spécifique (GPU, ARM…).

Pourquoi le conteneuriser ?

  • Reproductibilité : l'image est une spec exacte. Vous pouvez recréer le runner à l'identique sur n'importe quelle machine.
  • Isolation : le runner n'a pas accès à tout l'hôte — uniquement au socket Docker (si monté) et à son workspace.
  • Cohérence avec le reste du SI : tout l'écosystème Mediaschool est en docker-compose. Le runner doit se gérer comme les autres services (logs, restart policy, healthcheck).
  • Runner éphémère : on peut recréer un container propre à chaque job (flag EPHEMERAL) — impossible en install native.

Image utilisée : myoung34/github-runner — projet open source maintenu par la communauté, alternative mature à l'image officielle GitHub. Référence : github.com/myoung34/docker-github-actions-runner.

1.2.a — Créer un Personal Access Token (PAT) GitHub (5 min)

Le runner a besoin de s'enregistrer auprès de GitHub à son démarrage. Il utilise pour ça un PAT (token utilisateur). Le PAT permet au runner de demander un token de registration éphémère, puis de s'enregistrer tout seul — vous n'avez pas à copier le token manuellement à chaque redémarrage.

GitHub → Settings (votre compte, pas le repo) → Developer settingsPersonal access tokensFine-grained tokensGenerate new token :

  • Token name : iris-runner-<binome>
  • Expiration : 7 jours maximum (PSSI)
  • Repository access : Only select repositories → votre fork uniquement (principe du moindre privilège)
  • Repository permissionsAdministration: Read and write (obligatoire pour enregistrer un runner)

Copier le token affiché (format github_pat_11ABCD...) — il n'est visible qu'une fois. Stocker immédiatement dans votre gestionnaire (Vaultwarden) et dans le secret Docker ci-dessous.

1.2.b — Déclarer le service runner dans le docker-compose.yml (10 min)

Ouvrir docker-compose.yml et ajouter le service gha-runner à la suite des services existants :

services:

  # ... front, api, reverse-proxy existants ...

  gha-runner:
    image: myoung34/github-runner:2.321.0
    container_name: gha-runner-${BINOME}
    restart: unless-stopped

    environment:
      # URL du repo à surveiller (scope = repo unique)
      REPO_URL: https://github.com/${GITHUB_USER}/tp-semgrep-compose-sample
      # Nom affiché dans GitHub → Settings → Actions → Runners
      RUNNER_NAME: iris-runner-${BINOME}
      # PAT lu depuis un Docker secret (jamais en env clair)
      ACCESS_TOKEN_FILE: /run/secrets/gha_pat
      # Scope = repo (alternative: "org" pour un runner mutualisé)
      RUNNER_SCOPE: repo
      # Labels utilisés dans runs-on: du workflow
      LABELS: self-hosted,linux,iris,semgrep,docker
      # Runner éphémère : recréé proprement à chaque job (anti-pollution)
      EPHEMERAL: "true"
      # Pas d'auto-update : on pin la version de l'image
      DISABLE_AUTO_UPDATE: "true"

    volumes:
      # Accès au socket Docker de l'hôte → le runner peut lancer des containers
      # (nécessaire pour les jobs avec `container:` ou `docker run` dans steps)
      - /var/run/docker.sock:/var/run/docker.sock
      # Workspace persisté entre jobs (cache actions/checkout, node_modules...)
      - gha-work:/_work

    secrets:
      - gha_pat

    networks:
      - proxy

volumes:
  gha-work:

secrets:
  gha_pat:
    file: ./secrets/github_pat.txt

networks:
  proxy:
    external: true

Décryptage ligne par ligne :

CléRôle
image: ...:2.321.0Version pinée — pas :latest. Changer explicitement à chaque upgrade.
restart: unless-stoppedLe runner redémarre après un reboot serveur, mais pas si vous l'arrêtez explicitement.
REPO_URLScope repo. Pour un scope org, on mettrait l'URL de l'organisation et RUNNER_SCOPE: org.
ACCESS_TOKEN_FILEDocker secret monté en /run/secrets/. Le runner lit le PAT depuis ce fichier, pas depuis l'env.
LABELSTags arbitraires. Le workflow cible le runner via runs-on: [self-hosted, iris, semgrep] (ET logique).
EPHEMERAL: "true"Le container s'auto-détruit après chaque job et est recréé. Chaque job démarre dans un workspace propre.
/var/run/docker.sockPermet au runner d'orchestrer d'autres containers (nécessaire pour jobs avec container:). Contrepartie : équivalent root sur l'hôte — à n'utiliser que sur un runner de repo privé.
secrets.gha_pat.fileFormat "file-based secret" du compose v3. En prod : Vault / Docker Swarm secrets / Kubernetes secrets.

1.2.c — Préparer les variables et le PAT (5 min)

# Fichier .env (gitignoré) — variables pour interpoler dans compose
cat > .env <<EOF
BINOME=<binome>
GITHUB_USER=<votre_user>
EOF

# Le PAT va dans un fichier séparé, monté comme secret
mkdir -p secrets
printf '%s' "github_pat_11ABCD..." > secrets/github_pat.txt
chmod 600 secrets/github_pat.txt
chown "$USER:$USER" secrets/github_pat.txt

# Critique — exclure secrets et .env du versioning
echo "secrets/" >> .gitignore
echo ".env"     >> .gitignore

# Vérifier que git ne va pas committer ça
git status

La sortie de git status doit montrer uniquement .gitignore et docker-compose.yml en Modified. Si secrets/ ou .env apparaît dans "Untracked" pas encore ignoré, corriger avant de continuer.

1.2.d — Démarrer le runner et vérifier l'enregistrement (10 min)

# Pull + création + démarrage
docker compose up -d gha-runner

# Suivre les logs : le runner s'enregistre en ~10s
docker compose logs -f gha-runner
# Attendre la ligne : "√ Connected to GitHub"
# Puis : "Listening for Jobs"
# Ctrl+C pour quitter le suivi (le container tourne toujours)

# État attendu
docker compose ps
# gha-runner-<binome>   Up X seconds

Vérification côté GitHub — Settings → Actions → Runners :

  • Nouveau runner iris-runner-<binome> apparaît
  • Status : Idle (rond vert) — il attend un job
  • Labels : self-hosted linux iris semgrep docker
🔍 Dépannage
  • Runner registration failed → PAT expiré ou permissions manquantes. Régénérer avec Administration: Read/Write.
  • permission denied /var/run/docker.sock → l'user du container n'est pas dans le groupe docker. Ajouter group_add: ["<GID du groupe docker hôte>"] dans le service.
  • Runner Offline immédiatement après start → vérifier les logs, souvent un problème de résolution DNS ou HTTPS sortant bloqué.
  • Conflit de nom → supprimer l'ancien runner côté GitHub (Runners → Remove runner) et restart le container.

Étape 1.3 — Premier scan Semgrep en container (15 min)

Aucune installation native de Semgrep. On utilise directement l'image officielle : hub.docker.com/r/returntocorp/semgrep.

# Scan du code applicatif
docker run --rm \
  -v "$PWD:/src" \
  -w /src \
  returntocorp/semgrep:latest \
  semgrep scan --config=p/owasp-top-ten --config=p/secrets /src

# Scan spécifique Dockerfiles et docker-compose
docker run --rm -v "$PWD:/src" -w /src returntocorp/semgrep:latest \
  semgrep scan --config=p/dockerfile --config=p/docker-compose /src

Vous devez voir au moins 6 findings : SQLi, XSS, secret hardcodé, path traversal, :latest tag dans un Dockerfile, port DB exposé dans compose.

📦 Rendu fin de séance 1
  1. Le docker-compose.yml complété avec le service gha-runner, chaque clé annotée (décryptage ligne par ligne).
  2. Capture : le runner en état Idle dans GitHub, avec les labels visibles.
  3. Capture : sortie docker compose ps montrant gha-runner Up.
  4. Capture : 6 findings Semgrep (applicatif + Docker) depuis votre poste.
  5. Preuve que .gitignore exclut bien secrets/ et .env (git check-ignore -v secrets/github_pat.txt).

Étape 1.3 — Premier scan Semgrep en container (15 min)

Aucune installation native de Semgrep. On utilise directement l'image officielle : hub.docker.com/r/returntocorp/semgrep.

# Scan du code applicatif
docker run --rm \
  -v "$PWD:/src" \
  -w /src \
  returntocorp/semgrep:latest \
  semgrep scan --config=p/owasp-top-ten --config=p/secrets /src

# Scan spécifique Dockerfiles et docker-compose
docker run --rm -v "$PWD:/src" -w /src returntocorp/semgrep:latest \
  semgrep scan --config=p/dockerfile --config=p/docker-compose /src

Vous devez voir au moins 6 findings : SQLi, XSS, secret hardcodé, path traversal, :latest tag dans un Dockerfile, port DB exposé dans compose.

📦 Rendu fin de séance 1
  1. Le docker-compose.yml complété avec le service gha-runner commenté section par section.
  2. Capture : le runner en état Idle dans GitHub, avec les labels.
  3. Capture : sortie docker compose ps montrant gha-runner Up.
  4. Capture : 6 findings Semgrep (applicatif + Docker) depuis votre poste.

Séance 2 — Workflow containerisé + audit + gating PR (2h)

Étape 2.1 — Workflow GitHub Actions en container (30 min)

Principe : le workflow tourne sur votre runner (service compose), et chaque job s'exécute dans une image Docker dédiée. Semgrep a sa propre image, on l'utilise via la clé container: de GitHub Actions.

Créer .github/workflows/security.yml :

name: Security — SAST Semgrep containerisé

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]
  workflow_dispatch:

permissions:
  contents: read
  security-events: write
  pull-requests: write

jobs:

  # Job 1 : scan du code applicatif dans container Semgrep
  semgrep-code:
    name: Semgrep — code applicatif
    runs-on: [self-hosted, linux, iris, semgrep, docker]
    container:
      image: returntocorp/semgrep:latest
    timeout-minutes: 20
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }

      - name: Semgrep scan code (OWASP + secrets + custom)
        run: |
          semgrep scan \
            --config=p/owasp-top-ten \
            --config=p/secrets \
            --config=.semgrep/rules/ \
            --sarif --output=semgrep-code.sarif \
            --json  --output=semgrep-code.json \
            --error

      - uses: github/codeql-action/upload-sarif@v3
        if: always()
        with: { sarif_file: semgrep-code.sarif, category: code }

      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: semgrep-code-${{ github.run_id }}
          path: 'semgrep-code.*'
          retention-days: 30

  # Job 2 : scan du Dockerfile + docker-compose.yml (supply chain)
  semgrep-compose:
    name: Semgrep — Dockerfile & compose
    runs-on: [self-hosted, linux, iris, semgrep, docker]
    container:
      image: returntocorp/semgrep:latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v4

      - name: Semgrep scan conteneurs
        run: |
          semgrep scan \
            --config=p/dockerfile \
            --config=p/docker-compose \
            --config=.semgrep/rules/containers/ \
            --sarif --output=semgrep-containers.sarif \
            --json  --output=semgrep-containers.json \
            --error

      - uses: github/codeql-action/upload-sarif@v3
        if: always()
        with: { sarif_file: semgrep-containers.sarif, category: containers }

      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: semgrep-containers-${{ github.run_id }}
          path: 'semgrep-containers.*'

  # Job 3 : build et publication du rapport d'audit fusionné
  audit-report:
    name: Audit Markdown
    needs: [semgrep-code, semgrep-compose]
    if: always()
    runs-on: [self-hosted, linux, iris, docker]
    container:
      image: python:3.12-alpine
    steps:
      - uses: actions/checkout@v4
      - uses: actions/download-artifact@v4
        with: { pattern: 'semgrep-*', merge-multiple: true }
      - name: Build audit
        run: python3 .github/scripts/semgrep_to_md.py semgrep-code.json semgrep-containers.json > audit-semgrep.md
      - uses: actions/upload-artifact@v4
        with:
          name: audit-semgrep-${{ github.run_id }}
          path: audit-semgrep.md
          retention-days: 30

Points importants :

  • runs-on: [self-hosted, linux, iris, semgrep, docker] — GitHub cherche un runner qui possède tous ces labels. Le votre match.
  • container: image: returntocorp/semgrep:latest — le steps tournent dans ce container, lancé par le runner via le socket Docker monté en séance 1.
  • --error fait exit ≠ 0 si finding de severity ERROR → le job échoue → PR bloquée par la branch protection.
  • SARIF uploadé avec category: différent par job → 2 rubriques distinctes dans l'onglet Security.

Étape 2.2 — Règles custom (20 min)

Créer .semgrep/rules/no-eval.yml :

rules:
  - id: no-eval
    pattern: eval(...)
    message: |
      Usage de eval() interdit — source d'injection de code.
      Utilise JSON.parse ou un parseur dédié.
    languages: [javascript, typescript, python]
    severity: ERROR
    metadata:
      cwe: CWE-95
      owasp: A05:2025-Injection

Créer .semgrep/rules/containers/no-latest-tag.yml — interdire le tag :latest dans un Dockerfile ou un compose (reproductibilité) :

rules:
  - id: no-latest-tag-in-dockerfile
    pattern: FROM $IMG:latest
    message: |
      Image :latest interdite — épingle une version précise (SHA256 ou tag immutable)
      pour garantir la reproductibilité et éviter les supply chain attacks.
    languages: [dockerfile]
    severity: ERROR
    metadata:
      owasp: A03:2025-Supply-Chain-Failures

  - id: no-latest-tag-in-compose
    patterns:
      - pattern-regex: 'image:\s*[\w./-]+:latest'
    paths:
      include: ['docker-compose*.yml', 'compose*.yml']
    message: |
      Tag :latest dans docker-compose — épingler une version précise.
    languages: [yaml]
    severity: ERROR
    metadata:
      owasp: A03:2025-Supply-Chain-Failures

Créer .semgrep/rules/containers/no-host-network.yml — interdire network_mode: host et les ports d'admin exposés publiquement :

rules:
  - id: no-host-network
    pattern-regex: 'network_mode:\s*["'']?host["'']?'
    paths:
      include: ['docker-compose*.yml', 'compose*.yml']
    message: |
      network_mode: host contourne l'isolation Docker — très risqué en prod.
    languages: [yaml]
    severity: ERROR

  - id: no-public-db-port
    pattern-regex: '["''](3306|5432|27017|6379):(3306|5432|27017|6379)["'']'
    paths:
      include: ['docker-compose*.yml', 'compose*.yml']
    message: |
      Port de base de données exposé publiquement — bind sur 127.0.0.1 uniquement,
      ou utilise un réseau interne entre containers.
    languages: [yaml]
    severity: WARNING

Étape 2.3 — Script JSON → Markdown audit (30 min)

Fichier .github/scripts/semgrep_to_md.py — fusionne plusieurs JSON Semgrep (code + containers) en un seul audit :

#!/usr/bin/env python3
"""Fusionne plusieurs rapports Semgrep JSON en un audit Markdown unique.

Usage : python3 semgrep_to_md.py semgrep-code.json semgrep-containers.json
"""

import json, sys
from collections import Counter
from datetime import datetime, timezone

def badge(sev):
    return {"ERROR":"[CRITIQUE]","WARNING":"[MAJEUR]","INFO":"[MINEUR]"}.get(sev, f"[{sev}]")

def load(path):
    try:
        with open(path, encoding="utf-8") as fh: return json.load(fh)
    except FileNotFoundError: return {"results": [], "errors": []}

def main(paths):
    all_results, all_errors, by_source = [], [], {}
    for p in paths:
        d = load(p)
        r = d.get("results", [])
        all_results += r
        all_errors += d.get("errors", [])
        by_source[p] = len(r)

    now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
    sev = Counter(r.get("extra",{}).get("severity","INFO") for r in all_results)
    verdict = "BLOCKED" if sev.get("ERROR",0) else "PASS"

    out = [f"# Audit sécurité Semgrep — {now}", "",
           f"**Verdict** : `{verdict}`", "",
           "## Synthèse par sévérité", "",
           f"- Total : **{len(all_results)}**",
           f"- ERROR (critiques) : **{sev.get('ERROR',0)}**",
           f"- WARNING (majeurs) : **{sev.get('WARNING',0)}**",
           f"- INFO (mineurs) : **{sev.get('INFO',0)}**", "",
           "## Synthèse par source", ""]
    for src, n in by_source.items(): out.append(f"- `{src}` → {n} findings")
    out.append("")

    if all_results:
        out += ["## Détail", ""]
        for r in sorted(all_results, key=lambda x: {"ERROR":0,"WARNING":1,"INFO":2}.get(
                x.get("extra",{}).get("severity","INFO"), 3)):
            s = r.get("extra",{}).get("severity","INFO")
            meta = r.get("extra",{}).get("metadata",{})
            out += [f"### {badge(s)} `{r.get('check_id')}`", "",
                    f"- Fichier : `{r.get('path','?')}:{r.get('start',{}).get('line','?')}`",
                    f"- CWE : {meta.get('cwe','n/a')}",
                    f"- OWASP : {meta.get('owasp','n/a')}",
                    "",
                    f"> {r.get('extra',{}).get('message','').strip()}", ""]

    print("\n".join(out))
    return 0 if verdict == "PASS" else 2

if __name__ == "__main__":
    sys.exit(main(sys.argv[1:] or ["semgrep.json"]))

Étape 2.4 — Protéger la branche main (10 min)

GitHub → Settings → Branches → Add rule → Branch name pattern : main. Cocher :

  • Require a pull request before merging
  • Require status checks to pass before merging → Semgrep — code applicatif et Semgrep — Dockerfile & compose
  • Require branches to be up to date before merging
  • Do not allow bypassing the above settings

Étape 2.5 — Démonstration complète (20 min)

  1. Créer une branche feature/demo-bug et y ajouter eval(userInput) dans le front + FROM node:latest dans un Dockerfile.
  2. Pousser et ouvrir une PR vers main.
  3. Observer : les 3 jobs démarrent sur votre gha-runner. semgrep-code et semgrep-compose échouent.
  4. Vérifier l'onglet Security → Code scanning alerts : les findings sont groupés par category.
  5. Télécharger l'artefact audit-semgrep-<run_id> → ouvrir audit-semgrep.md fusionné.
  6. La PR affiche Merge blocked — 2 failing checks.
  7. Corriger (remplacer node:latest par node:20.11-alpine, virer eval), pousser, tous les jobs repassent verts, PR mergeable.

Étape 2.6 — Bonus si temps

  • Ajouter Trivy dans le workflow : scan des vulnérabilités des images Docker (CVE sur les packages apt/apk). container: aquasec/trivy:latest.
  • Ajouter Hadolint pour les bonnes pratiques Dockerfile : container: hadolint/hadolint:latest-alpine.
  • Publier l'audit MD en commentaire de PR via actions/github-script.
  • Configurer un job docker-compose-lint qui fait docker compose config --quiet pour valider la syntaxe compose.

3. Rendu attendu

  1. URL du repo + docker-compose.yml avec le service gha-runner expliqué.
  2. Capture du runner Idle dans GitHub + docker compose ps.
  3. Le fichier .github/workflows/security.yml expliqué section par section (rôle des 3 jobs).
  4. Au moins 4 règles custom dans .semgrep/rules/ (dont 2 pour containers).
  5. Le script semgrep_to_md.py avec une explication de 10 lignes (et pourquoi fusionner les JSON).
  6. 2 captures : PR bloquée (2 checks rouges) + PR passée après correction.
  7. L'audit Markdown fusionné produit par la dernière exécution.

4. Grille d'évaluation

CritèrePoidsDétail
Runner dans docker-compose.yml15 %Service, secrets, labels, volumes corrects
Workflow YAML correct (jobs containerisés)20 %3 jobs, container:, permissions, --error
Règles custom (≥ 4 dont containers)15 %Syntaxe OK, cas métier Docker couvert
Script audit fusionné15 %Agrège code + containers, classement sévérité
Protection de branche (2 checks)10 %PR de test bloquée sur les 2 jobs Semgrep
Démo end-to-end15 %Bug → PR rouge → correction → PR verte
Qualité doc + zéro emoji code10 %Conventions IRIS, .gitignore des secrets

5. Pièges à éviter

  • PAT committé dans le repo — révocation immédiate et rotation. D'où le .gitignore dès le début, et Docker secrets plutôt qu'environment:.
  • Socket Docker monté sans contrôle/var/run/docker.sock donne au runner un accès root de facto sur l'hôte. Acceptable ici (repo privé, runner éphémère), inacceptable sur un runner public. Alternative durcie : Docker-in-Docker (DinD) ou rootless Docker.
  • EPHEMERAL: "false" — le runner persiste son environnement entre les jobs → un job malveillant peut polluer le suivant. Toujours true sur repo privé.
  • :latest sur l'image du runnermyoung34/github-runner:latest casse la reproductibilité. Pin sur un tag version (ex 2.321.0).
  • Runner partagé sur repo public — GitHub le refuse par défaut pour de bonnes raisons. Repos privés uniquement.
  • --error oublié → le job passe en vert même avec des findings critiques. Toujours tester avec une PR volontairement cassée.
  • Branche main sans protection → le job peut échouer, rien n'empêche le merge. Double protection : job + branch rule.

🎮 Quiz — Testez vos connaissances

3 QCM · 1 Vrai/Faux · 1 Question ouverte

QCM 1 — Flag Semgrep bloquant

Quel flag Semgrep fait sortir en code ≠ 0 si une finding de severity ERROR est trouvée ?

  1. --strict
  2. --error
  3. --ci
  4. --fail-fast
📋 Correction

B — --error. Le flag --error change le code de sortie du process. --strict traite les warnings comme des erreurs, --ci est un raccourci CI mais ne bloque pas seul. Source : semgrep.dev/docs/cli-reference.

QCM 2 — Label du runner

Dans le workflow, que représente la liste runs-on: [self-hosted, linux, iris, semgrep] ?

  1. Une configuration réseau
  2. Des labels que le runner doit tous posséder pour matcher
  3. Une liste de commandes à exécuter
  4. Un groupe d'utilisateurs autorisés
📋 Correction

B. GitHub Actions cherche un runner qui possède tous les labels listés. Le runner du TP a été enregistré avec --labels "self-hosted,linux,iris,semgrep", donc il matche. C'est un ET logique.

QCM 3 — Output SARIF

À quoi sert le fichier SARIF uploadé par le workflow ?

  1. À sauvegarder le code source
  2. À peupler l'onglet Security / Code scanning alerts de GitHub
  3. À configurer les permissions du runner
  4. À logger les actions Git
📋 Correction

B. SARIF (Static Analysis Results Interchange Format) est un standard OASIS qui permet aux outils d'analyse statique de parler une langue commune. GitHub consomme ce format pour afficher les findings dans l'onglet Security → Code scanning alerts.

Vrai / Faux

Un runner auto-hébergé peut être utilisé en toute sécurité sur des repos publics sans précaution particulière.

📋 Correction

FAUX. La documentation GitHub est explicite : "untrusted workflows running on your self-hosted runner pose significant security risks for your machine and network environment, especially if your machine persists its environment between jobs". Sur repos publics, n'importe qui peut ouvrir une PR avec un workflow malveillant. Par défaut, GitHub interdit les runners auto-hébergés sur les repos publics. Source : docs.github.com — Self-hosted runner security.

Question ouverte

En 5 lignes, expliquez pourquoi la double protection (workflow bloquant + branch protection) est nécessaire. Que se passe-t-il si on ne protège pas la branche main ?

📋 Éléments de réponse

Le flag --error de Semgrep fait seulement échouer le job CI. Sans branch protection, GitHub n'exige pas que ce job soit vert pour autoriser le merge — un admin peut fusionner malgré l'échec. La branch protection ajoute la règle "require status checks to pass before merging" qui refuse le merge tant que le check Semgrep scan n'est pas vert. Les deux mécanismes sont complémentaires : le job produit l'état, la règle de branche l'enforce. Sans protection : un dev pressé peut merger du code contenant une SQLi critique.

Sources officielles