Aller au contenu
CI/CD & Automatisation medium

Scanner vos pipelines CI/CD avec poutine

24 min de lecture

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.

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).

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.

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.

Les deux outils sont complémentaires. Voici leurs différences principales :

Critèrepoutinezizmor
PlateformesGitHub Actions, GitLab CI, Azure DevOps, TektonGitHub Actions uniquement
LangageGo + Rego (OPA)Rust
ScopeOrganisation entièreFichiers locaux ou dépôt unique
Règles customOui (fichiers Rego)Non
Auto-fixNonOui (--fix)
Base CVEOui (OSV)Non
Règles13 règles30+ règles
Cas d’usage idéalAudit organisationnel, multi-CIScan local quotidien, GitHub Actions

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é)

La méthode la plus simple sur Linux et macOS :

Fenêtre de terminal
brew install poutine

Vérification : Confirmez l’installation et la version :

Fenêtre de terminal
poutine version

Résultat attendu :

Version: 1.0.7
Commit: Homebrew
Built At: 2026-02-02T19:01:05Z

La commande la plus simple analyse un dépôt local sans aucun token :

Fenêtre de terminal
cd mon-projet
poutine analyze_local .

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 Input
Severity: warning
Description: The pipeline contains an injection into bash or JavaScript with
an 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émentSignification
RuleLe nom de la règle violée
Severityerror (critique), warning (important), note (informatif)
DescriptionCe que poutine a détecté et pourquoi c’est risqué
DocumentationLien vers la page de règle avec exemples et remédiation
RepositoryLe dépôt analysé
DetailsFichier, 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 │
└────────────────────────────────┴────────────────────────────────────────────┴──────────┴────────┘

Pour scanner un dépôt sans le cloner, fournissez un token GitHub avec un accès en lecture :

Fenêtre de terminal
export GH_TOKEN=$(gh auth token)
poutine analyze_repo mon-org/mon-repo --token "$GH_TOKEN"

La commande la plus puissante de poutine — elle scanne tous les dépôts d’une organisation GitHub en parallèle :

Fenêtre de terminal
poutine analyze_org mon-org --token "$GH_TOKEN"

Options utiles pour les grandes organisations :

Fenêtre de terminal
# Ignorer les forks
poutine 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 8

poutine supporte aussi GitLab (self-hosted ou gitlab.com) :

Fenêtre de terminal
export GL_TOKEN="votre-token-gitlab"
poutine analyze_org mon-groupe/mon-projet \
--token "$GL_TOKEN" \
--scm gitlab \
--scm-base-url https://gitlab.example.com

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.

.github/workflows/vulnerable.yml
on: pull_request_target
jobs:
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 build

Correction : 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.

Sévérité : warning — Les expressions GitHub Actions interpolées directement dans un bloc run: permettent l’injection de code.

.github/workflows/vulnerable.yml
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 :

.github/workflows/safe.yml
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 }}

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]'

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 code
on: pull_request_target
jobs:
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 Dependabot
if: >-
!github.event.pull_request.head.repo.fork &&
github.event.pull_request.user.login == 'dependabot[bot]'

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_target
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

Correction : Déclarez toujours les permissions minimales :

# ✅ Permissions explicites et minimales
on: pull_request_target
permissions: {}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4

Exé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.

Fenêtre de terminal
# ❌ Aucune vérification d'intégrité
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
bash <(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.0

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 production
env:
ACTIONS_RUNNER_DEBUG: true

Runner 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 forks
on: pull_request
jobs:
test:
runs-on: self-hosted # Un contributeur externe peut exécuter du code ici

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.

Chaque entrée skip peut filtrer par règle, chemin, niveau, job ou purl :

.poutine.yml
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-filter

Pour ignorer des règles ponctuellement sans modifier le fichier de configuration :

Fenêtre de terminal
# Ignorer une seule règle
poutine analyze_local . --skip debug_enabled
# Ignorer plusieurs règles
poutine analyze_local . --skip debug_enabled --skip unverified_script_exec

poutine supporte les règles Rego personnalisées. Ajoutez un répertoire de règles dans votre configuration :

.poutine.yml
include:
- path: ./custom_rules

Puis créez un fichier Rego dans ce répertoire :

custom_rules/no_latest_tag.rego
# 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.poutine
import 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")
}

poutine supporte trois formats de sortie :

FormatCommandeUsage
pretty-f prettyTableaux lisibles dans le terminal (défaut)
json-f jsonParsing automatisé par des scripts
sarif-f sarifUpload vers GitHub Advanced Security

Exemple de traitement JSON avec jq :

Fenêtre de terminal
# Lister les findings par règle avec le nombre d'occurrences
poutine analyze_local . -f json 2>/dev/null \
| jq '.findings | group_by(.rule_id) | map({rule: .[0].rule_id, count: length})'

L’intégration recommandée utilise le format SARIF pour remonter les findings dans l’onglet Security de GitHub :

.github/workflows/poutine.yml
name: Audit sécurité des pipelines
on:
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: poutine

Si vous n’avez pas besoin du SARIF :

.github/workflows/poutine-simple.yml
name: Sécurité pipelines
on:
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.

Pour un audit régulier de toute votre organisation :

.github/workflows/poutine-org-audit.yml
name: Audit sécurité organisation
on:
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-org
RègleSévéritéCe qu’elle détecte
untrusted_checkout_execerrorCheckout de code de fork + exécution (npm, make, pip…)
if_always_trueerrorCondition if toujours vraie à cause de la syntaxe YAML
confused_deputy_auto_mergeerrorAuto-merge basé sur github.actor usurpable
injectionwarningInjection via ${{ }} dans un bloc run:
job_all_secretswarningtoJSON(secrets) ou accès dynamique aux secrets
default_permissions_on_risky_eventswarningPas de permissions: sur pull_request_target
pr_runs_on_self_hostedwarningRunner self-hosted accessible aux forks
known_vulnerability_in_build_componentwarningAction tierce avec une CVE connue (base OSV)
known_vulnerability_in_build_platformwarningPlateforme CI avec une CVE connue
debug_enablednoteACTIONS_RUNNER_DEBUG ou ACTIONS_STEP_DEBUG activé
unpinnable_actionnoteAction dont les dépendances internes ne sont pas pinnées
unverified_script_execnote`curl
github_action_from_unverified_creator_usednoteAction d’un créateur non vérifié sur le Marketplace
SymptômeCause probableSolution
not a git repositoryDépôt non initialiséExécuter git init avant analyze_local
Aucun finding affichéPas de workflows CI/CDVérifier que .github/workflows/ contient des YAML
token requiredToken absent pour analyze_repo/orgExporter GH_TOKEN ou utiliser --token
Findings known_vulnerability_in_build_componentActions avec CVE connuesMettre à jour les actions vers des versions non vulnérables
rate limit exceededTrop de requêtes API GitHubRéduire --threads ou utiliser un token avec plus de quota
Faux positif sur une règleRègle trop stricte pour votre contexteAjouter une entrée skip dans .poutine.yml
invalid configurationSyntaxe du .poutine.yml incorrecteVérifier l’indentation YAML
  1. 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.

  2. analyze_org est 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é.

  3. untrusted_checkout_exec est la vulnérabilité la plus critique — un checkout de code de fork suivi d’un npm install ou make permet l’exécution de code arbitraire avec les secrets du dépôt cible.

  4. if_always_true est un piège subtil — l’utilisation de ${{ }} dans une condition if multiligne rend la condition toujours vraie, même si l’expression elle-même est fausse.

  5. Le format SARIF permet d’afficher les findings dans l’onglet Security de GitHub pour un suivi dans le temps.

  6. .poutine.yml permet 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.

  7. 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.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn