Cover for Plumber CLI 0.4 Release - What's New

Plumber CLI 0.4 Release - What's New

Plumber CLI 0.4 is here, and it lands nine new controls, makes the Plumber Score the build gate, and adds a live badge you can push straight from CI.

A bigger control library, a severity-weighted A-E score that gates your build, and a badge to show it off.

Nine new controls, and a deeper GitHub surface

Nine new controls landed this cycle, across GitHub and GitLab. They target the supply-chain and secret-exposure classes seen in real incidents over the past year.

đź”— Third-party action supply chain

ControlProviderWhat it checks
Actions must come from authorized sourcesGitHubRestricts every step-level uses: and job-level reusable-workflow uses: to authorized sources: GitHub-official owners (actions/*, github/*), the scanned repo's own org (trustSameOrgActions, default on), a trustedGithubActions allowlist (exact owner/repo or owner/* wildcard), or a minimumStars popularity floor.
Pinned Upstream Actions must existGitHubFlags a step pinned to a 40-character commit SHA that the upstream repository confirms does not exist. Owner-agnostic (first- and third-party alike); non-SHA refs, local ./… actions, and docker:// images are out of scope. Requires gh / GH_TOKEN.
Actions must not use ambiguous tag/branch refsGitHubGitLabFlags uses: owner/repo@ref references that resolve upstream as both a tag and a branch. API-backed: stays silent when the upstream probe cannot run.
Release workflows must not restore an untrusted cacheGitHubFlags a release or publish job that restores a build cache (actions/cache, actions/cache/restore, or a setup-* action's built-in cache) with a key not scoped to the release ref. Release context is a release trigger or a canonical publish action such as pypa/gh-action-pypi-publish or softprops/action-gh-release.

🔑 Secret exposure and untrusted code

ControlProviderWhat it checks
pull_request_target must not check out the PR headGitHubFlags workflows on the pull_request_target trigger that check out the PR head (github.event.pull_request.head.sha or head_ref): the PR author's code then runs with the base repository's secrets and a write-scoped GITHUB_TOKEN. A job-level same-repository guard silences the finding, since fork code never runs.
Workflow must not write untrusted content to $GITHUB_ENVGitHubFlags a run: step where attacker-controlled free text (same field list as ISSUE-207) reaches $GITHUB_ENV or $GITHUB_PATH: inlined directly, dereferenced from an env:-bound shell variable, or written through a heredoc body (sinks: >>, >, tee).
Workflows must not expose all secrets at onceGitHubFlags a job that serialises the whole secrets context with toJson(secrets) or toJSON(secrets) into a run: script, an env: binding, or an action with: input. Scoped references like ${{ secrets.NPM_TOKEN }} are the intended pattern and stay silent.
Workflow must not execute unverified scriptsGitHubGitLabDetects workflow steps that download and immediately execute scripts (curl | bash, wget | sh, download-then-execute, base64 pipe-to-shell) without integrity verification.

The full control list, with per-control configuration and an issue page for every code, lives in the controls reference for GitHub and GitLab.

Sharper detection

Plumber did not only add controls this cycle. Existing ones got more precise, which matters just as much: a control that cries wolf trains people to ignore it.

  • Fewer false positives. Dangerous-trigger detection recognizes fork guards and author-association checks, template-injection is scoped to free-text event fields, and the curl | interpreter check flags a real remote fetch target rather than any piped local variable.
  • A smarter advisory filter. The known-CVE check resolves SHA-pinned action refs to a version, and treats a moving major tag such as v4 as the version span it can float across rather than a single point.
  • A hardened CLI. Configuration and CI-file reads are bounded, and terminal and merge-request output is escaped, so a crafted repository cannot corrupt a run or an operator’s terminal.

Every new control is a Rego rule on the same OPA policy engine Plumber already uses, so it inherits the shared scoring model, JSON output, and issue codes with no new machinery.

The Plumber Score is the gate now

0.4 makes the Plumber Score the gate: severity-weighted points from 0 to 100, an A-E letter, and a malus for any critical issue. It reflects real risk, not a count of checks that passed.

  • Gate on the score. Use --min-points (0-100) or --min-score (A-E). The default still fails on any finding, but the failure reason is now a real risk number.
  • passed means the score gate was met. The JSON report and the GitHub Action drop the old percentage; the default artifact name is plumber-report.
  • A run with nothing to score fails closed. An empty or provider-mismatched .plumber.yaml, a --skip-controls filter that removes every check, or a GitLab project with no pipeline file, fails instead of scoring a perfect 100 off zero checks. Where that is expected, use soft-fail to keep the job green.

This is the one breaking change in 0.4.0.

INFO

The full points model, severity weights, and the critical malus are documented on the Plumber Score page.

Push your score, get a live badge

Scores are more useful when they are visible. 0.4 adds score push: when Plumber runs in CI, it can publish the default branch’s score to the hosted service at score.getplumber.io and hand you a self-updating A-E badge for your README. It is the only way to get an official Plumber Score.

  • No secret to manage. Publishing uses CI-native OIDC, so there is no token to store or rotate.
  • CI only. A local plumber analyze never publishes, and a scan targeting a foreign repo is skipped automatically.
  • Opt-in and off by default. Enabling it makes your score and repository name public, and only the default branch’s score is shown.

Turning it on is one input in your pipeline plus a badge line in your README. The step-by-step for both GitHub and GitLab lives in the Plumber Score docs. Here is a published score in the wild:

Plumber Score

⬆️ How to update

  • CLI: brew update && brew upgrade plumber, or install via mise, release binaries, or Docker.
  • GitHub Actions: bump the Plumber action; to publish a badge, add permissions: id-token: write and set score-push: true.
  • GitLab CI: bump to the latest component catalog entry; set score_push: true to publish.
  • Gates: if you pinned a --threshold, move to --min-points / --min-score; the deprecated flag still works in the meantime. A run where no control evaluates (an empty config or a skip-all filter) fails closed, so use soft-fail (findings still reported, job stays green) where that is intentional.
  • Config: run plumber config generate or plumber config init to pick up the new controls, then tune per provider.
  • Verify binaries: deploying release binaries to production? Verify their SLSA attestations first. See verifying release binaries.

INFO

Versions