Skip to content
Use this GitHub action with your project
Add this Action to an existing workflow or create a new one
View on Marketplace

Latest commit

 

History

932 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Plumber

CI/CD security scanner for GitLab CI and GitHub Actions
One CLI, one .plumber.yaml, one Rego policy engine.

Plumber Score    OpenSSF Scorecard    SLSA 3

Build Status Latest Release Go Version GitHub Downloads Docker Pulls License

WebsiteDocsDiscordIssues


What Is Plumber?

Plumber scans CI/CD pipelines for risky patterns and security gaps.

  • GitLab CI: reads .gitlab-ci.yml, resolved includes, and repository settings.
  • GitHub Actions: reads .github/workflows/*.{yml,yaml} locally or through the GitHub API.
  • One config: .plumber.yaml contains provider-specific policy sections for GitLab and GitHub.

Plumber reports findings in the terminal, JSON, SARIF, GitLab SAST, CSV, OCSF, PBOM, and CycloneDX formats.

Plumber GitLab CI component running

Start Here

Run your first scan before reading the full docs.

brew tap getplumber/plumber
brew install plumber

plumber config generate # generate the default configuration file
plumber analyze

See the generated default config in this repo: defaultConfig/.plumber.yaml.

Plumber auto-detects the provider from your git remote. Use explicit flags when scanning a repo that is not the current checkout.

Choose Your Path

I want to... Use this Start here
Try Plumber locally CLI plumber analyze
Add checks to GitLab CI GitLab CI Component GitLab CI Component
Add checks to GitHub Actions GitHub Action GitHub Action
Audit many repos from a script CLI + JSON/SARIF Outputs
Tune policy rules .plumber.yaml Configuration

Local CLI

Install

brew tap getplumber/plumber
brew install plumber

Other options:

  • mise use -g github:getplumber/plumber
  • Download a binary from GitHub Releases
  • Run the Docker image: getplumber/plumber

Full install docs:

Authenticate

GitLab:

export GITLAB_TOKEN=glpat_xxxx

GitHub (preferred, uses the gh CLI's keyring):

gh auth login

Alternative (CI runners, automation):

export GH_TOKEN=ghp_xxxx

Local GitHub scans can run without a token for workflow-content checks. A token enables repo-level and action-metadata checks.

If a workflow uses an action hosted in an org with an IP allow list (which blocks the runner's GITHUB_TOKEN), set PLUMBER_METADATA_TOKEN to a token with public-repository read so Plumber can still resolve that action's version for the known-CVE check. Without it, Plumber falls back to an anonymous read and, if that is rate-limited too, skips the version check rather than guessing.

Run

Current repo:

plumber analyze

Specific GitLab project:

plumber analyze \
  --provider gitlab \
  --gitlab-url https://gitlab.com \
  --project group/project

Specific GitHub repo without a local clone:

plumber analyze \
  --provider github \
  --github-url github.com \
  --project owner/repo

GitHub Action

  1. Add the official Plumber action to .github/workflows/plumber.yml:
    name: Plumber
    
    on:
      pull_request:
      push:
        branches: [main]
    
    permissions:
      contents: read
      security-events: write
      # id-token: write   # uncomment to enable score-push below
    
    jobs:
      plumber:
        runs-on: ubuntu-24.04
        steps:
          - uses: actions/checkout@v6
          - uses: getplumber/plumber@<version>
            with:
              # Set to `true` to publish an official Plumber score badge
              # (it makes your score and repo name public, see Score Push section below)
              score-push: false

To resolve action versions hosted in an org with an IP allow list, pass a public-repo-read token via the metadata-token input (kept in a secret):

        with:
          metadata-token: ${{ secrets.PLUMBER_METADATA_TOKEN }}

Full guide: getplumber.io/docs/cli/github#run-with-github-actions

GitLab CI Component

  1. Add the official Plumber component to .gitlab-ci.yml:
    include:
      - component: gitlab.com/getplumber/plumber/plumber@<version>
        inputs:
          # Set to `true` to publish an official Plumber score badge
          # (it makes your score and repo name public, see Score Push section below)
          score_push: false
  2. Add GITLAB_TOKEN in Settings -> CI/CD -> Variables. Use read_api + read_repository for scanning, or api if you want Plumber to post MR comments or badges.

Full guide: getplumber.io/docs/cli/gitlab#run-with-the-gitlab-ci-component

Score Push

Enabling score push publishes a self-updating A–E badge to the hosted score service. It's the only way to get an official Plumber score. It's only available in CI, not when running the CLI locally.

Display it with a badge in your README (swap in your platform/owner/repo):

[![Plumber Score](https://score.getplumber.io/github.com/OWNER/REPO.svg)](https://score.getplumber.io/github.com/OWNER/REPO)

⚠️ Opt-in and off by default. Enabling it makes your score and repository name public. Only the default branch's score is displayed. See score docs.

Common analyze flags

Flag Purpose
--score-endpoint Score service base URL (default https://score.getplumber.io). Override only for a self-hosted score service.
--platform Plumber platform base URL. Setting it turns on platform mode (see below) and pushes this run's full results there over CI OIDC, taking precedence over --score-push. Requires an id-token grant: permissions: id-token: write on GitHub, the component's id_tokens: block on GitLab.

Platform mode

Without --platform nothing changes: Plumber collects everything itself and evaluates one policy, exactly as it always has.

With --platform, Plumber first reads the project's context from the platform - the resolved policy set and a cached settings snapshot - and uses it to decide what to collect and what to report:

  • One result per policy. The platform's policy set decides how many results the run produces, each keyed to its own policy and carrying its own score. Policies that share a control configuration are evaluated once.
  • The CI configuration comes from the platform. Resolving include: directives needs an API a CI job token cannot reach, so platform mode reads the resolved configuration from the platform instead of asking the git host itself.
  • So do the project settings. Branch protections, merge-request approval rules and settings, and CI/CD variable metadata are read from the snapshot rather than collected per run. That is what stops a project scanned once per policy file from re-fetching the same settings once per policy file. Variable values are never served and never needed: the controls read the protected and masked flags, not the secrets.
  • A CI job needs no GitLab token. Platform mode is built for CI, and a job already has what the rest would have been fetched for: its own identity in the predefined CI_* variables, its checkout, and its environment. Set --platform and Plumber runs without a GITLAB_TOKEN. A few checks still read the projects your pipeline includes from, and without a token those report not_evaluable rather than passing.
  • Your branch is evaluated against its own configuration. Plumber hashes the CI config in the checkout - the root file plus every local include - and compares it to what the platform's snapshot was resolved from. A branch that does not touch CI config matches and reuses the snapshot at no extra cost; a branch that does changes gets its own resolution from the platform.
  • Controls whose data is unavailable report not_evaluable, never a pass. If the platform cannot resolve a configuration, or reports a settings collection as failed, the controls that read it say so instead of reporting a clean result over data nobody collected. A lane the platform vouches for as genuinely empty is still a real verdict a control may fail on. A run prints which configuration it used and why, with no --verbose needed.

Platform mode reports less when data is missing, never something different: a run that cannot evaluate a control says so.

The platform can gate the run: if it returns a blocking decision for this push, the job exits 1 with a line naming every blocking policy. A platform that is down, slow, or erroring never blocks: the gate only ever fails open, and the two sentences below are exact so you can alert on them:

  • gate unavailable, letting through: the platform itself is unreachable (timeout, connection error, 5xx). The run proceeds.
  • gate NOT RUN: authentication/configuration failed: the request reached the platform but was rejected (expired/invalid token, misconfigured project). The run proceeds.

A third, informational line is deliberately distinct from both, so alerts on the sentences above stay precise: platform returned no gate verdict, letting through means the push was accepted (2xx) but the response carried no usable gate decision - typically a platform version that predates the gate. Routine during a platform rollout; the run proceeds.

Configuration

Plumber reads .plumber.yaml.

Create a config interactively:

plumber config init

Generate the full commented default template:

plumber config generate

Example:

version: "2.0"

gitlab:
  controls:
    containerImageMustNotUseForbiddenTags:
      enabled: true

github:
  controls:
    actionsMustBePinnedByCommitSha:
      enabled: true
      trustedOwners:
        - actions
        - github

Useful commands:

plumber config validate
plumber config view
plumber config diff
plumber explain ISSUE-411

Full config reference:

Overlay configuration

Extend Plumber's baseline and list only what you change:

extends: plumber:default
version: "2.0"
github:
  controls:
    githubActionMustComeFromAuthorizedSources:
      includePlumberDefaults: true # keep the curated trusted orgs, default
      trustedGithubActions:
        - myorg

Everything else is inherited, and new controls Plumber ships appear automatically. For allowlist controls, includePlumberDefaults: true unions your entries with Plumber's list, false uses only your own. Run plumber config generate --overlay for a starter, and plumber config resolve to print the full effective config.

Controls

Plumber ships controls for:

  • container image pinning and authorized sources
  • branch protection
  • GitLab merge request approval rules (minimum approvals, coverage of all protected branches) and approval settings (author/committer approval, per-MR overrides, re-authentication, approval reset)
  • GitLab CI/CD settings variables that must be protected and masked
  • unverified script execution (curl | bash, base64 -d | bash, etc.)
  • Docker-in-Docker
  • weakened security jobs
  • unsafe variable expansion
  • GitHub action pinning, archived actions, ref confusion, impostor commits, and known CVEs
  • dangerous GitHub triggers and overbroad permissions

Full catalogs:

Outputs

Output Flag Use it for
Terminal default Human review during local or CI runs
JSON --output results.json Automation and dashboards
SARIF --sarif results.sarif GitHub Code Scanning and SARIF-compatible tools
GitLab SAST --glsast gl-sast-report.json GitLab Security Dashboard / MR widget
CSV --csv results.csv Spreadsheet tools, ad-hoc analysis
OCSF --ocsf plumber.ocsf.json OCSF consumers and GRC platforms (Compliance Finding, schema 1.8.0)
PBOM --pbom pbom.json Pipeline inventory
CycloneDX --pbom-cyclonedx cdx.json SBOM tooling

Plumber terminal output

Example:

plumber analyze \
  --output results.json \
  --sarif results.sarif \
  --csv results.csv \
  --pbom pbom.json \
  --pbom-cyclonedx cdx.json

Artifacts without a verdict

To produce an artifact (typically the PBOM) and nothing else, pass --no-controls. No control is evaluated, no score is computed, and the run exits 0 as long as data collection succeeded:

plumber analyze --pbom-cyclonedx cdx.json --no-controls --print=false

It overrides whatever .plumber.yaml enables, so the same config keeps working for a normal plumber analyze in another job. Nothing the run produces claims a verdict: the JSON, CSV and OCSF reports mark every control skipped rather than passed, and the PBOM records the collected inventory (images, includes, upstream versions) with no compliance flags on it, per-image or per-include.

The run still fails (exit 3) when data collection did not produce a usable pipeline: a degraded collection, a .gitlab-ci.yml that was fetched but does not parse, or (on GitLab) no CI configuration at all. In each case the inventory would be empty, and an empty PBOM must not ship as a complete one.

Flags that read or publish a verdict are ignored, with a notice naming them: --min-points, --min-score, --threshold, --badge, --score-push, --mr-comment, --platform, and also --sarif / --glsast. Those last two are security reports with no honest empty form: an empty SARIF is what makes GitHub Code Scanning clear previously-reported alerts, and an empty GitLab SAST report shows a clean Security Dashboard. Writing them from a run that evaluated nothing would dismiss real alerts, so they are skipped rather than emitted empty.

More details:

Exit Codes

Code Meaning
0 The Plumber Score meets the gate (--min-points / --min-score), or --no-controls was used and data collection succeeded
1 The Plumber Score is below the gate (or the deprecated --threshold is not met)
2 Invalid usage, configuration, or a runtime / provider / auth / network failure
3 A check could not be verified and --fail-warnings is set (e.g. an action version that could not be resolved)

Self-Hosted GitLab

If you run a self-hosted GitLab instance, host or mirror the Plumber component inside your instance, publish a release, and include that component URL from your pipelines.

Guide: getplumber.io/docs/cli/gitlab#hosting-on-self-hosted-gitlab

Troubleshooting

Problem What to check
GITLAB_TOKEN environment variable is required Export GITLAB_TOKEN or add it as a CI/CD variable
GitHub upstream scan refuses to start Set GH_TOKEN, GITHUB_TOKEN, or run gh auth login
No GitHub repo-level findings Local GitHub scans soft-degrade without token/API scope
Config warnings Run plumber config validate
Need to inspect a finding Run plumber explain ISSUE-XXX

More help:

Development

Build locally:

make build

Run tests:

make test

Contributing guide: CONTRIBUTING.md

Resources

License

Plumber is licensed under the Mozilla Public License 2.0.

About

Plumber detects CI/CD security issues in your GitHub workflows and gives you a score

Topics

Resources

Contributing

Security policy

Stars

785 stars

Watchers

4 watching

Forks

Releases

Used by

Contributors

Languages