Si votre pipeline GitLab CI/CD utilise des images Docker non épinglées, des includes sur main ou un build en Docker-in-Docker, ou si vos workflows GitHub Actions référencent des actions tierces par tag mutable, vous exposez votre chaîne à une attaque de supply chain. Plumber est un scanner de sécurité de pipelines : il lit votre configuration CI, la confronte à une série de contrôles, puis rend un score de 0 à 100 (lettre A à E) et des chemins d'attaque à corriger. Ce guide, pour débutants et intermédiaires, part de la CLI, passe au composant GitLab et à l'action GitHub, puis traite le PBOM, les rapports, le score et Plumber Radar.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Installer Plumber en vérifiant son empreinte, puis lancer un premier scan avec un jeton GitLab ou GitHub
- Lire un chemin d'attaque et le bloc Plumber Score d'un scan réel, puis comprendre comment le score se calcule et détailler un code avec
plumber explain - Corriger les failles fréquentes : image non épinglée, action non épinglée par SHA, source non autorisée, branche non protégée
- Configurer les contrôles avec une surcouche (
extends: plumber:default) et la valider - Intégrer Plumber à la CI avec le composant GitLab ou l'action GitHub, tous deux épinglés
- Régler le seuil du job (
--min-points,--min-score) et interpréter les codes de sortie 0, 1, 2 et 3 - Exporter un PBOM, un CycloneDX, un rapport SARIF ou GitLab SAST
- Vérifier l'hygiène CI/CD d'un fournisseur avec Plumber Radar
Qu'est-ce que Plumber ?
Section intitulée « Qu'est-ce que Plumber ? »Plumber est un scanner de sécurité pour GitLab CI/CD et GitHub Actions. Côté GitLab, il interroge l'API du projet et lit la configuration mergée ; côté GitHub, il analyse les fichiers de .github/workflows avec le moteur de règles Rego, puis complète ses constats par l'API GitHub : protection de branche, avis de sécurité des actions, dépôts archivés, états des workflows. Il signale les écarts les plus dangereux : images non fiables, tags mutables, actions non épinglées par SHA, sources non autorisées, branches non protégées, scripts téléchargés sans vérification, Docker-in-Docker.
Quelques termes reviennent tout au long du guide. Une attaque de supply chain passe par une dépendance que vous ne maîtrisez pas (une action, une image, un template) plutôt que par votre propre code. Épingler une dépendance, c'est la figer sur un identifiant qui ne change jamais : le SHA du commit pour une action, le digest (sha256:…) pour une image de conteneur. Un tag comme v4 ou 3.12 reste mutable, car son propriétaire peut le déplacer vers un autre contenu. Le provider désigne la plateforme analysée, GitLab ou GitHub. Le Docker-in-Docker (DinD) est un job qui démarre son propre démon Docker dans un conteneur privilégié.
Chaque problème porte un code d'issue (ISSUE-XXX) que vous détaillez avec plumber explain. Le détail de ce qui est regroupé en chemin d'attaque et de ce qui reste un finding individuel se lit plus bas, dans la section sur le score.
Plumber est un logiciel open source, distribué sous licence MPL 2.0 (fichier LICENSE du dépôt getplumber/plumber). La documentation officielle (getplumber.io/docs/cli) complète ce guide pour les options rares.
Plumber n'est pas seul : zizmor et poutine analysent en profondeur les workflows GitHub, alors que Plumber couvre deux plateformes sous une même configuration et rend un score unique. Le lab Pipeline CI durci les fait passer ensemble à zéro finding.
Par où commencer ?
Section intitulée « Par où commencer ? »Plumber s'utilise de quatre façons, et le choix change surtout l'ordre d'apprentissage. La CLI montre la sortie brute et permet d'itérer en quelques secondes. Le composant GitLab et l'action GitHub évitent d'écrire un job complet, mais masquent une partie du fonctionnement. La Platform, le service de pilotage multi-projets de Plumber (présenté dans la section « Quand passer à la Platform ? »), répond à un besoin de gouvernance, pas de découverte. Repérez votre situation dans la colonne de gauche.
| Votre situation | Porte d'entrée recommandée | Pourquoi |
|---|---|---|
| Vous découvrez Plumber | CLI locale | Vous comprenez ce que l'outil détecte et comment lire la sortie |
| Vous avez un pipeline GitLab | Composant GitLab CI/CD | Un include suffit, les exports sont déjà branchés |
| Vous avez des workflows GitHub | Action GitHub | Un step, avec un rapport SARIF (format d'échange standard) dans l'onglet Security |
| Vous devez centraliser plusieurs projets | Platform | Vous passez de l'analyse projet par projet à la gouvernance |
Prérequis
Section intitulée « Prérequis »Avant de lancer votre premier scan, vous avez besoin de :
- un dépôt Git lié à un projet GitLab ou GitHub ;
- côté GitLab : un jeton avec les scopes
read_apietread_repository, avec le rôle Maintainer sur le projet (sinon la protection de branche reste inaccessible) ; - côté GitHub : un jeton exporté dans
GH_TOKENouGITHUB_TOKEN(Plumber litGH_TOKENen premier), ou une sessiongh auth login, avec en plus Administration:read pour lire la protection de branche et Actions:read pour lire l'état des workflows (le paragraphe « Quel jeton GitHub créer ? » plus bas détaille le type de jeton) ; - un terminal interactif si vous voulez utiliser
plumber config init.
Comment installer Plumber ?
Section intitulée « Comment installer Plumber ? »Plumber est distribué comme binaire unique, sans dépendance. Les trois premières méthodes posent l'exécutable sur votre poste ; la variante Docker évite toute installation et convient aux runners de CI. Quel que soit le canal, épinglez la version : un résultat d'analyse ne se compare que d'une exécution à l'autre sur la même version.
mise use -g github:getplumber/plumber@0.7.2brew tap getplumber/plumberbrew trust --formula getplumber/plumber/plumberbrew install plumberCes trois lignes sont celles du README de Plumber, brew trust compris. Cette méthode n'a pas été rejouée pour ce guide. La formule suit la dernière version publiée : elle ne fixe pas la 0.7.2. Pour une version figée, préférez l'onglet mise, le binaire ou l'image Docker.
B=https://github.com/getplumber/plumber/releases/download/v0.7.2curl -fsSL -O "$B/plumber-linux-amd64"curl -fsSL -O "$B/checksums.txt"grep ' plumber-linux-amd64$' checksums.txt | sha256sum -c -install -m 0755 plumber-linux-amd64 ~/.local/bin/plumberLa commande install copie le binaire dans ~/.local/bin avec les droits d'exécution ; vérifiez que ce dossier figure dans votre PATH (echo "$PATH"), sinon ajoutez-le ou choisissez un autre dossier du PATH. La commande sha256sum -c doit répondre plumber-linux-amd64: OK. Les releases publient aussi les binaires (linux-arm64, darwin-amd64, darwin-arm64 et windows-amd64.exe), plus une attestation de provenance que gh attestation verify <fichier> --repo getplumber/plumber contrôle.
docker pull getplumber/plumber:0.7.2@sha256:14698bde5dbc40fe2b5b9d339b039084b18f3b195e2fba0ceef9ff05ec69654cdocker run --rm -e GH_TOKEN -v "$PWD:/work" -w /work getplumber/plumber:0.7.2@sha256:14698bde5dbc40fe2b5b9d339b039084b18f3b195e2fba0ceef9ff05ec69654c analyzeLa seconde commande lance l'analyse du dépôt courant, monté dans /work, et transmet le jeton GH_TOKEN du poste sans l'écrire en clair. L'image a pour ENTRYPOINT /plumber et tourne avec l'utilisateur plumber. Le digest est celui de l'index multi-architecture (amd64 et arm64).
Vérification (mise, Homebrew ou binaire ; avec Docker, lancez docker run --rm <image@digest> version) :
plumber versionLa sortie doit afficher trois lignes : plumber version 0.7.2, le hash de commit (620b4de274459229ad4f35041ca8bb48d8241d13) et la date de build (2026-10-17T04:18:53Z). L'option --version n'existe pas : c'est la sous-commande version.
Plumber vérifie aussi, à la fin d'une analyse, si une version plus récente existe, et affiche la commande de mise à jour de votre canal d'installation. Désactivez cet avis avec PLUMBER_NO_UPDATE_CHECK=1.
Comment lancer un premier scan en 5 minutes ?
Section intitulée « Comment lancer un premier scan en 5 minutes ? »Le chemin est toujours le même : s'authentifier, scanner, lire, corriger. Plumber lit votre remote origin pour détecter le provider : une URL GitLab bascule sur l'API GitLab, une URL GitHub sur l'analyse des workflows locaux complétée par l'API GitHub. Aucun fichier .plumber.yaml n'est nécessaire pour démarrer : sans fichier, Plumber affiche No .plumber.yaml found; using Plumber's built-in default configuration. et applique sa configuration par défaut embarquée.
-
Authentifiez-vous auprès de votre provider
Côté GitLab, créez un jeton via User Settings -> Access Tokens avec
read_apietread_repository, puis exportez-le :Fenêtre de terminal export GITLAB_TOKEN=<votre-jeton>Côté GitHub, exportez un jeton (voir le paragraphe suivant pour son type et ses droits) ou ouvrez une session avec la CLI
gh:Fenêtre de terminal export GH_TOKEN=<votre-jeton># ou : gh auth loginQuel jeton GitHub créer ? Un jeton fine-grained (à granularité fine), parce qu'il porte le droit Administration:read, nécessaire à la lecture de la protection de branche, sans rien ouvrir d'autre. Un jeton classique de scope
repola lit aussi, mais il donne l'écriture sur tous vos dépôts : à éviter. Créez le jeton fine-grained dans Settings > Developer settings > Personal access tokens > Fine-grained tokens, en lui donnant Administration:read et Actions:read. Ce droit ne s'obtient que sur un dépôt que vous administrez : sur le dépôt d'un tiers, le contrôle de protection de branche reste partiel. Dans l'action GitHub, leGITHUB_TOKENdu workflow ne porte pas ce droit :branchMustBeProtectedy reste alors partiel (voir l'entréegithub-tokende l'action). Plumber litGH_TOKEN, puisGITHUB_TOKEN, puis la sessiongh. -
Lancez l'analyse
Fenêtre de terminal cd mon-projetplumber analyze --score-pointPlumber détecte le provider, l'URL et le chemin du projet depuis
origin. L'option--score-pointajoute le décompte des points sous le score, que la CLI affiche de toute façon. -
Lisez d'abord le bloc Score, puis remontez vers le détail
Cherchez en priorité la lettre et les points sur 100 avec la ligne de statut ; les chemins d'attaque (
Attack paths), qui coûtent le plus cher ; les findings individuels (Individual findings) avec leurs codesISSUE-XXX; le job ou le workflow concerné.
Peut-on scanner un dépôt sans le cloner ?
Section intitulée « Peut-on scanner un dépôt sans le cloner ? »Oui : analyze accepte une cible positionnelle, qui évite d'être dans le dépôt et de dépendre de son remote. Les formes reconnues sont owner/repo (GitHub par défaut), github.com/owner/repo, https://gitlab.com/groupe/projet, git@hote:chemin.git, avec un suffixe /tree/<branche> pour viser une branche. Un hôte autre que github.com et gitlab.com est traité comme GitLab, sauf si vous ajoutez --provider github.
plumber analyze nektos/act --score-pointplumber analyze https://gitlab.com/Bob74/pipeline-craft --branch solution/lab-12 --score-pointSi le dépôt local n'a pas de remote origin, ou s'il pointe ailleurs que vers GitLab ou GitHub, passez les paramètres à la main : --gitlab-url https://gitlab.example.com --project mon-groupe/mon-projet pour un GitLab auto-hébergé, --github-url https://github.example.com --project mon-org/mon-projet pour GitHub Enterprise Server (laissez --github-url vide pour api.github.com).
Que signifient les codes de sortie ?
Section intitulée « Que signifient les codes de sortie ? »Le code de sortie est ce que votre CI lit pour faire échouer le job. Plumber en utilise quatre, et le dernier est celui qu'on rencontre sans l'attendre.
| Code | Sens | Exemple |
|---|---|---|
| 0 | Analyse réussie, seuil atteint | Score supérieur ou égal à --min-points |
| 1 | Seuil non atteint (gate) | Status: FAILED (gate blocks at 100 pts) |
| 2 | Erreur d'exécution | Jeton manquant, configuration invalide, réseau |
| 3 | Analyse incomplète | Collecte de données partielle : score retenu |
Le code 3 prime sur le seuil : une collecte dégradée sort toujours en 3, quel que soit le score. Avec --fail-warnings, une clé de configuration inconnue sort en 2 et un contrôle « could not verify » en 3.
Exemple réel : un pipeline GitLab open source
Section intitulée « Exemple réel : un pipeline GitLab open source »Le dépôt de lab Bob74/pipeline-craft, branche solution/lab-12, a été scanné le 17 octobre 2026 avec la 0.7.2 et un jeton GitLab. Les images non épinglées par digest sont regroupées en chemins d'attaque, un par image : chacun dit ce qu'un attaquant atteindrait si cette image était compromise. Voici le chemin le plus grave et le bloc Score, sans le logo ASCII ni les lignes de séparation. Les chemins 2 et 3 sont omis.
MED Attack path 1 Entry docker.io/docker:27 (mutable image tag) │ └──▶ runs in job `docker-build` └─▶ reaches secrets Plumber could not list and a step that publishes
So if this image is compromised, an attacker can read the secrets Plumber could not list and ship a malicious release Note Plumber could not list the secrets Note Capped at Medium: it needs a dependency compromise first. Plumber could not read: secrets Plumber could not list Fix pin the image by digest ↳ details: run `plumber explain -a 1`
Score Plumber Score 88 / 100 Status: FAILED (gate blocks at 100 pts) 100 - 12 (attack paths) - 0 (individual findings)
Worst case: if this image is compromised, an attacker can read the secrets Plumber could not list and ship a malicious release (attack path 1: docker.io/docker:27) Best fix: pin the image by digest (image docker.io/alpine:3.20), +3 pts, 91 / 100 (A)
medium paths x1 -6 low paths x2 -6Le chemin 1 est Medium : il atteint « a step that publishes », une étape qui publie, ce qui ferait un High, mais une image sur un tag mutable ne devient dangereuse qu'après la compromission de son éditeur, et Plumber le plafonne à Medium (ligne Note). Les deux autres chemins, celui de python:3.12-slim et celui de alpine:3.20, sont Low, parce que Plumber n'a pas pu lister les secrets des jobs concernés : un fait non prouvé ne monte pas un chemin. Total : 6 + 3 + 3 = 12 points perdus, 88 points, B. Le job échoue tout de même, puisque le seuil par défaut est de 100 points.
Exemple réel : un workflow GitHub open source
Section intitulée « Exemple réel : un workflow GitHub open source »Le dépôt nektos/act (commit 4f41128) a été scanné le 17 octobre 2026 avec la 0.7.2, un jeton GitHub et la configuration par défaut. Il illustre un cas où les findings individuels pèsent lourd et où un seul chemin High apparaît. Les sorties ci-dessous sont abrégées : les lignes ↳ details, Worst case et (action …) de la sortie réelle sont omises. Le chemin est affiché avec son entrée, le job concerné et ce qu'il atteint :
HIGH Attack path 1 Entry cargo-bins/cargo-binstall@main via `vedantmgoyal2009/winget-releaser@4ffc7888bffd` (untrusted and mutable external action) │ └──▶ runs in job `release` from workflow `release.yml` └─▶ reaches 3 secrets, a token with push access and write access to actions and a step that publishes
So if this action is compromised or malicious, an attacker can read 3 secrets of the repository, push in your repository and ship a malicious release Note the job runs in environment `release` Note Capped at High: the source is not authorized, so a compromise is more likely Fix pin the outermost action to a commit SHA whose definition pins this reference, or vendor the chain into `./.github/actions/` and pin it thereL'action winget-releaser, épinglée par SHA, embarque elle-même une action composite qui référence cargo-binstall@main : Plumber résout les actions composites et les workflows réutilisables, y compris ceux d'autres dépôts, et affiche la chaîne avec le mot via. L'entrée est mutable, le propriétaire est hors des sources autorisées, donc le chemin est plafonné à High et non à Medium. Le bloc Score qui en résulte :
Score Plumber Score 55 / 100 Status: FAILED (gate blocks at 100 pts) 100 - 15 (attack paths) - 30 (individual findings) individual findings count for 30 at most
Best fix: pin the outermost action to a commit SHA whose definition pins this reference, or vendor the chain into `./.github/actions/` and pin it there, +15 pts, 70 / 100 (C)
ISSUE-307 low x6 -0 ISSUE-713 medium x6 -15 ISSUE-801 medium x6 -15 high paths x1 -15La lecture tient en trois points. Le chemin High coûte 15 points. Les six ISSUE-713 (action venue d'une source non autorisée, mais épinglée par SHA, donc Medium) et les six ISSUE-801 (pas de permissions: explicite) sont Medium (5 points chacun, soit 60 en théorie) : le plafond des findings individuels les ramène à 30 points, que le tableau répartit à 15 par code. Les six ISSUE-307 (credentials persistés par actions/checkout) sont Low et ne retirent rien. Résultat : 100 - 15 - 30 = 55 points, C, et le meilleur correctif, épingler l'action la plus externe, rapporterait 15 points.
Quelles erreurs corriger d'abord ?
Section intitulée « Quelles erreurs corriger d'abord ? »Plumber documente 76 codes d'issues organisés en neuf familles (voir l'annexe en bas de guide). Sur un projet jamais audité, commencez par quatre erreurs simples à corriger et présentes quel que soit le provider. Les extraits YAML qui suivent sont des fragments de .plumber.yaml : la section sur la surcouche montre comment les assembler sans perdre les autres contrôles.
1. Images de conteneur non épinglées par digest
Section intitulée « 1. Images de conteneur non épinglées par digest »Dans le lab pipeline-craft, ces findings sont les entrées des chemins d'attaque vus plus haut. Le problème n'est pas seulement l'usage de latest : un tag de version comme python:3.12-slim reste mutable. Si le registre republie l'image, votre pipeline peut exécuter autre chose demain. Le code est ISSUE-103 (« Container image is not pinned by digest »).
Pour activer le mode strict, la clé se trouve dans le contrôle containerImageMustNotUseForbiddenTags, sous la section du provider :
version: "2.0"gitlab: controls: containerImageMustNotUseForbiddenTags: enabled: true containerImagesMustBePinnedByDigest: trueLe contrôle accepte aussi une liste exemptImages : une image n'est exemptée que si son registre et son nom correspondent et si son tag figure dans tags (le digest est ignoré, un autre tag reste contrôlé). Les images exemptées sont listées sous « Exempted by configuration » dans la sortie.
2. Actions tierces non épinglées par SHA (provider GitHub)
Section intitulée « 2. Actions tierces non épinglées par SHA (provider GitHub) »C'est la première correction à faire côté provider GitHub, sous le code ISSUE-701 (« Third-party action reference is not pinned by commit SHA »). Un step dont le uses: pointe sur owner/action@v4 ou owner/action@main reste mutable côté mainteneur. Si l'action est compromise, comme tj-actions/changed-files en mars 2025 (CVE-2025-30066), votre workflow exécute silencieusement le code modifié avec ses secrets. Corrigez en épinglant sur le SHA du commit, tag en commentaire :
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1Activez le contrôle dans la section GitHub. L'option qui exempte les actions de premier rang est trustedOwners : elle limite le signal initial à la surface tierce, celle que GitHub ne maintient pas. Le guide Épingler les actions par SHA détaille la méthode et sa maintenance, et la fiche de l'incident tj-actions/changed-files raconte le scénario réel.
github: controls: actionsMustBePinnedByCommitSha: enabled: true trustedOwners: - actions - github - dependabot3. Includes sur main, master ou HEAD (provider GitLab)
Section intitulée « 3. Includes sur main, master ou HEAD (provider GitLab) »Un include pointant sur une branche mutable rend votre pipeline difficile à reproduire et peut casser sans changement dans votre dépôt.
include: - project: security/templates file: /pipelines/sast.yml ref: maininclude: - project: security/templates file: /pipelines/sast.yml ref: v2.4.1Le contrôle associé est includesMustNotUseForbiddenVersions avec ISSUE-404. Un tag vaut mieux qu'une branche, mais il peut encore être redéplacé : pour une garantie complète, épinglez le SHA du commit en gardant le tag en commentaire, comme le montre Durcir vos pipelines.
4. Branche principale non protégée
Section intitulée « 4. Branche principale non protégée »Si la branche par défaut n'est pas protégée, un push direct ou une modification non revue peut contourner vos pratiques de revue. Le contrôle existe sous les deux providers et renvoie ISSUE-501 (branche non protégée, Critical sur la branche par défaut, High ailleurs) ou ISSUE-505 (protection non conforme). Côté GitHub, il lit la protection de branche et les rulesets ; sans le droit Administration:read, il s'abstient et le dit dans le champ partialControls du JSON.
gitlab: controls: branchMustBeProtected: enabled: true defaultMustBeProtected: true namePatterns: - main allowForcePush: falseUne cinquième erreur très courante : Docker-in-Docker
Section intitulée « Une cinquième erreur très courante : Docker-in-Docker »Plumber détecte aussi les jobs qui utilisent le service docker:dind, sous ISSUE-412 (« Docker-in-Docker service detected »). Sur des runners partagés, DinD exige le mode privileged, qui augmente le risque d'évasion de conteneur et de mouvement latéral. Pour construire des images sans lui, deux outils sans démon existent, avec des exigences différentes sur un runner à executor Docker. kaniko, tiré de son fork maintenu osscontainertools/kaniko (le dépôt de Google est archivé depuis juin 2025), construit avec les profils de sécurité par défaut. Buildah échoue avec ces profils et demande des filtres seccomp et AppArmor assouplis. Le guide Registries GitLab donne les deux jobs et leurs réglages. Le contrôle existe aussi côté GitHub avec ISSUE-413 (daemon non sécurisé).
Quelles protections supply chain activer ensuite ?
Section intitulée « Quelles protections supply chain activer ensuite ? »Une fois les quatre erreurs de base corrigées, Plumber offre une famille de contrôles 7XX dédiée aux actions tierces, un vecteur d'attaque de supply chain que des incidents réels ont démontré (tj-actions/changed-files en mars 2025). Ces contrôles vont plus loin que le simple pinning et méritent d'être activés dès que votre pipeline est stable.
Restreindre les sources d'actions autorisées (ISSUE-713)
Section intitulée « Restreindre les sources d'actions autorisées (ISSUE-713) »Épingler une action par SHA garantit qu'elle ne change pas, mais pas qu'elle vienne d'un auteur de confiance. Le contrôle githubActionMustComeFromAuthorizedSources signale toute action dont le propriétaire n'est ni GitHub, ni votre organisation, ni sur votre liste d'autorisation, ni (si vous fixez minimumStars) au-dessus du seuil d'étoiles. C'est la parade directe contre le squat de dépôt ou une action inconnue qui hériterait de vos secrets. C'est ici que s'emploie trustGithubOfficialActions, et pas dans le contrôle d'épinglage.
github: controls: githubActionMustComeFromAuthorizedSources: enabled: true trustGithubOfficialActions: true trustSameOrgActions: true # Seuil d'étoiles minimal (0 = désactivé, nécessite l'API GitHub) minimumStars: 0 trustedGithubActions: - docker/setup-buildx-action - docker/login-action - ossf/scorecard-actionBloquer les actions portant un CVE connu (ISSUE-703)
Section intitulée « Bloquer les actions portant un CVE connu (ISSUE-703) »Le contrôle actionsMustNotCarryKnownCVEs croise chaque action épinglée avec les avis de sécurité publiés. C'est l'un des rares cas où une dépendance peut ouvrir un chemin Critical : un avis publié sur la version épinglée suffit à lever le plafond. Le contrôle complémentaire actionsMustNotBeArchived (ISSUE-702) signale les actions dont le dépôt amont est archivé. Les deux interrogent l'API GitHub : sans jeton, ils sont ignorés et l'analyse est incomplète.
github: controls: actionsMustNotCarryKnownCVEs: enabled: true actionsMustNotBeArchived: enabled: trueFermer la porte pull_request_target (ISSUE-804)
Section intitulée « Fermer la porte pull_request_target (ISSUE-804) »Le contrôle pullRequestTargetMustNotCheckoutHead détecte un workflow déclenché par pull_request_target qui fait un checkout de la HEAD de la pull request. Le code non fusionné d'un contributeur externe s'exécute alors avec les secrets du dépôt. Le guide Sécuriser pull_request_target détaille les patterns sûrs.
github: controls: pullRequestTargetMustNotCheckoutHead: enabled: trueNe jamais laisser un déclencheur peu fiable écrire le cache (ISSUE-705)
Section intitulée « Ne jamais laisser un déclencheur peu fiable écrire le cache (ISSUE-705) »Le contrôle untrustedTriggersMustNotWriteCache (« Untrusted trigger granted cache write ») signale un job dont le cache-mode vaut write ou write-only sous un déclencheur peu fiable : pull_request_target, issue_comment ou workflow_run. Un cache écrit par un tel job peut être restauré ensuite par un job privilégié. Le remède est cache-mode: read, que l'on pose au niveau du workflow ou sous un job (fragment ci-dessous). Le contrôle accepte deux listes d'exemptions : exemptWorkflows (motif sur le nom de fichier) et exemptJobs (motif <workflow>/<job id>).
on: pull_request_targetcache-mode: read # niveau workflow ; la clé peut aussi se poser sous un jobjobs: test: runs-on: ubuntu-24.04Le fragment ne montre que l'emplacement de la clé : un workflow complet porte aussi ses permissions: et ses étapes.
Comment écrire le fichier de configuration ?
Section intitulée « Comment écrire le fichier de configuration ? »Le schéma v2.0 organise les contrôles par provider : tout ce qui concerne GitLab CI/CD vit sous gitlab.controls:, tout ce qui concerne GitHub Actions sous github.controls:. La clé version: "2.0" est obligatoire. Le moteur Rego est le seul moteur d'évaluation, il n'y a pas de bloc engine:. La référence des contrôles liste toutes les clés.
Surcouche ou configuration complète ?
Section intitulée « Surcouche ou configuration complète ? »Il existe deux façons d'écrire .plumber.yaml, et elles ne se comportent pas pareil. Une surcouche (overlay) commence par extends: plumber:default et ne liste que ce que vous changez ; tout bloc absent hérite de la configuration de base et les contrôles ajoutés à la base s'activent d'eux-mêmes. Une configuration complète, sans extends, décrit tout : un bloc absent n'est pas exécuté. La surcouche est l'usage normal.
Surcouche (extends: plumber:default) | Configuration complète | |
|---|---|---|
| Bloc absent | Hérite de la base | Contrôle non exécuté |
| Maps (clés) | Fusionnées avec la base | Telles qu'écrites |
| Listes | Remplacent celles de la base | Telles qu'écrites |
includePlumberDefaults | Actif (garde les listes de confiance de la base) | Sans effet |
Le piège tient à la ligne des listes : écrire trustedOwners: [actions] dans une surcouche remplace la liste de base au lieu de l'allonger. Générez le point de départ avec --overlay :
plumber config generate --overlay --output .plumber.yamlLe fichier produit contient la clé extends: plumber:default, la version "2.0" et un exemple d'override commenté. Voici une surcouche complète qui active le mode strict sur les digests et exempte une image d'infrastructure :
extends: plumber:defaultversion: "2.0"
gitlab: controls: containerImageMustNotUseForbiddenTags: containerImagesMustBePinnedByDigest: true
github: controls: githubActionMustComeFromAuthorizedSources: includePlumberDefaults: true trustedGithubActions: - docker/setup-buildx-actionQuelles commandes autour de la configuration ?
Section intitulée « Quelles commandes autour de la configuration ? »Ces commandes se lisent en trois groupes : validate et view inspectent ce que Plumber comprend de votre fichier ; resolve, slim et diff le confrontent à la base ; generate et init en créent un. Notez le --force de generate : sans lui, la commande refuse d'écraser un .plumber.yaml existant.
plumber config validate # fautes de frappe, clés inconnues, schémaplumber config view # configuration lue, sans commentairesplumber config resolve # configuration EFFECTIVE, après fusion avec la baseplumber config slim # réduit une configuration complète en surcouche minimaleplumber config diff # écarts avec le défaut embarquéplumber config generate --output .plumber.yaml --force # template complet commentéplumber config init # assistant interactif (terminal requis)config validate signale les fautes de frappe avec une suggestion (Unknown control in gitlab.controls: "branchMustBeProtectd". Did you mean "branchMustBeProtected"?) ; avec --fail-warnings, un avertissement sort en code 2. config resolve est la commande à lancer en cas de doute : elle imprime ce que Plumber exécute vraiment, sans fichier ou après héritage. config slim fait le chemin inverse d'une configuration complète vers une surcouche (les commentaires sont perdus).
Comment migrer une configuration du schéma v1 ?
Section intitulée « Comment migrer une configuration du schéma v1 ? »Si votre .plumber.yaml est écrit au schéma v1, avec un controls: au top-level, config validate répond legacy config schema detected et vous invite à migrer. plumber config migrate réécrit le fichier au schéma v2.0 :
# Crée .plumber.yaml.v2 à côté du fichier originalplumber config migrate
# Ou remplace en place (le fichier original part en .plumber.yaml.bak)plumber config migrate --in-placeLa migration encapsule le bloc controls: sous gitlab.controls:, passe version: "1.0" à "2.0" et supprime le bloc engine:, absent du schéma v2.0. Elle est idempotente : sur un fichier déjà en v2.0, elle répond Nothing to do. Elle renomme aussi la clé de contrôle releaseWorkflowsMustNotRestoreUntrustedCache en untrustedTriggersMustNotWriteCache.
Comment ajouter Plumber à GitLab CI avec le composant officiel ?
Section intitulée « Comment ajouter Plumber à GitLab CI avec le composant officiel ? »Le composant CI/CD officiel est le bon point d'entrée côté GitLab (voir sa documentation). Il est publié dans le catalogue sous gitlab.com/getplumber/plumber/plumber et se référence par le tag Git du dépôt, @v0.7.2 : la forme @0.7.2 est refusée à la résolution (« content not found »). Pour l'option la plus stricte, remplacez le tag par le SHA du commit du tag, avec la version en commentaire :
include: - component: gitlab.com/getplumber/plumber/plumber@d364d54c7c045571d129252e4fb0cf2926c029fa # v0.7.2Démarrage rapide recommandé
Section intitulée « Démarrage rapide recommandé »Ajoutez d'abord le jeton GITLAB_TOKEN dans Settings -> CI/CD -> Variables, en le masquant et en limitant sa portée à l'environnement plumber : le job du composant se rattache à cet environnement, et le jeton reste invisible des autres jobs du projet. Utilisez ensuite ce squelette :
workflow: rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" - if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS when: never - if: $CI_COMMIT_BRANCH - if: $CI_COMMIT_TAG
include: - component: gitlab.com/getplumber/plumber/plumber@v0.7.2Ce bloc workflow:rules évite de créer à la fois un pipeline de branche et un pipeline de merge request sur le même push. Le job s'exécute sur les merge requests, la branche par défaut et les tags, et utilise par défaut l'image du composant, épinglée par digest (getplumber/plumber@sha256:14698bde…).
Quels réglages le composant expose-t-il ?
Section intitulée « Quels réglages le composant expose-t-il ? »Le bloc inputs: surcharge les valeurs par défaut. Deux réglages comptent d'abord : stage, qui sort le job du .pre, et min_points (ou min_score), le seuil du Plumber Score en dessous duquel le job échoue (le calcul du score est détaillé plus bas). Trois groupes d'entrées complètent l'ensemble : la cible (project_path, branch, server_url, ci_config_path, remplies depuis les variables CI), la configuration (config_file, controls, skip_controls, fail_warnings), et les fichiers de sortie.
include: - component: gitlab.com/getplumber/plumber/plumber@v0.7.2 inputs: stage: test min_points: 80 # échoue sous 80 points sur 100 config_file: .plumber.yaml score_point: true # décompte des points dans le journal environment_name: plumber # environnement auquel le jeton est restreintLes exports sont actifs par défaut : output_file (plumber-report.json), pbom_file (plumber-pbom.json), pbom_cyclonedx_file (plumber-cyclonedx-sbom.json) et glsast_file (gl-sast-report.json), ce dernier étant déjà téléversé comme rapport sast du Security Dashboard. Pour désactiver un export, passez-lui une chaîne vide. D'autres entrées existent : allow_failure, verbose, mr_comment, badge, score_push, score_endpoint, platform et platform_allow_http. L'entrée score est un no-op déprécié : utilisez score_point. Le composant n'a aucune entrée pour le profil de score (voir plus bas).
Dans quel ordre le composant cherche-t-il la configuration ?
Section intitulée « Dans quel ordre le composant cherche-t-il la configuration ? »Le composant recherche la configuration dans cet ordre : l'input config_file s'il est défini, puis le fichier .plumber.yaml à la racine du dépôt, puis la configuration par défaut embarquée dans l'image.
Et si vous écrivez un job manuel ?
Section intitulée « Et si vous écrivez un job manuel ? »Gardez cette variante pour les besoins spécifiques. L'image a pour ENTRYPOINT /plumber : avec un script:, il faut donc neutraliser l'entrypoint (entrypoint: [""]), sans quoi GitLab lancerait /plumber avec votre script comme argument.
plumber-scan: stage: test image: name: getplumber/plumber:0.7.2@sha256:14698bde5dbc40fe2b5b9d339b039084b18f3b195e2fba0ceef9ff05ec69654c entrypoint: [""] script: - /plumber analyze --gitlab-url "$CI_SERVER_URL" --project "$CI_PROJECT_PATH" --branch "$CI_COMMIT_REF_NAME" --glsast gl-sast-report.json --score-point artifacts: when: always reports: sast: gl-sast-report.jsonSi vous voulez ajouter --mr-comment et --badge, séparez-les en deux jobs : le commentaire de merge request n'a de sens qu'en pipeline de MR, et le badge ne se met à jour que sur la branche par défaut.
Comment utiliser le composant sur un GitLab auto-hébergé ?
Section intitulée « Comment utiliser le composant sur un GitLab auto-hébergé ? »Un GitLab auto-hébergé ne peut pas consommer directement le composant de gitlab.com. La documentation de l'éditeur propose deux méthodes : importer le dépôt https://gitlab.com/getplumber/plumber.git, ou le forker et le synchroniser par miroir (méthode recommandée, car les mises à jour suivent). Dans les deux cas, activez le CI/CD Catalog dans le projet, publiez une release sur un tag existant, puis consommez le composant local :
include: - component: gitlab.example.com/infrastructure/plumber/plumber@v0.7.2Pour une garantie complète, épinglez le SHA du commit du miroir (celui du tag v0.7.2 sur gitlab.com est d364d54c7c045571d129252e4fb0cf2926c029fa) avec la version en commentaire. Les SHA du miroir GitLab et du dépôt GitHub diffèrent.
Comment ajouter Plumber à GitHub Actions avec l'action officielle ?
Section intitulée « Comment ajouter Plumber à GitHub Actions avec l'action officielle ? »L'action GitHub getplumber/plumber (voir sa documentation) est une action composite : elle télécharge le binaire de la version demandée, vérifie sa somme de contrôle et son attestation de provenance (SLSA), lance l'analyse, téléverse le rapport SARIF dans l'onglet Security et publie les rapports en artefact. Comme tout uses:, elle s'épingle sur le SHA du commit de la release, tag en commentaire.
name: plumberon: push: branches: [main] pull_request:
permissions: {}
jobs: plumber: runs-on: ubuntu-24.04 permissions: contents: read actions: read security-events: write steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: getplumber/plumber@ccd274c2abc9f66d7e44051d612d49c8ed1747ac # v0.7.2 with: min-score: BLes permissions minimales sont contents: read (le checkout), actions: read (lire les états de workflow et un journal de run pour prouver la portée réelle du GITHUB_TOKEN) et security-events: write (téléversement SARIF). Ajoutez id-token: write seulement si vous publiez le score avec score-push. Sur un dépôt privé, le téléversement SARIF exige GitHub Advanced Security ; désactivez-le avec upload-sarif: false sinon.
Les entrées principales portent les mêmes noms que les options de la CLI, avec des tirets : min-points, min-score, config-file, controls, skip-controls, fail-warnings. D'autres règlent les sorties : upload-sarif, upload-artifacts (30 jours de rétention), output, pbom, pbom-cyclonedx, sarif. L'action expose en sortie passed, report et sarif. Trois entrées méritent une explication :
github-token: par défaut leGITHUB_TOKENdu workflow. Une lecture refusée n'est jamais une erreur : le fait concerné resteunknowndans le score ;soft-fail: true: le code 1 (seuil non atteint) ne fait plus échouer le job, mais les codes 2 et 3 échouent toujours ;score: à l'inverse du composant GitLab, l'entréescorede l'action active bien le décompte détaillé (--score-point).
L'entrée version vaut v0.7.2 par défaut et verify-attestation vaut true : laissez la vérification active.
Comment exporter un PBOM et l'analyser ?
Section intitulée « Comment exporter un PBOM et l'analyser ? »Le PBOM (Pipeline Bill of Materials) est l'inventaire des dépendances de votre pipeline. Côté GitLab, il liste les images (image: et services:), les composants, les templates et les includes. Côté GitHub, il liste les conteneurs (container: et services:), les actions tierces (uses: owner/repo@ref) et les workflows réutilisables. Pour les actions composites et les workflows d'autres dépôts, le PBOM garde la provenance : le champ nested marque une dépendance trouvée à l'intérieur d'une autre, et via donne la chaîne.
plumber analyze \ --pbom plumber-pbom.json \ --pbom-cyclonedx plumber-cyclonedx-sbom.jsonQuand utiliser chaque format ?
Section intitulée « Quand utiliser chaque format ? »Les deux fichiers décrivent le même pipeline, mais pas pour le même lecteur. Le PBOM natif conserve des métadonnées propres à Plumber ; le CycloneDX (version 1.5, JSON) perd ce détail au profit d'un format que Trivy, Grype ou Dependency-Track consomment directement. Rien n'empêche de générer les deux. L'éditeur présente le PBOM comme une fonctionnalité en phase initiale, sans standard reconnu pour les inventaires de pipelines.
| Format | Quand l'utiliser | Ce qu'il contient |
|---|---|---|
| PBOM natif | Vous voulez comprendre le pipeline en détail | Métadonnées riches propres à Plumber, dont nested et via |
| CycloneDX | Vous voulez brancher un autre outil | Format standard pour Trivy, Grype ou Dependency-Track |
Pour obtenir l'inventaire seul, sans évaluer les contrôles, ajoutez --no-controls : Plumber collecte et écrit les fichiers d'inventaire (PBOM, JSON, CSV, OCSF) mais ne rend ni verdict ni score, et sort en code 0 si la collecte est complète. L'option est incompatible avec --controls et --skip-controls.
Scanner le CycloneDX avec Trivy
Section intitulée « Scanner le CycloneDX avec Trivy »La sous-commande sbom prend le fichier en entrée sans retélécharger les images ; le filtre --severity limite la sortie aux niveaux qui justifient une action immédiate.
trivy sbom plumber-cyclonedx-sbom.json --severity HIGH,CRITICALScanner le CycloneDX avec Grype
Section intitulée « Scanner le CycloneDX avec Grype »Le préfixe sbom: indique à Grype que l'argument est un inventaire et non une image, et --fail-on high fait sortir la commande en code non nul, ce qui casse le job de CI dès qu'une vulnérabilité de ce niveau apparaît.
grype sbom:plumber-cyclonedx-sbom.json --fail-on highComment remonter les findings dans les tableaux de bord ?
Section intitulée « Comment remonter les findings dans les tableaux de bord ? »Le PBOM inventorie le pipeline ; pour faire remonter les écarts dans vos plateformes, Plumber produit des rapports de findings standardisés. Vous les générez en ajoutant un drapeau à plumber analyze, chacun écrivant dans un fichier :
| Option | Format | Usage |
|---|---|---|
--sarif | SARIF 2.1.0 | GitHub Code Scanning (onglet Security) |
--glsast | GitLab SAST (schéma 15.0.4) | Security Dashboard et widget de merge request GitLab |
--csv | CSV | Une ligne par contrôle, plus une par finding en échec : tableur |
--ocsf | OCSF 1.8.0 (Compliance Finding) | Outils d'observabilité sécurité compatibles OCSF |
--output | JSON | Rapport complet, relisible par plumber explain --run |
plumber analyze --sarif plumber.sarif --glsast gl-sast-report.json --csv plumber.csvCes formats ne remplacent pas la lecture du résumé : ils servent à historiser les findings et à les rapprocher des autres scanners déjà branchés sur vos tableaux de bord. Dans un SARIF, la règle est le code d'issue, le niveau suit la sévérité contextuelle, et la propriété plumber/baseSeverity garde la sévérité propre du code.
Comment comprendre un code ou un chemin avec plumber explain ?
Section intitulée « Comment comprendre un code ou un chemin avec plumber explain ? »plumber explain est la commande réflexe devant un ISSUE-XXX inconnu : elle fonctionne hors ligne, sans configuration ni dépôt, et le préfixe ISSUE- peut être omis.
plumber explain ISSUE-412 # raccourci : plumber explain 412plumber explain --list # liste compacte des codesplumber explain --all # référence exhaustiveplumber explain 701 --json # sortie JSON exploitableElle renvoie la description, la remédiation et le lien vers la documentation du code. Le JSON ajoute le nom du contrôle (controlName), le rôle du code (role), le type d'entrée (entryKind) et, pour un finding imbriqué, la remédiation à appliquer à la chaîne (nestedRemediation).
explain relit aussi un chemin d'attaque du dernier scan, avec ses claims (voir la section sur le score) :
plumber explain -a 1 # chemin numéro 1 du dernier scanplumber explain -a all # tous les cheminsplumber explain -a 1 --run plumber-report.json # relit un rapport JSON de CIplumber explain -a 1 --project nektos/act --provider githubL'option -a N n'est valable que si le scan a au moins N chemins : sur un run à un seul chemin, explain -a 3 répond par une erreur (code 2) qui indique le nombre de chemins disponibles. Chaque scan garde son JSON dans le cache utilisateur (~/.cache/plumber/runs/<provider>/<propriétaire>/<dépôt>.json, lisible par vous seul), ce qui permet de relire un chemin sans relancer l'analyse. Pour un scan fait en CI, l'option --run lit le rapport JSON récupéré en artefact.
Autre commande utile pour un outil tiers : plumber catalog imprime en JSON le catalogue des contrôles et des issues de votre version.
Comment Plumber calcule-t-il le score ?
Section intitulée « Comment Plumber calcule-t-il le score ? »Le Plumber Score (profil scoring-v4, le défaut) part de 100 points et retire un prix fixe pour chaque chemin d'attaque, puis un prix plus faible pour chaque finding individuel, c'est-à-dire un problème qu'aucun chemin n'utilise. Un chemin se lit en trois temps : une entrée (une action, une image, une entrée utilisateur injectée dans un script, une branche non protégée), le job où ce code s'exécute, et ce qu'il atteint (secrets, jeton d'écriture, publication, déploiement). La lettre se lit sur les points finaux, avec des seuils fixes.
| Lettre | Points | Ce que la lettre signifie |
|---|---|---|
| A | 90 à 100 | Aucune exposition notable |
| B | 71 à 89 | Exposition mineure |
| C | 51 à 70 | Exposition à corriger |
| D | 31 à 50 | Exposition importante accumulée |
| E | 0 à 30 | Un chemin d'attaque Critical subsiste, ou des pertes très lourdes |
Quel prix pour chaque niveau ?
Section intitulée « Quel prix pour chaque niveau ? »Le niveau d'un chemin dépend de ce qu'il atteint, et seuls les faits prouvés comptent. Il est Critical quand un secret ou un jeton d'écriture est prouvé, et que s'y ajoute soit un changement prouvé (publication, déploiement, écriture dans le dépôt), soit une entrée contrôlée par l'ouvreur d'une pull request. Un jeton déclaré en écriture sur contents, packages ou deployments compte à la fois comme privilège et comme changement : un job contents: write atteint par une telle entrée, sous un déclencheur privilégié comme pull_request_target, est donc Critical (sur pull_request, une PR de fork ne reçoit qu'un jeton en lecture). Le chemin est High quand un changement prouvé, ou un secret ou jeton d'écriture prouvé, est atteint seul. Une entrée de pull request sans secret, jeton d'écriture ni changement prouvé reste Low, comme tout chemin où du code s'exécute sans que rien de prouvé soit atteint. Le tableau donne les prix.
| Niveau | Prix d'un chemin d'attaque | Prix d'un finding individuel |
|---|---|---|
| Critical | 30 | 20 |
| High | 15 | 10 |
| Medium | 6 | 5 |
| Low | 3 | 2 |
Une dépendance seulement mal épinglée (tag mutable, action sans SHA) est plafonnée à Medium, puisqu'il faut d'abord compromettre son éditeur ; le plafond monte à High quand la source est hors de vos sources autorisées. Une dépendance connue mauvaise (avis publié, commit hors du dépôt de l'action) n'a pas de plafond. Un finding utilisé par un chemin ne coûte rien de plus : la sévérité affichée est celle du chemin, et le JSON garde la sévérité propre du code dans baseSeverity.
Comment les limites protègent-elles la note ?
Section intitulée « Comment les limites protègent-elles la note ? »Trois limites évitent qu'une accumulation de petits constats fasse s'effondrer le score.
- Les findings individuels coûtent 30 points au total, au maximum, les plus graves d'abord : à eux seuls, ils ne peuvent jamais faire descendre un dépôt en E.
- Une limite emboîtée s'applique à tout ce qui n'est pas Critical : les éléments Low comptent pour 29 points au plus, Medium et Low ensemble pour 49, High, Medium et Low ensemble pour 69. Sans aucun Critical, un dépôt garde donc au moins 31 points (D), au moins 51 (C) sans High, au moins 71 (B) sans rien au-dessus de Low.
- Seul un chemin Critical force le E : il bloque le score à 30 points au plus. Un finding Critical isolé coûte ses 20 points et ne force rien.
Qu'est-ce que prouvé, supposé ou inconnu ?
Section intitulée « Qu'est-ce que prouvé, supposé ou inconnu ? »Chaque fait sur lequel repose un chemin est une affirmation (claim) qui porte un état. proven : tous les faits sont établis. assumed : elle repose sur une valeur prise par défaut faute de signal, par exemple le GITHUB_TOKEN d'un dépôt dont Plumber n'a pas pu lire l'écriture réelle. unknown : un fait n'a pas pu être décidé (lecture refusée, référence non résolue). Seul le prouvé déplace un palier : le supposé et l'inconnu restent affichés dans la portée du chemin, sans rien changer à son niveau. Dans l'exemple pipeline-craft, c'est ce qui rend les chemins Low : les secrets n'ont pas pu être listés.
Les lignes Note expliquent l'écart : Plumber assumed the token can write: … pour un état supposé, Plumber could not read: … pour un état inconnu, et plumber explain -a N liste toutes les affirmations sous Claims. Plumber pose aussi trois questions par job : qui peut le déclencher (un inconnu, après approbation d'un mainteneur, uniquement ce dépôt), ce qu'il détient (secrets par nom, jeton) et s'il s'exécute (un workflow désactivé, un if: toujours faux ou un workflow_call jamais appelé ne forment aucun chemin). Un jeton sans bloc permissions compte comme détenu, jamais comme un changement. Le détail complet figure dans la documentation officielle du score.
Peut-on utiliser une autre formule ?
Section intitulée « Peut-on utiliser une autre formule ? »Le profil scoring-v3 attribue des points par sévérité de code, sans chemins d'attaque. Il se choisit avec --score-profile v3 ou la variable PLUMBER_ANALYZE_SCORE_PROFILE=v3, et le champ profileId du JSON indique le profil utilisé. Sur le commit de nektos/act scanné plus haut, ce profil donne 28,0 points, E contre 55 points, C pour le profil par défaut : un profil classe plus durement un dépôt qui accumule des constats Medium. Seule la CLI expose cette option : le composant GitLab et l'action GitHub n'ont aucune entrée de profil.
Comment choisir le seuil qui fait échouer le job ?
Section intitulée « Comment choisir le seuil qui fait échouer le job ? »Le score est la seule porte de sortie : il pilote le code de retour via --min-points (0-100) ou --min-score (lettre A à E). Le défaut est --min-points 100, donc le moindre finding fait échouer le job (FAILED (gate blocks at 100 pts)). Fixez un seuil réaliste si vous acceptez un résidu : --min-points 80 échoue sous 80, --min-score B échoue sur C, D et E. Posé seul, --min-score décide seul (le défaut de 100 points ne s'applique pas) ; posés ensemble, les deux doivent passer.
plumber analyze --min-points 80 # échoue sous 80 points sur 100plumber analyze --min-score B # échoue en dessous d'un BTrois cas particuliers surprennent. Un dépôt GitHub sans aucun workflow passe le seuil si les contrôles activés ne remontent rien ; il échoue si aucun contrôle n'a été évalué (configuration sans section pour le provider, filtre qui écarte tout), avec Status: NOTHING CHECKED (no control was evaluated). L'option --threshold est dépréciée. Enfin, en mode Platform, les options --min-*, --controls et --skip-controls sont ignorées avec un avertissement.
Comment cibler seulement certains contrôles ?
Section intitulée « Comment cibler seulement certains contrôles ? »Ces deux drapeaux attendent des noms de contrôles séparés par des virgules, pas des codes d'issues, et s'excluent mutuellement. Ils servent à isoler une correction en cours ; ne les laissez pas dans le job de CI, sinon vous cessez de mesurer les contrôles écartés.
# Uniquement les images et la protection de brancheplumber analyze --controls containerImageMustNotUseForbiddenTags,branchMustBeProtected
# Tout sauf la protection de brancheplumber analyze --skip-controls branchMustBeProtectedChaque option de la CLI a son pendant en variable d'environnement, de la forme PLUMBER_ANALYZE_<OPTION> (par exemple PLUMBER_ANALYZE_MIN_POINTS=80) ; les options de la ligne de commande priment.
Commentaires de merge request et badges projet
Section intitulée « Commentaires de merge request et badges projet »Avec le composant GitLab, activez-les ainsi :
include: - component: gitlab.com/getplumber/plumber/plumber@v0.7.2 inputs: mr_comment: true badge: truePlumber Radar : comment vérifier l'hygiène CI/CD d'un fournisseur ?
Section intitulée « Plumber Radar : comment vérifier l'hygiène CI/CD d'un fournisseur ? »Plumber Radar est une page publique de getplumber.io qui publie le Plumber Score de projets open source scannés en continu, avec la CLI et une configuration d'étude dont le YAML est consultable sur la page. Vous la lisez sans compte à l'adresse getplumber.io/radar. Avant d'ajouter l'action ou l'image d'un fournisseur à votre liste d'autorisation, vous voyez comment ce fournisseur protège ses propres pipelines. La liste se filtre par provider, par configuration, par activité, et selon qu'elle inclut ou non les dépôts archivés et les forks.
La vue la plus utile pour juger un fournisseur est le rapport d'organisation, qui agrège tous ses projets publics. Il s'ouvre en cliquant sur le nom d'une organisation, ou par une URL de la forme https://getplumber.io/radar?q=<organisation>/&provider=github&report=1.

Ce rapport sur l'organisation docker (capture du 14 octobre 2026) se lit de haut en bas. Le bandeau donne le nombre de chemins d'attaque par niveau, le graphique la répartition des notes des 53 projets. Le bloc « What an attacker could do » dit ce qu'un attaquant atteindrait, et « How they could get in » les entrées qu'il emprunterait, chacune avec son niveau. Pour un chemin Critical, la page résume le risque mais retient les détails. Les chiffres bougent à chaque scan : seule la page en ligne fait foi.
Trois précautions évitent de surinterpréter ces chiffres.
- C'est une photographie datée : chaque ligne indique la date de son dernier scan, les projets n'ont pas tous été scannés par la même version de Plumber, et la note bouge à chaque commit du fournisseur.
- Le Radar juge le pipeline du fournisseur, pas son code : pour la maintenance, la revue et la gouvernance du projet, croisez avec OpenSSF Scorecard, et pour une démarche d'évaluation complète des tiers, avec le guide TPRM pour équipes techniques.
- Une bonne note ne remplace pas l'épinglage : même une action notée A s'épingle par SHA de commit dans vos workflows.
Quand passer à la Platform ?
Section intitulée « Quand passer à la Platform ? »La Platform n'est pas le bon point de départ pour un débutant. Elle devient pertinente quand vous devez suivre plusieurs projets dans le temps, disposer d'un tableau de bord centralisé, gérer des politiques communes à plusieurs équipes et être alerté d'une dérive. Avec --platform <url> (entrée platform du composant et de l'action), la CLI pousse tout le résultat à la Platform par OIDC ; ce mode prime sur --score-push et rend les seuils locaux inertes.
Si votre besoin tient dans « je veux comprendre et sécuriser mon pipeline », restez sur la CLI, le composant GitLab ou l'action GitHub. Si votre besoin devient « je veux gouverner un portefeuille de projets », regardez la Platform.
Annexe : les codes d'issues par famille
Section intitulée « Annexe : les codes d'issues par famille »Plumber 0.7.2 documente 76 codes d'issues en neuf familles, chacun avec une description et une remédiation accessibles via plumber explain ISSUE-XXX. La famille la plus fournie est la 4XX (20 codes : jobs, includes, composants, Docker-in-Docker, nommage), suivie de la 2XX (14 codes) ; la famille 7XX dédiée aux actions tierces en compte 15. Pour la liste exacte de votre installation, utilisez plumber explain --list, qui rend une ligne par code, ou plumber catalog pour le JSON. L'index des issues en ligne les décrit aussi.
Le tableau donne la grille de lecture pour identifier la famille d'une issue remontée :
| Famille | Plage | Couvre | Exemples |
|---|---|---|---|
| Images de conteneurs | ISSUE-1XX (3 codes) | Sources, tags, digest | 101 source non autorisée, 102 tag interdit, 103 image non épinglée par digest |
| Exécution et expressions | ISSUE-2XX (14 codes) | Variables, injection, conditions | 203 debug trace, 207 injection d'expression, 209 écriture dans $GITHUB_ENV, 213 toJson(github) |
| Secrets et jetons | ISSUE-3XX (8 codes) | Fuites, secrets: inherit, jetons | 302 secrets: inherit, 307 credentials persistés, 309 toJson(secrets) |
| Jobs, templates et structure | ISSUE-4XX (20 codes) | Jobs, includes, composants, DinD, OIDC, nommage | 404 include mutable, 410 job de sécurité affaibli, 412 DinD, 414 composant de source non autorisée, 421 publication par jeton statique au lieu d'OIDC, 422 workflow sans name: |
| Protection de branche et revue | ISSUE-5XX (6 codes) | Protection de branche, règles d'approbation | 501 branche non protégée, 504 aucune règle d'approbation sur les branches protégées, 505 protection non conforme |
| Politique de sécurité GitLab | ISSUE-6XX (1 code) | Projet de politique de sécurité lié | 601 projet de politique absent ou mauvais |
| Actions tierces (supply chain) | ISSUE-7XX (15 codes) | Pinning, sources, CVE, archivage, code distant, cache | 701 action non épinglée par SHA, 703 CVE connu, 705 déclencheur peu fiable avec écriture de cache, 706 FROM non épinglé par digest, 713 source non autorisée, 714 code distant mutable |
| Permissions et déclencheurs | ISSUE-8XX (4 codes) | permissions:, déclencheurs | 801 pas de permissions: explicite, 804 pull_request_target piégé |
| Hygiène du dépôt | ISSUE-9XX (5 codes) | Mises à jour, scanners, SECURITY.md | 903 pas d'outil de mise à jour, 904 pas de scanner statique, 905 pas de SECURITY.md |
La famille 7XX concentre les protections supply chain contre les actions tierces compromises. Les familles 8XX et 9XX sont quasi exclusivement GitHub : elles couvrent les risques propres à l'écosystème Actions (permissions du GITHUB_TOKEN, outil de mise à jour, scanners, SECURITY.md).
Dépannage
Section intitulée « Dépannage »Avant de parcourir le tableau, retenez deux points : sans jeton, l'analyse est incomplète (code 3), et le défaut min-points 100 fait échouer le job au premier finding.
| Symptôme | Cause probable | Solution |
|---|---|---|
Status: INCOMPLETE (data collection failed), code de sortie 3, score absent | Jeton absent ou refusé : l'API n'a pas pu lire la protection de branche, les environnements ou les états des workflows | Exportez GH_TOKEN ou GITLAB_TOKEN (ou gh auth login), avec les droits listés en prérequis |
Auth none, running in degraded mode | Aucun jeton détecté | Idem : seuls les contrôles sur le contenu des workflows s'exécutent |
Could not verify sur des actions | Enrichissement par l'API GitHub sauté (CVE, dépôt archivé, étoiles) | Fournissez un jeton ; PLUMBER_METADATA_TOKEN résout les versions d'actions tierces |
GITLAB_TOKEN environment variable is required for GitLab analysis | Variable manquante côté provider GitLab | Exportez GITLAB_TOKEN avant plumber analyze |
401 Unauthorized (GitLab) | Jeton incomplet ou invalide | Vérifiez read_api et read_repository |
403 Forbidden on MR comment ou sur le badge | Scope ou rôle insuffisant | Utilisez le scope api (et un rôle Maintainer pour le badge) |
Status: FAILED (gate blocks at 100 pts) malgré une bonne lettre | Seuil par défaut min-points 100 : le moindre finding fait échouer | Normal ; fixez --min-points ou --min-score si vous acceptez un résidu |
Status: NOTHING CHECKED (no control was evaluated) | Aucun contrôle évalué (configuration sans section pour ce provider, filtre qui les écarte tous) | Vérifiez .plumber.yaml, --controls et --skip-controls |
unknown key 'controls' at root ou legacy config schema detected | Configuration au schéma v1 | Lancez plumber config migrate |
Unknown control in gitlab.controls: … | Faute de frappe dans un nom de contrôle | Reprenez la suggestion Did you mean … ; config validate la répète |
Error: the run in …/runs/… has 1 attack path avec explain -a 3 | Le scan n'a pas autant de chemins | Demandez -a 1, ou -a all |
Avertissement --threshold is deprecated | Seuil exprimé en pourcentage | Passez à --min-points ou --min-score |
| Le composant ne s'exécute pas | Job en .pre sans autre stage normal | Forcez stage: test |
| Deux pipelines sur le même push | workflow:rules absent | Ajoutez le bloc de démarrage rapide |
| Le job manuel échoue au démarrage | Entrypoint /plumber actif avec un script: | Ajoutez entrypoint: [""] à l'image |
| Le projet n'est pas auto-détecté | Remote Git absent ou mal nommé | Vérifiez origin, passez une cible owner/repo ou --gitlab-url / --github-url et --project |
Avertissement contextual score unavailable, scoring-v3 used | Plumber n'a pas pu établir les faits du pipeline | Le score suit le profil par points de sévérité ; le champ profileId du JSON le confirme |
À retenir
Section intitulée « À retenir »- Plumber scanne la sécurité de vos pipelines GitLab CI/CD et GitHub Actions depuis la même CLI ; le composant GitLab et l'action GitHub suivent le même numéro de version (0.7.2)
- Un jeton est requis, même sur un dépôt public : sans lui, l'analyse est incomplète, le score est retenu et le code de sortie vaut 3
- Le schéma v2.0 sépare les contrôles par provider ; une surcouche (
extends: plumber:default) ne liste que vos changements, et ses listes remplacent celles de la base - Le job échoue via le Plumber Score :
--min-points(défaut 100) ou--min-score; codes de sortie 0 réussi, 1 seuil non atteint, 2 erreur, 3 analyse incomplète - Commencez par corriger les images non épinglées (ISSUE-103), les actions non épinglées par SHA (ISSUE-701), les includes mutables (ISSUE-404) et les branches non protégées (ISSUE-501)
- Activez ensuite les protections supply chain : sources autorisées (ISSUE-713), CVE connues (ISSUE-703),
pull_request_target(ISSUE-804) et cache écrit par un déclencheur peu fiable (ISSUE-705) - Épinglez Plumber lui-même : image
getplumber/plumber:0.7.2@sha256:…avecentrypoint: [""], composant@v0.7.2, action par SHA du commit avec# v0.7.2 - Le score se calcule par chemins d'attaque ; seul le prouvé déplace un palier, les findings individuels coûtent 30 points au plus et seul un chemin Critical force le E
- Le PBOM inventorie le pipeline ; le CycloneDX se branche sur Trivy et Grype ;
--sarif,--glsast,--csvet--ocsfalimentent vos tableaux de bord plumber explaindétaille un code hors ligne et, avec-a N, un chemin du dernier scan (valable si le scan en a au moins N)- Plumber Radar montre, sans compte, la posture CI/CD des organisations open source dont vous adoptez les actions ou les images
- 76 codes d'issues en neuf familles ; la liste exacte vient toujours de
plumber explain --listsur votre version
Contrôle de connaissances
Section intitulée « Contrôle de connaissances »Vérifiez que l'essentiel de ce guide est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.
Contrôle de connaissances
Validez vos connaissances avec ce quiz interactif
Informations
- Le chronomètre démarre au clic sur Démarrer
- Questions à choix multiples, vrai/faux et réponses courtes
- Vous pouvez naviguer entre les questions
- Les résultats détaillés sont affichés à la fin
Lance le quiz et démarre le chronomètre
Vérification
(0/0)Score par domaine
Un domaine regroupe plusieurs guides et reçoit plusieurs questions : son score est plus fiable qu'un score par thème, qui porte souvent sur une seule question.
Bilan
Points forts
Aucun sujet réussi à 80 % sur au moins deux questions pour l’instant.
Points faibles
Aucun sujet sous le seuil de réussite.
À faire ensuite
Des indices pour retenter votre chance ?
Continuer le cours : Préparer la certification CI/CD Associate
Continuer le cours : Durcir le runner avec Harden-Runner
Nouveau quiz complet avec des questions aléatoires
Retravailler uniquement les questions ratées
Retour à la liste des certifications
FAQ : questions fréquentes
Section intitulée « FAQ : questions fréquentes »Ces questions reviennent systématiquement avant l'adoption : ce que fait l'outil, ce qu'il coûte, ce qu'il apporte face aux scanners déjà en place, s'il touche à vos fichiers et comment il fait échouer un pipeline.
LICENSE au tag v0.7.2. Vous pouvez donc l'utiliser en ligne de commande et en pipeline sans acheter de licence.La Platform (tableau de bord multi-projets, historique, gouvernance) est un produit distinct, pertinent seulement quand vous pilotez un portefeuille de projets : ses conditions ne relèvent pas de cette licence et ce guide ne les décrit pas.permissions:, protéger une branche) restent à votre main. C'est un détecteur, pas un correcteur automatique.- En CLI :
--min-points(0-100) ou--min-score(A-E). - Dans le composant GitLab : les inputs
min_points/min_score; dans l'action GitHub :min-points/min-score.
plumber analyze --min-points 80 # échoue sous 80 points sur 100
plumber analyze --min-score B # échoue en dessous d'un B
L'option --threshold est dépréciée. Le défaut est min-points: 100 : le moindre finding fait échouer le job avec le code de sortie 1. Une analyse incomplète (jeton absent) sort en code 3.scoring-v4) part de 100 points et retire un prix fixe par chemin d'attaque (Critical 30, High 15, Medium 6, Low 3), puis un prix par finding individuel (Critical 20, High 10, Medium 5, Low 2), plafonné à 30 points au total.- Seul un chemin Critical force le E : il bloque le score à 30 points au plus.
- Seuls les faits prouvés déplacent un palier ; un fait supposé ou inconnu reste affiché sans rien changer.
- Une dépendance seulement mal épinglée est plafonnée à Medium, ou High si sa source n'est pas autorisée.
- Sur nektos/act au commit 4f41128, le scan du 17 octobre 2026 donne 55 points (C).
--score-profile v3applique une formule par points de sévérité de code.
Status: INCOMPLETE) et sort avec le code 3.Exportez GITLAB_TOKEN, GH_TOKEN ou GITHUB_TOKEN, ou ouvrez une session gh auth login.Côté GitHub, créez un jeton fine-grained (Settings > Developer settings > Personal access tokens > Fine-grained tokens) : le droit Administration:read, nécessaire à la protection de branche, n'existe que pour ce type et ne s'obtient que sur un dépôt que vous administrez. Le GITHUB_TOKEN de l'action ne le porte pas : branchMustBeProtected y reste partiel.