🧪 TP SLAM — Audit de code automatisé · 2 séances · 3h
| Public | BTS SIO 2e année, option SLAM |
|---|---|
| Durée | Séance 1 (1h) + Séance 2 (2h) = 3h |
| Lien | Fait suite au cours théorique 2h — Sécurité proactive |
| Compétences | B3.3 Assurer la cybersécurité d'une solution applicative |
| Prérequis | Git + 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
mainne peut contenir de finding OWASP de sévérité ERROR. - Les images Docker et le
docker-compose.ymldoivent ê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.ymlavec 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
mainpour forcer le passage du check.
2. Prérequis matériels
| Élément | Détail |
|---|---|
| Compte GitHub | Un par binôme, un repo privé par binôme |
| Accès serveur IRIS | SSH avec clé, utilisateur runner-<binome> membre du groupe docker |
| Docker engine | Version ≥ 24.0 avec plugin docker compose v2 |
| Stack outils | git, curl, jq — pas besoin de pipx ni de binaires natifs |
| Réseau | HTTPS 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)
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 settings → Personal access tokens → Fine-grained tokens → Generate 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 permissions → Administration: 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.0 | Version pinée — pas :latest. Changer explicitement à chaque upgrade. |
restart: unless-stopped | Le runner redémarre après un reboot serveur, mais pas si vous l'arrêtez explicitement. |
REPO_URL | Scope repo. Pour un scope org, on mettrait l'URL de l'organisation et RUNNER_SCOPE: org. |
ACCESS_TOKEN_FILE | Docker secret monté en /run/secrets/. Le runner lit le PAT depuis ce fichier, pas depuis l'env. |
LABELS | Tags 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.sock | Permet 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.file | Format "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-hostedlinuxirissemgrepdocker
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. Ajoutergroup_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.
- Le
docker-compose.ymlcomplété avec le servicegha-runner, chaque clé annotée (décryptage ligne par ligne). - Capture : le runner en état Idle dans GitHub, avec les labels visibles.
- Capture : sortie
docker compose psmontrantgha-runnerUp. - Capture : 6 findings Semgrep (applicatif + Docker) depuis votre poste.
- Preuve que
.gitignoreexclut biensecrets/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.
- Le
docker-compose.ymlcomplété avec le servicegha-runnercommenté section par section. - Capture : le runner en état Idle dans GitHub, avec les labels.
- Capture : sortie
docker compose psmontrantgha-runnerUp. - 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.--errorfait 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 applicatifetSemgrep — 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)
- Créer une branche
feature/demo-buget y ajoutereval(userInput)dans le front +FROM node:latestdans un Dockerfile. - Pousser et ouvrir une PR vers
main. - Observer : les 3 jobs démarrent sur votre
gha-runner.semgrep-codeetsemgrep-composeéchouent. - Vérifier l'onglet Security → Code scanning alerts : les findings sont groupés par
category. - Télécharger l'artefact
audit-semgrep-<run_id>→ ouvriraudit-semgrep.mdfusionné. - La PR affiche
Merge blocked — 2 failing checks. - Corriger (remplacer
node:latestparnode:20.11-alpine, virereval), 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-lintqui faitdocker compose config --quietpour valider la syntaxe compose.
3. Rendu attendu
- URL du repo +
docker-compose.ymlavec le servicegha-runnerexpliqué. - Capture du runner Idle dans GitHub +
docker compose ps. - Le fichier
.github/workflows/security.ymlexpliqué section par section (rôle des 3 jobs). - Au moins 4 règles custom dans
.semgrep/rules/(dont 2 pour containers). - Le script
semgrep_to_md.pyavec une explication de 10 lignes (et pourquoi fusionner les JSON). - 2 captures : PR bloquée (2 checks rouges) + PR passée après correction.
- L'audit Markdown fusionné produit par la dernière exécution.
4. Grille d'évaluation
| Critère | Poids | Détail |
|---|---|---|
Runner dans docker-compose.yml | 15 % | 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-end | 15 % | Bug → PR rouge → correction → PR verte |
| Qualité doc + zéro emoji code | 10 % | Conventions IRIS, .gitignore des secrets |
5. Pièges à éviter
- PAT committé dans le repo — révocation immédiate et rotation. D'où le
.gitignoredès le début, et Docker secrets plutôt qu'environment:. - Socket Docker monté sans contrôle —
/var/run/docker.sockdonne 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. Toujourstruesur repo privé.:latestsur l'image du runner —myoung34/github-runner:latestcasse la reproductibilité. Pin sur un tag version (ex2.321.0).- Runner partagé sur repo public — GitHub le refuse par défaut pour de bonnes raisons. Repos privés uniquement.
--erroroublié → 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 ?
--strict--error--ci--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] ?
- Une configuration réseau
- Des labels que le runner doit tous posséder pour matcher
- Une liste de commandes à exécuter
- 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 ?
- À sauvegarder le code source
- À peupler l'onglet Security / Code scanning alerts de GitHub
- À configurer les permissions du runner
- À 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
- Semgrep docs — semgrep.dev/docs
- Semgrep — Sample CI configs — semgrep.dev/docs/semgrep-ci/sample-ci-configs
- Semgrep — Configurer blocking — semgrep.dev/docs/semgrep-ci/configuring-blocking-and-errors-in-ci
- GitHub — Self-hosted runners — docs.github.com/en/actions/hosting-your-own-runners
- GitHub — Runner as a service — docs.github.com — Configure as service
- GitHub — SARIF support — docs.github.com — SARIF
- OWASP Top 10:2025 — owasp.org/Top10/2025
