poutine scanne vos pipelines CI/CD (GitHub Actions, GitLab CI, Azure DevOps,
Tekton) et détecte 13 types de vulnérabilités : exécution de code non fiable,
injections, secrets surexposés, conditions if toujours vraies, auto-merge
trompeur. Ce guide vous montre comment l’installer, scanner un dépôt local ou
une organisation entière, comprendre les résultats et intégrer poutine dans
votre CI. Prérequis : Homebrew (ou Docker) et un dépôt contenant des workflows
CI/CD.
Qu’est-ce que poutine ?
Section intitulée « Qu’est-ce que poutine ? »poutine est un scanner de sécurité créé par BoostSecurity.io qui détecte les mauvaises configurations et vulnérabilités dans les pipelines de build d’un dépôt. Il parse les fichiers de workflows CI/CD et applique des règles de sécurité écrites en Rego (le langage de politique d’Open Policy Agent).
Une analogie pour comprendre
Section intitulée « Une analogie pour comprendre »Imaginez un contrôleur qualité dans une usine qui inspecte la chaîne de fabrication elle-même, pas les produits finis. Il vérifie que les machines ne sont pas mal configurées, que les accès aux matières premières dangereuses sont contrôlés et que personne n’a laissé une porte ouverte sur la chaîne. poutine fait la même chose avec vos pipelines CI/CD : il audite la chaîne de production logicielle, pas le code applicatif.
Ce qui distingue poutine
Section intitulée « Ce qui distingue poutine »Multi-plateforme
Analyse GitHub Actions, GitLab CI, Azure DevOps et Tekton Pipelines as Code. Un seul outil pour toutes vos plateformes CI/CD.
Scan d'organisation
Peut scanner tous les dépôts d’une organisation en une seule commande
avec analyze_org pour dresser un état des lieux global.
Règles OPA/Rego
Les règles de détection sont écrites en Rego (Open Policy Agent), un langage déclaratif de politique. Vous pouvez écrire vos propres règles.
Base CVE intégrée
Détecte les actions et plateformes avec des vulnérabilités connues (CVE) via la base OSV, en plus des mauvaises configurations.
poutine vs zizmor
Section intitulée « poutine vs zizmor »Les deux outils sont complémentaires. Voici leurs différences principales :
| Critère | poutine | zizmor |
|---|---|---|
| Plateformes | GitHub Actions, GitLab CI, Azure DevOps, Tekton | GitHub Actions uniquement |
| Langage | Go + Rego (OPA) | Rust |
| Scope | Organisation entière | Fichiers locaux ou dépôt unique |
| Règles custom | Oui (fichiers Rego) | Non |
| Auto-fix | Non | Oui (--fix) |
| Base CVE | Oui (OSV) | Non |
| Règles | 13 règles | 30+ règles |
| Cas d’usage idéal | Audit organisationnel, multi-CI | Scan local quotidien, GitHub Actions |
Prérequis
Section intitulée « Prérequis »Avant de commencer, assurez-vous d’avoir :
- Homebrew (pour l’installation la plus simple) ou Docker
- Un dépôt Git contenant des workflows CI/CD
- Un token GitHub (pour scanner des dépôts distants ou des organisations)
- Un terminal sous Linux, macOS ou Windows (WSL recommandé)
Installer poutine
Section intitulée « Installer poutine »La méthode la plus simple sur Linux et macOS :
brew install poutineSans rien installer sur votre machine :
docker run -e GH_TOKEN ghcr.io/boostsecurityio/poutine:latestPour scanner un dépôt local avec Docker, montez le volume :
docker run --rm -v $(pwd):/workspace \ ghcr.io/boostsecurityio/poutine:latest \ analyze_local /workspaceTéléchargez le binaire depuis la
page des releases et
ajoutez-le à votre $PATH :
# Exemple pour Linux amd64curl -Lo poutine https://github.com/boostsecurityio/poutine/releases/download/v1.0.7/poutine_1.0.7_linux_amd64chmod +x poutinesudo mv poutine /usr/local/bin/Vérification : Confirmez l’installation et la version :
poutine versionRésultat attendu :
Version: 1.0.7Commit: HomebrewBuilt At: 2026-02-02T19:01:05ZVotre premier scan local
Section intitulée « Votre premier scan local »La commande la plus simple analyse un dépôt local sans aucun token :
cd mon-projetpoutine analyze_local .Comprendre la sortie
Section intitulée « Comprendre la sortie »poutine affiche les résultats sous forme de tableaux regroupés par règle. Voici un exemple réel :
Rule: Injection with Arbitrary External Contributor InputSeverity: warningDescription: The pipeline contains an injection into bash or JavaScript withan expression that can contain user input.Documentation: https://boostsecurityio.github.io/poutine/rules/injection
┌─────────────────┬──────────────────────────────────────────────────┬──────────────────────────────────────┐│ REPOSITORY │ DETAILS │ URL │├─────────────────┼──────────────────────────────────────────────────┼──────────────────────────────────────┤│ localrepo/local │ .github/workflows/ci.yml │ /tree/HEAD/.github/workflows/ci.yml ││ │ Job: respond │ ││ │ Step: 0 │ ││ │ Sources: github.event.comment.body │ │└─────────────────┴──────────────────────────────────────────────────┴──────────────────────────────────────┘Chaque finding contient :
| Élément | Signification |
|---|---|
| Rule | Le nom de la règle violée |
| Severity | error (critique), warning (important), note (informatif) |
| Description | Ce que poutine a détecté et pourquoi c’est risqué |
| Documentation | Lien vers la page de règle avec exemples et remédiation |
| Repository | Le dépôt analysé |
| Details | Fichier, job, step et sources concernés |
En fin de scan, un tableau récapitulatif liste toutes les règles avec leur statut (Passed/Failed) :
Summary of findings:┌────────────────────────────────┬────────────────────────────────────────────┬──────────┬────────┐│ RULE ID │ RULE NAME │ FAILURES │ STATUS │├────────────────────────────────┼────────────────────────────────────────────┼──────────┼────────┤│ injection │ Injection with External Contributor Input │ 1 │ Failed ││ untrusted_checkout_exec │ Arbitrary Code Execution from Untrusted │ 2 │ Failed ││ default_permissions_on_risky… │ Default permissions used on risky events │ 3 │ Failed ││ known_vulnerability_in_build… │ Build Component with Known Vulnerability │ 0 │ Passed │└────────────────────────────────┴────────────────────────────────────────────┴──────────┴────────┘Scanner un dépôt distant ou une organisation
Section intitulée « Scanner un dépôt distant ou une organisation »Scanner un dépôt GitHub distant
Section intitulée « Scanner un dépôt GitHub distant »Pour scanner un dépôt sans le cloner, fournissez un token GitHub avec un accès en lecture :
export GH_TOKEN=$(gh auth token)poutine analyze_repo mon-org/mon-repo --token "$GH_TOKEN"Scanner une organisation entière
Section intitulée « Scanner une organisation entière »La commande la plus puissante de poutine — elle scanne tous les dépôts d’une organisation GitHub en parallèle :
poutine analyze_org mon-org --token "$GH_TOKEN"Options utiles pour les grandes organisations :
# Ignorer les forkspoutine analyze_org mon-org --token "$GH_TOKEN" --ignore-forks
# Paralléliser sur 8 threads (défaut : 2)poutine analyze_org mon-org --token "$GH_TOKEN" --threads 8Scanner une instance GitLab
Section intitulée « Scanner une instance GitLab »poutine supporte aussi GitLab (self-hosted ou gitlab.com) :
export GL_TOKEN="votre-token-gitlab"poutine analyze_org mon-groupe/mon-projet \ --token "$GL_TOKEN" \ --scm gitlab \ --scm-base-url https://gitlab.example.comLes principales vulnérabilités détectées
Section intitulée « Les principales vulnérabilités détectées »poutine embarque 13 règles couvrant les vulnérabilités les plus critiques des pipelines CI/CD. Voici les plus importantes, classées par sévérité.
Exécution de code non fiable (untrusted_checkout_exec)
Section intitulée « Exécution de code non fiable (untrusted_checkout_exec) »Sévérité : error — La vulnérabilité la plus dangereuse détectée par poutine.
Le workflow fait un checkout de code venant d’un fork (via
pull_request_target) puis exécute un outil qui consomme des fichiers du
disque : npm install, make, pip install, gradle build, etc. Ces outils
sont appelés LOTP (Living Off The Pipeline) — ils lisent des fichiers de
configuration (package.json, Makefile, setup.py) qui peuvent contenir du
code malveillant contrôlé par l’attaquant.
on: pull_request_targetjobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: repository: ${{ github.event.pull_request.head.repo.full_name }} ref: ${{ github.event.pull_request.head.sha }} # ❌ DANGEREUX : exécute du code du fork avec les secrets du dépôt cible - run: npm install && npm test - run: make buildCorrection : Ne jamais exécuter de commandes de build sur du code non
fiable dans un contexte pull_request_target. Utilisez plutôt le trigger
pull_request standard (qui ne donne pas accès aux secrets) ou séparez le
workflow en deux jobs avec un label-based gate.
Injection via input utilisateur (injection)
Section intitulée « Injection via input utilisateur (injection) »Sévérité : warning — Les expressions GitHub Actions interpolées directement
dans un bloc run: permettent l’injection de code.
steps: - name: Process comment run: | # ❌ DANGEREUX : l'attaquant contrôle le contenu du commentaire echo "Body: ${{ github.event.comment.body }}" echo "Issue: ${{ github.event.issue.title }}"Correction : Passez les valeurs via des variables d’environnement :
steps: - name: Process comment run: | # ✅ SÉCURISÉ : les valeurs passent par des variables d'environnement echo "Body: ${COMMENT_BODY}" echo "Issue: ${ISSUE_TITLE}" env: COMMENT_BODY: ${{ github.event.comment.body }} ISSUE_TITLE: ${{ github.event.issue.title }}Condition if toujours vraie (if_always_true)
Section intitulée « Condition if toujours vraie (if_always_true) »Sévérité : error — Un piège subtil de la syntaxe GitHub Actions.
Quand vous utilisez ${{ }} dans une condition if multiligne avec |,
GitHub Actions évalue l’expression en string puis vérifie si elle est truthy.
Le problème : les espaces et retours à la ligne autour de l’expression la
rendent toujours vraie.
# ❌ DANGEREUX : cette condition est TOUJOURS vraie !if: | ${{ github.actor == 'dependabot[bot]' || github.actor == 'renovate[bot]' }}L’expression est évaluée comme une string contenant des espaces ("\n true\n"),
et toute string non vide est truthy en GitHub Actions.
# ✅ CORRECT : une seule ligne, sans ${{ }}if: github.actor == 'dependabot[bot]' || github.actor == 'renovate[bot]'Auto-merge trompeur (confused_deputy_auto_merge)
Section intitulée « Auto-merge trompeur (confused_deputy_auto_merge) »Sévérité : error — L’attaque du “confused deputy” (mandataire confus).
Le workflow auto-merge une PR en vérifiant uniquement que github.actor est
dependabot[bot]. Un attaquant peut déclencher une action Dependabot sur une
PR de fork contenant du code malveillant, et le workflow la merge
automatiquement.
# ❌ Vérifie uniquement l'actor, pas l'origine du codeon: pull_request_targetjobs: automerge: if: ${{ github.actor == 'dependabot[bot]' }} steps: - run: gh pr merge --auto --squash "$PR_URL"Correction : Vérifiez que la PR ne vient pas d’un fork :
# ✅ Vérifie que la PR n'est pas d'un fork ET que l'auteur est Dependabotif: >- !github.event.pull_request.head.repo.fork && github.event.pull_request.user.login == 'dependabot[bot]'Secrets surexposés (job_all_secrets)
Section intitulée « Secrets surexposés (job_all_secrets) »Sévérité : warning — Injecter la totalité des secrets dans un job expose des informations sensibles inutilement.
env: # ❌ Expose TOUS les secrets du dépôt ALL_SECRETS: ${{ toJSON(secrets) }}Correction : Exposez uniquement les secrets nécessaires au job :
env: # ✅ Un seul secret, nécessaire pour cette tâche DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}Permissions par défaut sur événements risqués (default_permissions_on_risky_events)
Section intitulée « Permissions par défaut sur événements risqués (default_permissions_on_risky_events) »Sévérité : warning — Sans bloc permissions: explicite sur un workflow
déclenché par pull_request_target ou issue_comment, le workflow hérite des
permissions par défaut. Sur les anciennes organisations, ces permissions sont
souvent read-write sur tout.
# ❌ Pas de permissions définies + trigger risquéon: pull_request_targetjobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4Correction : Déclarez toujours les permissions minimales :
# ✅ Permissions explicites et minimaleson: pull_request_targetpermissions: {}jobs: build: runs-on: ubuntu-latest permissions: contents: read steps: - uses: actions/checkout@v4Exécution de scripts non vérifiés (unverified_script_exec)
Section intitulée « Exécution de scripts non vérifiés (unverified_script_exec) »Sévérité : note — Le pattern curl | bash télécharge et exécute un script
distant sans vérifier son intégrité. En CI, ce pattern est exécuté à
chaque build — la probabilité de télécharger un script compromis augmente
avec le temps.
# ❌ Aucune vérification d'intégritécurl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bashbash <(curl -s https://codecov.io/bash)Correction : Utilisez une action épinglée par SHA ou vérifiez la somme de contrôle du script :
steps: # ✅ Utiliser une action GitHub officielle et épinglée - uses: azure/setup-helm@fe7b79cd5ee1e45176fcad797de68ecaf3ca4814 # v4.2.0Debugging activé (debug_enabled)
Section intitulée « Debugging activé (debug_enabled) »Sévérité : note — Activer ACTIONS_RUNNER_DEBUG ou ACTIONS_STEP_DEBUG
augmente la verbosité des logs et peut exposer des logs de debug étendus
contenant des informations sensibles.
# ❌ Debug activé en productionenv: ACTIONS_RUNNER_DEBUG: trueRunner self-hosted sur les PR (pr_runs_on_self_hosted)
Section intitulée « Runner self-hosted sur les PR (pr_runs_on_self_hosted) »Sévérité : warning — Un job utilisant un runner self-hosted sur un
événement pull_request permet à des contributeurs externes d’exécuter du
code sur votre infrastructure. Même sans accès aux secrets, un attaquant
peut obtenir sudo sur la plupart des runners et exfiltrer des données.
# ❌ Runner self-hosted accessible aux forkson: pull_requestjobs: test: runs-on: self-hosted # Un contributeur externe peut exécuter du code iciConfigurer poutine avec .poutine.yml
Section intitulée « Configurer poutine avec .poutine.yml »Pour un contrôle reproductible, créez un fichier .poutine.yml à la racine de votre dépôt. Ce fichier permet d’ignorer des findings et d’inclure des règles personnalisées.
Ignorer des findings (skip)
Section intitulée « Ignorer des findings (skip) »Chaque entrée skip peut filtrer par règle, chemin, niveau, job
ou purl :
skip: # Ignorer tous les findings de niveau note - level: note
# Ignorer une règle spécifique pour certains workflows - rule: unverified_script_exec path: - .github/workflows/setup.yml - .github/workflows/install.yml
# Ignorer une règle globalement - rule: unpinnable_action
# Ignorer une action spécifique (par purl) - rule: github_action_from_unverified_creator_used purl: - pkg:githubactions/dorny/paths-filterIgnorer via la ligne de commande
Section intitulée « Ignorer via la ligne de commande »Pour ignorer des règles ponctuellement sans modifier le fichier de configuration :
# Ignorer une seule règlepoutine analyze_local . --skip debug_enabled
# Ignorer plusieurs règlespoutine analyze_local . --skip debug_enabled --skip unverified_script_execInclure des règles personnalisées
Section intitulée « Inclure des règles personnalisées »poutine supporte les règles Rego personnalisées. Ajoutez un répertoire de règles dans votre configuration :
include: - path: ./custom_rulesPuis créez un fichier Rego dans ce répertoire :
# METADATA# title: Docker image using latest tag# description: Detects usage of :latest tag in container images# custom:# level: warning
package rules.no_latest_tag
import data.poutineimport rego.v1
rule := poutine.rule(rego.metadata.chain())
results contains poutine.finding(rule, pkg.purl, { "path": workflow.path, "job": job.id, "details": "Container uses :latest tag",}) if { pkg := input.packages[_] workflow := pkg.github_actions_workflows[_] job := workflow.jobs[_] job.container.image endswith(job.container.image, ":latest")}Formats de sortie
Section intitulée « Formats de sortie »poutine supporte trois formats de sortie :
| Format | Commande | Usage |
|---|---|---|
pretty | -f pretty | Tableaux lisibles dans le terminal (défaut) |
json | -f json | Parsing automatisé par des scripts |
sarif | -f sarif | Upload vers GitHub Advanced Security |
Exemple de traitement JSON avec jq :
# Lister les findings par règle avec le nombre d'occurrencespoutine analyze_local . -f json 2>/dev/null \ | jq '.findings | group_by(.rule_id) | map({rule: .[0].rule_id, count: length})'Intégrer poutine dans votre CI
Section intitulée « Intégrer poutine dans votre CI »GitHub Actions avec upload SARIF
Section intitulée « GitHub Actions avec upload SARIF »L’intégration recommandée utilise le format SARIF pour remonter les findings dans l’onglet Security de GitHub :
name: Audit sécurité des pipelineson: push: branches: [main] paths: - '.github/workflows/**' pull_request: paths: - '.github/workflows/**'
permissions: {}
jobs: poutine: name: Scan poutine runs-on: ubuntu-latest permissions: security-events: write contents: read steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false
- name: Scan poutine uses: boostsecurityio/poutine-action@main # L'action génère automatiquement results.sarif
- name: Uploader les résultats SARIF uses: github/codeql-action/upload-sarif@ea9e4e37992a54ee68a9571ad585dd722115571f # v3.28.14 with: sarif_file: results.sarif category: poutineScan simple avec échec du pipeline
Section intitulée « Scan simple avec échec du pipeline »Si vous n’avez pas besoin du SARIF :
name: Sécurité pipelineson: pull_request: paths: - '.github/workflows/**'
permissions: {}
jobs: poutine: runs-on: ubuntu-latest permissions: contents: read steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false
- name: Installer poutine run: | curl -Lo poutine https://github.com/boostsecurityio/poutine/releases/download/v1.0.7/poutine_1.0.7_linux_amd64 chmod +x poutine sudo mv poutine /usr/local/bin/
- name: Audit des workflows run: poutine analyze_local .Le pipeline échouera si des findings de sévérité error ou warning sont
détectés.
Audit d’organisation planifié
Section intitulée « Audit d’organisation planifié »Pour un audit régulier de toute votre organisation :
name: Audit sécurité organisationon: schedule: - cron: '0 6 * * 1' # Chaque lundi à 6h
permissions: {}
jobs: audit: runs-on: ubuntu-latest permissions: security-events: write steps: - name: Installer poutine run: | curl -Lo poutine https://github.com/boostsecurityio/poutine/releases/download/v1.0.7/poutine_1.0.7_linux_amd64 chmod +x poutine sudo mv poutine /usr/local/bin/
- name: Scanner l'organisation run: | poutine analyze_org ${{ github.repository_owner }} \ --token "$GH_TOKEN" \ --ignore-forks \ --threads 4 \ -f sarif > results.sarif env: GH_TOKEN: ${{ secrets.ORG_READ_TOKEN }}
- name: Uploader les résultats uses: github/codeql-action/upload-sarif@ea9e4e37992a54ee68a9571ad585dd722115571f # v3.28.14 with: sarif_file: results.sarif category: poutine-orgTableau récapitulatif des règles
Section intitulée « Tableau récapitulatif des règles »| Règle | Sévérité | Ce qu’elle détecte |
|---|---|---|
untrusted_checkout_exec | error | Checkout de code de fork + exécution (npm, make, pip…) |
if_always_true | error | Condition if toujours vraie à cause de la syntaxe YAML |
confused_deputy_auto_merge | error | Auto-merge basé sur github.actor usurpable |
injection | warning | Injection via ${{ }} dans un bloc run: |
job_all_secrets | warning | toJSON(secrets) ou accès dynamique aux secrets |
default_permissions_on_risky_events | warning | Pas de permissions: sur pull_request_target |
pr_runs_on_self_hosted | warning | Runner self-hosted accessible aux forks |
known_vulnerability_in_build_component | warning | Action tierce avec une CVE connue (base OSV) |
known_vulnerability_in_build_platform | warning | Plateforme CI avec une CVE connue |
debug_enabled | note | ACTIONS_RUNNER_DEBUG ou ACTIONS_STEP_DEBUG activé |
unpinnable_action | note | Action dont les dépendances internes ne sont pas pinnées |
unverified_script_exec | note | `curl |
github_action_from_unverified_creator_used | note | Action d’un créateur non vérifié sur le Marketplace |
Dépannage
Section intitulée « Dépannage »| Symptôme | Cause probable | Solution |
|---|---|---|
not a git repository | Dépôt non initialisé | Exécuter git init avant analyze_local |
| Aucun finding affiché | Pas de workflows CI/CD | Vérifier que .github/workflows/ contient des YAML |
token required | Token absent pour analyze_repo/org | Exporter GH_TOKEN ou utiliser --token |
Findings known_vulnerability_in_build_component | Actions avec CVE connues | Mettre à jour les actions vers des versions non vulnérables |
rate limit exceeded | Trop de requêtes API GitHub | Réduire --threads ou utiliser un token avec plus de quota |
| Faux positif sur une règle | Règle trop stricte pour votre contexte | Ajouter une entrée skip dans .poutine.yml |
invalid configuration | Syntaxe du .poutine.yml incorrecte | Vérifier l’indentation YAML |
À retenir
Section intitulée « À retenir »-
poutine scanne les pipelines CI/CD (GitHub Actions, GitLab CI, Azure DevOps, Tekton) pour détecter 13 types de vulnérabilités de supply chain.
-
analyze_orgest la commande la plus puissante — elle audite une organisation entière en une seule commande pour dresser un état des lieux global de la sécurité. -
untrusted_checkout_execest la vulnérabilité la plus critique — un checkout de code de fork suivi d’unnpm installoumakepermet l’exécution de code arbitraire avec les secrets du dépôt cible. -
if_always_trueest un piège subtil — l’utilisation de${{ }}dans une conditionifmultiligne rend la condition toujours vraie, même si l’expression elle-même est fausse. -
Le format SARIF permet d’afficher les findings dans l’onglet Security de GitHub pour un suivi dans le temps.
-
.poutine.ymlpermet d’ignorer les faux positifs de manière granulaire (par règle, chemin, niveau, job ou purl) et d’ajouter des règles Rego personnalisées. -
Combinez poutine et zizmor : poutine pour l’audit d’organisation et le multi-CI, zizmor pour le scan local fin et rapide avec auto-fix.