Plumber

repository·main·Indexed 20 days ago

https://github.com/getplumber/plumber

A CI/CD security scanner for GitLab CI and GitHub Actions that uses a Rego policy engine to detect risky patterns and security gaps in pipeline definitions. Plumber provides a CLI for local and remote analysis, integrates via GitHub Actions and GitLab CI components, and uses a .plumber.yaml configuration file to manage security rules and score gates.

Tokens
42K
Snippets
115
Records
197
Agent score
69%

What's inside plumber

  1. Understand PBOM dependency coverage by provider

    main

    The contents of a PBOM depend on whether you are analyzing a GitLab or GitHub pipeline. Plumber auto-detects the provider from the active analyzer.

    ProviderContainer images"Includes" equivalent
    GitLabimage: + services: blocks across all jobsGitLab CI components, templates, project includes, local includes, remote URL includes
    GitHubcontainer: + services: blocks across all jobsThird-party action references (uses: owner/repo@ref) and reusable-workflow calls (jobs.<name>.uses: …/.github/workflows/x.yml@ref)
  2. Understand Plumber's Detection Philosophy and Limits

    main

    Plumber is designed as a complementary layer to runtime monitoring, focusing on static analysis of GitHub Actions. It follows these core principles:

    • Non-Assertive Results: A clean result means "no known pattern seen," not that the workflow is "safe."
    • Intent-Based Severity: Plumber prioritizes the act of hiding (obfuscation) over specific encoding types. Obfuscation-then-execution is treated as a critical signal.
    • Tri-state Reporting: When a source cannot be resolved (due to being private, offline, or a Docker-image action), Plumber emits a could-not-verify state rather than a silent pass.
    • Static Blind Spots: Users should be aware that static analysis cannot detect:
      • Runtime fetch-and-execute: Code pulled from a moving reference at execution time.
      • Transitive risks: Vulnerabilities in the dependencies of an action (beyond the first level).
      • Execution-time leaks: Secrets leaked during the actual running of a process.
  3. Vulnerability detection in PBOMs

    main

    When scanning a PBOM, expect to see few to no vulnerabilities in most scanners. This is because many pipeline components (like GitLab templates or GitHub reusable workflows) are configuration files, not software packages, and are not tracked in standard CVE databases (NVD, OSV, etc.).

    GitHub Actions Security

    Plumber includes a specific control, actionsMustNotCarryKnownCVEs (enabled by default), which checks GitHub uses: references against the GitHub Security Advisories database. If an advisory is found, Plumber stamps the include with hasCve: true and an advisories list. Generic CVE scanners typically do not perform this check.

    Best Practices for Image Scanning

    To perform deep vulnerability scanning on the container images identified in your PBOM, use specialized image scanners directly:

    trivy image golang:1.22
    grype docker.io/library/golang:1.22
  4. Understand Plumber pipeline scoring (scoring-v3)

    main

    Plumber uses a scoring system (profile scoring-v3) to evaluate pipeline risk. It produces a letter score (A–E) and points (0–100).

    Key Concepts

    • Score: The letter grade (A, B, C, D, or E). This is the primary metric for pipeline health.
    • Points: A numerical value from 0 to 100.
      • Raw points: Calculated from issue losses before any penalties.
      • Final points: The value used to determine the letter score, which includes the Critical malus if applicable.
    • Critical malus: If any issue with Critical severity is found, the finalPoints are capped at a maximum of 30, forcing the letter score to E regardless of other findings.

    Letter Score Thresholds

    LetterFinal pointsMeaning
    A≥ 90Excellent, very low risk
    B71 – 89Good, a few Low or Medium issues
    C51 – 70Moderate, worth fixing
    D31 – 50Poor, High-severity issues
    E< 31Critical, at least one Critical issue or heavy losses
  5. Understand the .plumber.yaml v2 schema changes

    main

    The v2 schema introduces three primary changes to the configuration structure:

    1. Per-provider nesting: The top-level controls: block is removed. Controls must now be nested under a provider key (e.g., gitlab.controls: or github.controls:). This allows different values for the same control across different providers.
    2. Removal of engine:: The engine: block is deprecated and removed. The Rego/OPA engine now runs unconditionally.
    3. Version bump: The version: field must be set to "2.0".

    Schema Comparison

    v1 (Legacy):

    version: "1.0"
    controls:
      branchMustBeProtected:
        enabled: true
    engine:
      enabled: true

    v2 (Current):

    version: "2.0"
    gitlab:
      controls:
        branchMustBeProtected:
          enabled: true
    github:
      controls:
        actionsMustBePinnedByCommitSha:
          enabled: true
          trustedOwners: [actions, github]
  6. Detect Obfuscation and Malware Signals in Action Source

    main

    Legitimate GitHub Actions rarely need to hide their logic. The presence of the following patterns in fetched action source code should be treated as high or critical signals of malicious intent:

    • Encoded Execution: Using base64 -d | sh, openssl enc -d | sh, xxd -r, or eval "$(... | base64 -d)".
    • JavaScript Obfuscation: Patterns like eval(atob(...)), new Function(atob(x)), or exec(Buffer.from(x,'base64')).
    • Suspicious Downloads: Using curl -o /tmp/x; sh /tmp/x or performing host swaps (e.g., downloading from a Gist or a non-standard domain).
    • High Entropy: Large, high-entropy encoded blobs or suspicious minification of normally readable source code.
    • Secret Harvesting: Commands like printenv | curl or attempts to read .git/config to exfiltrate credentials.
  7. Quickstart: Run your first Plumber scan

    main

    To get started immediately, install the Plumber CLI, generate a default configuration, and run an analysis. Plumber auto-detects your provider (GitLab or GitHub) from your git remote.

    brew tap getplumber/plumber
    brew install plumber
    
    plumber config generate # generate the default configuration file
    plumber analyze
  8. Pin Dockerfile base images by digest

    main

    To prevent supply-chain attacks where a registry tag is re-pushed to point to a different image layer, use an immutable @sha256:... digest instead of a mutable tag in your FROM instruction.

    You can find the digest for a tag using docker inspect:

    docker inspect --format='{{index .RepoDigests 0}}' <image>:<tag>

    To keep pins current, use automation tools like Dependabot (package-ecosystem: docker) or Renovate (pinDigests: true).

    # ❌ Avoid mutable tags
    FROM alpine:3.20
    
    # ✅ Use immutable digests
    FROM alpine:3.20@sha256:b7d40c02c23be0ca99da3a0e5e8bd2f0a0a2b3a0e5e8bd2f0a0a2b3a0e5e8bd2
  9. Disable GitHub API-backed rules in sealed CI environments

    main

    Several rules (including ISSUE-702, 109, 110, 111, 113, and 114) require access to the GitHub REST API via the gh CLI. By default, if no gh token is available, these rules degrade silently without failing the run.

    To explicitly disable these API-dependent rules in environments where GitHub API access is restricted or unavailable, set the PLUMBER_DISABLE_GITHUB_API environment variable to 1.

    export PLUMBER_DISABLE_GITHUB_API=1
  10. Secure reusable workflows and secrets

    main

    When using reusable workflows, avoid using secrets: inherit. This forwards every secret from the caller to the callee, increasing the risk if the callee is compromised.

    Mitigation: Use explicit per-secret mapping in the secrets: block of the workflow call.

    Additionally, avoid using fromJSON(secrets.X).y to access structured secrets. This bypasses GitHub's automatic log redaction because the sub-fields are treated as fresh strings.

    Mitigation: Store structured secrets as individual leaf secrets (e.g., API_TOKEN instead of a JSON object CREDS) and bind them directly.

    # ❌ before
    jobs:
      call:
        uses: org/shared/.github/workflows/publish.yml@v1
        secrets: inherit
    
    # ✅ after — explicit per-secret mapping
    jobs:
      call:
        uses: org/shared/.github/workflows/publish.yml@v1
        secrets:
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
    
    # ❌ before — .token bypasses redaction once fromJSON runs
    jobs:
      deploy:
        env:
          API_TOKEN: ${{ fromJSON(secrets.CREDS).token }}
        steps:
          - run: echo "token=$API_TOKEN" >> deploy.log
    
    # ✅ after — split the structured secret, store each leaf separately
    jobs:
      deploy:
        env:
          API_TOKEN: ${{ secrets.API_TOKEN }}
        steps:
          - run: echo "token=$API_TOKEN" >> deploy.log
  11. Mitigate `pull_request_target` head checkout vulnerabilities

    main

    Using pull_request_target combined with an explicit checkout of the PR head (e.g., using github.event.pull_request.head.sha) is a critical security risk (CVE-2025-30066) because it allows fork-controlled code to run with base-repository secrets.

    To mitigate this, use a split trigger pattern: use pull_request_target only for metadata operations (like labeling) and use a standard pull_request trigger for code execution. Alternatively, add a job-level if: guard to ensure the job only runs on pull requests within the same repository.

    # ✅ Recommended: split trigger pattern
    name: Preview — metadata
    on: [pull_request_target]
    permissions:
      pull-requests: write
    jobs:
      label:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4     # base repo, no ref: override
          - run: gh pr edit --add-label auto-preview