Skip to main content

GitHub

This page covers the GitHub Actions particulars of the Plumber CLI: how to authenticate, the controls that ship for GitHub, the issues they emit, a complete github.controls: configuration block, and the drop-in GitHub Action for automated compliance scanning with SARIF upload. For installation, see the Installation page. For the provider-agnostic command reference, see the main CLI page.

On GitHub Actions, Plumber flags issues like:

  • Third-party actions not pinned by commit SHA, the tj-actions / reviewdog supply chain vector (CVE-2025-30066)
  • Actions hosted in archived repositories or carrying published GitHub Advisory entries
  • Dangerous triggers (pull_request_target, workflow_run) that expose secrets to PR-author code
  • Template injection via ${{ github.event.* }} inlined into shell scripts
  • Reusable workflows called with secrets: inherit instead of named-secret passing
  • Implicit / over-broad permissions: blocks and permissions: write-all grants
  • Debug-trace toggles (ACTIONS_STEP_DEBUG, ACTIONS_RUNNER_DEBUG) that leak secrets into job logs
  • Docker-in-Docker services and weakened security jobs

Quick Start

Two ways to scan a GitHub repository. Pick the path that matches how you work: Local CLI or GitHub Action.

Run the CLI locally

Best for trying Plumber on one repo, checks before you push, security-team audits, or scanning upstream repos without changing CI.

  1. Install Plumber (Installation): Homebrew, mise, a prebuilt binary, Docker, or from source.

  2. Authenticate: run gh auth login, or set GH_TOKEN (see Authentication below).

  3. Run the scan from inside your checked-out repo, or against a remote one (see Running a scan):

    Terminal window
    plumber analyze
  4. Read the report: a per-control compliance breakdown and a letter grade, plus an optional JSON report, PBOM, and CycloneDX SBOM (see Example Output).

Installation

Authentication

Pick whichever auth flow fits your environment:

Terminal window
# Option 1: GitHub CLI (recommended for local use)
gh auth login
# Option 2: Fine-grained Personal Access Token
# Settings > Developer settings > Personal access tokens > Fine-grained tokens
# Repository access: pick the repo(s) to scan
# Permissions: Contents = Read, Metadata = Read, Administration = Read
export GH_TOKEN=github_pat_xxxx
# Option 3: Classic PAT (broader scope, still works)
# Permissions: `repo` scope (read access to repo + admin metadata)
export GH_TOKEN=ghp_xxxx

If a workflow uses an action hosted in an org with an IP allow list, a runner's GITHUB_TOKEN is blocked when Plumber resolves that action's version for the known-CVE check; Plumber falls back to an anonymous read, and skips the check rather than guessing if that is rate-limited too. To resolve those versions reliably, set PLUMBER_METADATA_TOKEN (or the action's metadata-token input) to a token with public-repository read.

Caution

Administration: Read (fine-grained) or repo scope (classic) is needed for the branchMustBeProtected rule to evaluate force-push and code-owner-approval settings. Without it the rule abstains and Plumber reports the abstention explicitly in partialControls rather than claiming a false 100% pass.

Info

branchMustBeProtected reads both classic Branch Protection (Settings > Branches) and Repository / Organization Rulesets (Settings > Rules > Rulesets). Rules from either mechanism are unioned, stricter wins. A code-owner-approval rule defined only in a Ruleset is honored.

When a token-scoped control cannot fully evaluate, Plumber adds a partialControls entry to report.json so CI gates can tell the difference between "100% compliant" and "100% on what we could see":

"partialControls": [
{
"control": "branchMustBeProtected",
"reason": "Token lacks Administration:Read scope; force-push and code-owner-approval rules (ISSUE-505) not evaluated.",
"affectedBranches": 1,
"remediation": "Re-run with a token carrying Administration:Read (fine-grained PAT) or `repo` scope (classic PAT)."
}
]

When this array is non-empty, at least one control abstained on at least part of its input. Treat the run as suspect rather than trusting the compliance percentage. On a clean run the array is either omitted or empty.

Running a scan

Terminal window
# Local clone (auto-detected from git remote)
plumber analyze
# Upstream-fetch: scan a repo without cloning it
plumber analyze --github-url github.com --project myorg/myrepo

On GitHub Enterprise Server, pass the GHES host via --github-url ghes.example.com. Plumber auto-detects github.com from your git remote.

Command Reference

plumber analyze

The main command for analyzing GitHub Actions workflows.

Terminal window
plumber analyze [flags]

Flags

FlagRequiredDefaultDescription
--gitlab-urlNo*auto-detectGitLab instance URL (e.g., https://gitlab.com). Mutually exclusive with --github-url.
--github-urlNo*auto-detectGitHub host (e.g., github.com or ghes.example.com). Mutually exclusive with --gitlab-url.
--projectNo*auto-detectProject / repo path. GitLab: group/project. GitHub: owner/repo.
--configNo.plumber.yamlPath to configuration file
--thresholdNo100Minimum compliance % to pass (0-100)
--branchNoProject defaultBranch to analyze
--outputNo-Write JSON results to file
--pbomNo-Write PBOM (Pipeline Bill of Materials) to file
--pbom-cyclonedxNo-Write PBOM in CycloneDX SBOM format
--printNotruePrint text output to stdout
--mr-commentNofalsePost/update a compliance comment on the merge request (MR pipelines only; requires api scope)
--badgeNofalseCreate/update a Plumber compliance badge on the project (requires api scope; only runs on default branch)
--controlsNo-Run only listed controls (comma-separated). Cannot be used with --skip-controls
--skip-controlsNo-Skip listed controls (comma-separated). Cannot be used with --controls
--fail-warningsNofalseFail on warnings: configuration warnings such as unknown keys (exit 2) and "could not verify" warnings such as a skipped known-CVE check (exit 3)
--ci-config-pathNoauto-detectOverride CI configuration file path. Defaults to project CI config path from GitLab settings (usually .gitlab-ci.yml)
--verbose, -vNofalseEnable verbose/debug output

Info

* Auto-detected from git remote (requires origin) if not specified. Supports both SSH and HTTPS remote URLs.
* You can always override with --gitlab-url and --project

Environment Variables

VariableRequiredDescription
GH_TOKEN / GITHUB_TOKENGitHub onlyGitHub API token. Fine-grained PAT needs Contents: Read, Metadata: Read, Administration: Read. Classic PAT needs repo. Alternatively, run gh auth login and Plumber will pick up the gh CLI credential.
PLUMBER_METADATA_TOKENNoToken used only to resolve third-party action versions for the known-CVE check (ISSUE-703). Set it when an action is hosted in an org with an IP allow list that blocks the runner's GITHUB_TOKEN; a public-repository read scope is enough. Takes precedence over GH_TOKEN for that lookup. When unset, Plumber falls back to an anonymous read.
PLUMBER_NO_UPDATE_CHECKNoSet to any value (e.g., 1) to disable the automatic version check.

For the configuration commands (plumber config init / generate / migrate / view / validate), plumber explain, and exit codes, see the main CLI page. They behave identically for both providers.

Usage Examples

Save JSON Output

Terminal window
docker run --rm \
-e GH_TOKEN=ghp_xxxx \
-v $(pwd):/output \
getplumber/plumber:latest analyze \
--github-url github.com \
--project myorg/myrepo \
--branch main \
--config /.plumber.yaml \
--threshold 100 \
--output /output/results.json

Silent Mode (JSON Only)

Terminal window
plumber analyze \
--github-url github.com \
--project myorg/myrepo \
--config .plumber.yaml \
--threshold 100 \
--output results.json \
--print false

Example Output

The CLI output is color-coded in your terminal for easy scanning: green for passing controls, red for failures.

Tip

When using --output, results are saved as JSON for programmatic access and CI/CD integration.

Plumber CLI output showing compliance results

Available Controls

Plumber ships 16 GitHub controls (14 default-on plus the opt-in workflowMustIncludeRequiredActions and pipelineMustNotLeakSecretsInConfig). Each can be enabled / disabled and customized in .plumber.yaml. When a control detects a violation, it creates an Issue (e.g., ISSUE-701) with a direct link to its documentation page.

ControlIssuesDescription
Container images must not use forbidden tagsISSUE-102Flags mutable tags (latest, dev, …); can require all images be pinned by digest
Pipeline must not enable debug traceISSUE-203Flags truthy ACTIONS_STEP_DEBUG / ACTIONS_RUNNER_DEBUG in literal env:, ${{ }} expression bindings, and runtime >> $GITHUB_ENV script writes
Workflow must not inline user input into shell scriptsISSUE-207Flags ${{ github.event.* }}, ${{ github.head_ref }}, ${{ github.actor }} interpolated directly into a run: shell (OWASP CICD-SEC-1)
Reusable workflow must not use secrets: inheritISSUE-302Flags jobs.<name>.secrets: inherit, which forwards every caller secret to the reusable workflow. Use an explicit secrets map
Workflow must not leak secrets in configurationISSUE-301Opt-in. Pipes every file under .github/workflows/ through gitleaks; flags hardcoded API tokens, private keys, and other credentials. Each finding carries only a redacted preview
Security jobs must not be weakenedISSUE-410Flags CodeQL / dependency-review / gitleaks / trufflehog / OSV-Scanner jobs neutralized via continue-on-error: true or manual-only dispatch (OWASP CICD-SEC-4)
Workflow must not execute unverified scriptsISSUE-411Detects curl | bash, wget | sh, download-then-execute, redirect-then-execute, and base64 pipe-to-shell patterns in run: steps without integrity verification (OWASP CICD-SEC-3)
Workflow must not use Docker-in-DockerISSUE-412 ISSUE-413Flags docker:dind services and insecure daemon configuration (TLS disabled, plaintext port 2375)
Workflows must include required actionsISSUE-417Opt-in. Asserts every workflow includes the action(s) your org requires (DNF: outer OR, inner AND), resolving against step-level and reusable-workflow uses:
Branch must be protectedISSUE-501 ISSUE-505Verifies default / named branches have force-push disabled and code-owner approval. Reads classic Branch Protection AND Repository / Organization Rulesets
Third-party actions must be pinned by commit SHAISSUE-701Requires uses: owner/repo@<sha> (40-char commit SHA) instead of a mutable tag or branch ref. trustedOwners exempts first-party actions/* and github/*
Actions must not reference archived repositoriesISSUE-702Flags uses: refs whose upstream GitHub repository has been archived (no security patches coming). One cached API call per unique ref
Actions must not carry known CVEsISSUE-703Cross-references every pinned action against the GitHub Advisory Database (actions ecosystem), semver-filtered so refs past the fix stay silent
Workflow permissions must be declaredISSUE-801Flags workflows missing an explicit permissions: block (falls back to the repo-wide GITHUB_TOKEN default)
Workflow must not use dangerous triggersISSUE-802Flags pull_request_target and workflow_run, which run with base-repo secrets while being influenceable by an unprivileged caller
Job must not run with write-all permissionsISSUE-803Flags permissions: write-all at workflow or job scope. Pairs with workflowsMustDeclarePermissions to close the least-privilege loop
Security Job Weakening Detection (GitHub)

A security job can be silently neutralized while the workflow still shows a green check. This control flags that. Maps to OWASP CICD-SEC-4 (Poisoned Pipeline Execution).

Plumber identifies a security job when its name matches one of the globs in securityJobPatterns. On GitHub the name is <workflow-file-basename-without-.yml>/<job-id> (for example codeql-analysis/analyze for .github/workflows/codeql-analysis.yml + jobs.analyze). The <workflow>/ prefix exists so two workflow files defining a job with the same id never collide.

Four useful pattern shapes:

PatternMatchesWhen to use
*<token>*The token anywhere in the nameYou don't know which workflow file will host the job
<workflow>/*Every job in one workflow fileAll security jobs live in security.yml
*/<jobid>Specific job id in any workflowJob id is canonical (e.g. codeql) but file location varies
<workflow>/<jobid>Exact match, no wildcardYou control both names and want strict matching
github:
controls:
securityJobsMustNotBeWeakened:
enabled: true
securityJobPatterns:
- "*codeql*"
- "*dependency-review*"
- "*trufflehog*"
- "*gitleaks*"
- "*osv-scanner*"
- "*-sast"
- "*-sast-*"
- "*-scan"
- "*scan*"
- "*-security"
- "*-security-*"
- "*-audit"
- "*-audit-*"
# Real-world slash-form examples (commented; uncomment and
# adapt to your repo for a tighter match than the wildcards
# above). Format: <workflow-filename-without-extension>/<job-id>.
# - codeql-analysis/analyze # GitHub's default CodeQL template
# - dependency-review/dependency-review # GitHub's default Dependency Review template
# - security/gitleaks # gitleaks job in security.yml
# - security/trufflehog # trufflehog job in security.yml
# - security/* # every job in security.yml
# - "*/sast" # any job named "sast", any workflow
# - ci/semgrep-sast # exact match, no wildcard
allowFailureMustBeFalse:
enabled: true
rulesMustNotBeRedefined:
enabled: true
whenMustNotBeManual:
enabled: true
Unverified Script Execution Detection (GitHub)

Detects workflow run: steps that download and immediately execute scripts from the internet without integrity verification. This is the GitHub Actions counterpart of the GitLab control and targets the Megalodon attack pattern: obfuscated base64 payloads piped to a shell (echo "…" | base64 -d | bash) as well as classic curl | bash / wget | sh chains. Maps to OWASP CICD-SEC-3 (Dependency Chain Abuse) and CICD-SEC-8 (Ungoverned Usage of 3rd Party Services).

Detected patterns:

  • Direct pipe to shell: curl ... | bash, wget ... | sh, curl ... | python, and any <command> | <shell> chain
  • Download-and-execute: curl -o script.sh ... && bash script.sh
  • Download-redirect-execute: curl ... > install.sh; sh install.sh
  • Inline obfuscation: echo "..." | base64 -d | sh

Lines that include checksum or signature verification (sha256sum, gpg --verify, cosign verify, and similar) on the same line as the download are excluded.

github:
controls:
pipelineMustNotExecuteUnverifiedScripts:
enabled: true
trustedUrls: []
# - https://internal-artifacts.example.com/*

Add trusted URL patterns to trustedUrls (host-precise globs) to suppress findings for known-good sources.

Docker-in-Docker Detection (GitHub)

Flags jobs that spin up a docker:dind service. A Docker daemon inside a CI container on a shared runner in privileged mode enables container escape, lateral movement, and access to secrets from other jobs on the same runner.

When detectInsecureDaemon is enabled (default: true), the control also flags jobs where TLS is disabled (DOCKER_TLS_CERTDIR="") or the Docker host uses the plaintext port (tcp://docker:2375).

github:
controls:
pipelineMustNotUseDockerInDocker:
enabled: true
detectInsecureDaemon: true

Issues

The CLI reports Issues using identifiers like ISSUE-XXX. Each links to a documentation page with a description, the impact, and before/after fix examples. You can also inspect a code locally:

Terminal window
plumber explain ISSUE-703
# shorthand:
plumber explain 703
IssueTitle
ISSUE-102Forbidden container image tag
ISSUE-103Container image not pinned by digest (sub-option of ISSUE-102)
ISSUE-203Workflow enables runner debug logging
ISSUE-207Workflow inlines user input into shell scripts
ISSUE-302Reusable workflow called with secrets: inherit
ISSUE-301Secret leak in workflow configuration
ISSUE-410Security job weakened
ISSUE-411Unverified script execution
ISSUE-412Docker-in-Docker service detected
ISSUE-413Docker-in-Docker with insecure daemon configuration
ISSUE-417Required action or reusable workflow is missing
ISSUE-501Branch protection missing
ISSUE-505Branch protection configuration not compliant
ISSUE-701Third-party action not pinned by commit SHA
ISSUE-702Action hosted in an archived repository
ISSUE-703Action carries a known security advisory (CVE)
ISSUE-801Workflow permissions not declared
ISSUE-802Workflow uses a dangerous trigger
ISSUE-803Workflow grants write-all permissions

Example Configuration

The github.controls: section of a schema v2 .plumber.yaml:

version: "2.0"
github:
controls:
# Third-party action references must be pinned by 40-character commit SHA.
# trustedOwners is the exemption list; first-party `actions/*` and
# `github/*` are exempt by default.
actionsMustBePinnedByCommitSha:
enabled: true
trustedOwners:
- actions
- github
# uses: owner/repo@ref pointing at an archived GitHub repository.
actionsMustNotBeArchived:
enabled: true
# Cross-references every pinned action against the GitHub Advisory
# Database under the `actions` ecosystem.
actionsMustNotCarryKnownCVEs:
enabled: true
# Same forbidden-tag list as GitLab plus a digest-pinning sub-option.
containerImageMustNotUseForbiddenTags:
enabled: true
tags:
- latest
- dev
- development
- staging
- main
- master
containerImagesMustBePinnedByDigest: true
# Truthy ACTIONS_STEP_DEBUG / ACTIONS_RUNNER_DEBUG in any merged env
# block, expression binding, or runtime $GITHUB_ENV write.
pipelineMustNotEnableDebugTrace:
enabled: true
forbiddenVariables:
- ACTIONS_STEP_DEBUG
- ACTIONS_RUNNER_DEBUG
# Docker-in-Docker services + insecure daemon configuration
# (DOCKER_TLS_CERTDIR="" or DOCKER_HOST tcp://...:2375).
pipelineMustNotUseDockerInDocker:
enabled: true
detectInsecureDaemon: true
# `jobs.<name>.secrets: inherit` hands every secret visible to the
# caller (repo, organisation, environment) to the reusable workflow.
# Declare each secret explicitly instead.
reusableWorkflowsMustNotInheritSecrets:
enabled: true
# See the Security Job Weakening Detection section above for the full
# naming-format reference and slash-form pattern examples.
securityJobsMustNotBeWeakened:
enabled: true
securityJobPatterns:
- "*codeql*"
- "*dependency-review*"
- "*trufflehog*"
- "*gitleaks*"
- "*osv-scanner*"
- "*-sast"
- "*-sast-*"
- "*-scan"
- "*scan*"
- "*-security"
- "*-security-*"
- "*-audit"
- "*-audit-*"
allowFailureMustBeFalse:
enabled: true
rulesMustNotBeRedefined:
enabled: true
whenMustNotBeManual:
enabled: true
# curl | bash, wget | sh, download-then-execute, base64 pipe-to-shell.
pipelineMustNotExecuteUnverifiedScripts:
enabled: true
trustedUrls: []
# - https://internal-artifacts.example.com/*
# `${{ github.event.* }}`, `${{ github.head_ref }}` or `${{ github.actor }}`
# interpolated directly into a `run:` shell. Bind through env: first.
workflowMustNotInjectUserInputInScripts:
enabled: true
# `pull_request_target` and `workflow_run` run with the base repo's
# secrets while being influenceable by an unprivileged caller.
workflowMustNotUseDangerousTriggers:
enabled: true
# Workflows without an explicit `permissions:` block fall back to the
# repo-wide GITHUB_TOKEN default. Declare `permissions: { contents: read }`
# at the workflow level for least privilege.
workflowsMustDeclarePermissions:
enabled: true
# `permissions: write-all` at workflow or job scope.
workflowMustNotGrantPermissionsWriteAll:
enabled: true
# Opt-in. Assert every workflow includes the action(s) your org requires.
workflowMustIncludeRequiredActions:
enabled: false
# requiredGroups:
# - ["actions/attest-build-provenance"]
# - ["your-org/license-scan", "your-org/sbom"]
# Reads both classic Branch Protection and Repository / Organization
# Rulesets, unions them, stricter wins.
branchMustBeProtected:
enabled: true
defaultMustBeProtected: true
namePatterns:
- main
- master
- release/*
- production
- dev
allowForcePush: false
codeOwnerApprovalRequired: true

See the full configuration reference for every option, the Installation page, and the main CLI page for the provider-agnostic commands and output formats.

Selective Control Execution

You can run or skip specific controls using their YAML key names from .plumber.yaml. This is useful for iterative debugging or targeted CI checks.

Terminal window
# Only check SHA pinning and declared permissions
plumber analyze --controls actionsMustBePinnedByCommitSha,workflowsMustDeclarePermissions
# Run everything except the advisory-database check
plumber analyze --skip-controls actionsMustNotCarryKnownCVEs

Controls not selected are reported as skipped in the output. The --controls and --skip-controls flags are mutually exclusive.

Valid GitHub control names
Control Name
actionsMustBePinnedByCommitSha
actionsMustNotBeArchived
actionsMustNotCarryKnownCVEs
branchMustBeProtected
containerImageMustNotUseForbiddenTags
pipelineMustNotEnableDebugTrace
pipelineMustNotExecuteUnverifiedScripts
pipelineMustNotUseDockerInDocker
reusableWorkflowsMustNotInheritSecrets
securityJobsMustNotBeWeakened
workflowMustIncludeRequiredActions
workflowMustNotGrantPermissionsWriteAll
workflowMustNotInjectUserInputInScripts
workflowMustNotUseDangerousTriggers
workflowsMustDeclarePermissions

GitHub Action

Use this path for automated compliance on every push or pull request, SARIF in Code Scanning, and no Plumber install on developer machines. Plumber ships as the Plumber Compliance Scanner on the GitHub Marketplace: a uses: step that runs the same engine, the same .plumber.yaml, and the same controls and issues as the CLI.

The action automatically:

  • Downloads and checksum-verifies the Plumber binary (with optional SLSA attestation verification)
  • Runs plumber analyze with your chosen inputs
  • Uploads findings to GitHub Code Scanning via SARIF
  • Uploads the JSON report, PBOM, and CycloneDX SBOM as workflow artifacts
  • Posts a job summary with compliance percentage, Plumber score, and severity counts

Quick Start

  1. Ensure a .plumber.yaml exists in your repo root. Generate one with the CLI if you have it installed, or download the default:

    Terminal window
    plumber config generate
    # or:
    curl -fsSL https://raw.githubusercontent.com/getplumber/plumber/main/.plumber.yaml -o .plumber.yaml
  2. Add the action to your workflow (e.g. .github/workflows/plumber.yml):

    name: Plumber compliance
    on:
    push:
    branches: [main]
    pull_request: null
    permissions:
    contents: read
    security-events: write
    jobs:
    plumber:
    runs-on: ubuntu-24.04
    steps:
    - uses: actions/checkout@v6
    - uses: getplumber/plumber@COMMIT_SHA
  3. Push and check results: findings appear in the Code Scanning tab, the job summary, and the downloadable artifact bundle.

Tip

Pin by commit SHA, not a tag. Copy the ready-to-paste SHA-pinned uses: line from the GitHub Action section of the README. This is automatically updated on new releases.

Caution

The job needs security-events: write permission for SARIF upload to Code Scanning. Without it the upload step is skipped.

Customizing the action

Override any input to fit your needs:

- uses: getplumber/plumber@COMMIT_SHA # vX.Y.Z
with:
threshold: "80"
config-file: configs/strict.plumber.yaml
controls: actionsMustBePinnedByCommitSha,branchMustBeProtected
score: "true"
soft-fail: "true"
upload-sarif: "true"
upload-artifacts: "true"

All inputs

InputDefaultDescription
version(pinned in action.yml)Plumber release tag to install. Downloaded from GitHub Releases and verified against checksums.txt
verify-attestationtrueVerify the binary's SLSA build-provenance attestation via gh attestation verify. Disable for air-gapped or GHES setups
github-token${{ github.token }}Token for the GitHub API and SARIF upload. Needs Administration:read for full branchMustBeProtected, and security-events:write for SARIF
metadata-token-Optional token (public-repo read) used only to resolve third-party action versions for the known-CVE check, when an action is hosted in an org with an IP allow list that blocks the runner's GITHUB_TOKEN. Falls back to an anonymous read when unset
project(current repo)owner/repo to scan remotely (upstream-fetch, no checkout needed)
github-url(github.com)GitHub Enterprise Server host (e.g. ghes.example.com)
threshold100Minimum compliance percentage to pass (0-100)
config-file(auto-detect)Path to .plumber.yaml. Defaults to repo root; the run fails if absent
controls-Run only these controls (comma-separated). Mutually exclusive with skip-controls
skip-controls-Skip these controls (comma-separated). Mutually exclusive with controls
scoretrueShow the Plumber letter score, points, and breakdown
fail-warningsfalseFail on warnings: unknown configuration keys (exit 2) and "could not verify" checks such as a skipped known-CVE lookup (exit 3). soft-fail does not mask exit 3
soft-failfalseDo not fail the job when compliance is below the threshold. Findings are still produced and uploaded
upload-sariftrueUpload the SARIF report to GitHub Code Scanning
upload-artifactstrueUpload JSON report, PBOM, and CycloneDX SBOM as a workflow artifact
artifact-nameplumber-complianceName of the uploaded artifact bundle
outputplumber-report.jsonJSON report output path. Empty to skip
pbomplumber-pbom.jsonPBOM output path. Empty to skip
pbom-cyclonedxplumber-cyclonedx-sbom.jsonCycloneDX SBOM output path. Empty to skip
sarifplumber.sarifSARIF 2.1.0 output path. Empty to skip

Outputs

OutputDescription
complianceOverall compliance percentage from the run
passedtrue when compliance is at or above the threshold
reportPath to the JSON report
sarifPath to the SARIF report

Use outputs in downstream steps:

- uses: getplumber/plumber@COMMIT_SHA # vX.Y.Z
id: plumber
- run: echo "Compliance is ${{ steps.plumber.outputs.compliance }}%"

Code Scanning integration

When upload-sarif is true (the default), the action uploads a SARIF 2.1.0 report to GitHub Code Scanning. Each Plumber finding becomes an alert in the Security tab with:

  • The issue code as the rule ID (e.g. ISSUE-701)
  • Severity mapped to SARIF level (error, warning, note) and a numeric security-severity for triage
  • File location pointing at the workflow YAML line
  • A helpUri linking to the issue's documentation page on getplumber.io

Info

Code Scanning requires GitHub Advanced Security on private repos. On public repos it is free.

GitHub Enterprise Server

For GHES, pass the host via github-url:

- uses: getplumber/plumber@COMMIT_SHA # vX.Y.Z
with:
github-url: ghes.example.com
verify-attestation: "false"

Set verify-attestation: "false" if the runner cannot reach the sigstore transparency log or your GHES instance does not support gh attestation verify.

Examples

Scan a remote repo without cloning:

- uses: getplumber/plumber@COMMIT_SHA # vX.Y.Z
with:
project: my-org/other-repo
github-token: ${{ secrets.PLUMBER_PAT }}

Run only SHA-pinning and permissions checks:

- uses: getplumber/plumber@COMMIT_SHA # vX.Y.Z
with:
controls: actionsMustBePinnedByCommitSha,workflowsMustDeclarePermissions

Soft-fail in PRs, hard-fail on main:

- uses: getplumber/plumber@COMMIT_SHA # vX.Y.Z
with:
soft-fail: ${{ github.event_name == 'pull_request' && 'true' || 'false' }}

Troubleshooting

IssueSolution
no GitHub token found (upstream-fetch mode)Run gh auth login, or set GH_TOKEN / GITHUB_TOKEN. Upstream-fetch refuses to start without a token because the anonymous tier is rate-limited
401 UnauthorizedToken is invalid or expired. Fine-grained PAT needs Contents: Read + Metadata: Read; classic PAT needs repo
branchMustBeProtected shows in partialControlsToken lacks Administration: Read (fine-grained) or repo (classic). The rule abstains rather than claim a false pass
403 / rate-limit errorsAnonymous or under-scoped token. Authenticate with a PAT or gh auth login
ISSUE-703 fires in CI but not locally, or "could not verify" an action's versionThe action's org enforces an IP allow list that blocks the runner's GITHUB_TOKEN. Set metadata-token (action) or PLUMBER_METADATA_TOKEN (CLI) to a public-repo-read token
404 Not FoundVerify --project owner/repo and, for GHES, that --github-url points at the right host
Branch protection rule not detectedPlumber reads classic Branch Protection AND Rulesets; confirm the rule is enabled (not in evaluate mode) on a branch matching your namePatterns
Configuration file not foundEnsure --config points at the real file (use an absolute path in Docker). Create one with plumber config generate or plumber config init
SARIF upload fails with 403The job needs security-events: write permission. Add it under the workflow or job-level permissions: block
Attestation verification failsThe runner cannot reach sigstore or gh CLI is not installed. Set verify-attestation: "false" to skip

Info

Need help? Open an issue on GitHub or join our Discord.