Skip to main content

OpenSource CLI

The Plumber CLI allows you to analyze GitLab CI/CD pipelines from the command line for security and compliance issues like:

  • Container images using mutable tags (latest, dev)
  • Container images from untrusted registries
  • Unprotected branches
  • Hardcoded jobs not from includes/components
  • Outdated includes/templates
  • Forbidden version patterns (e.g., main, HEAD)
  • Missing required components or templates

This is useful for local testing, CI/CD integration, or automated compliance checks.

View on GitHub

Installation

Quick Start

  1. Generate a config file

    Terminal window
    plumber config generate

    This creates .plumber.yaml with default compliance rules. You can customize it later.

  2. Create and set your GitLab token

    In GitLab, go to User Settings → Access Tokens (direct link) and create a Personal Access Token with read_api + read_repository scopes. Project Access Tokens also work: create one inside your project under Settings → Access Tokens with the same scopes and at least Maintainer role.

    Caution

    The token must belong to a user (or project bot) with Maintainer role (or higher) on the project to access branch protection settings and other project configurations.

    Terminal window
    export GITLAB_TOKEN=glpat-xxxx
  3. Run analysis

    Terminal window
    # If you're in a git repo with a GitLab remote, just run:
    # the gitlab url and projects are automatically detected from the .git
    # auto-detection requires the remote to be set to 'origin'
    plumber analyze
    # Or specify the project explicitly:
    plumber analyze --gitlab-url https://gitlab.com --project mygroup/myproject

    Plumber auto-detects the GitLab URL and project from your git remote (requires remote to be set to origin).

  4. Review results

    Plumber reads your .plumber.yaml config and outputs a compliance report. You can also store the output in JSON format with the --output flag.

Command Reference

plumber analyze

The main command for analyzing GitLab CI/CD pipelines.

Terminal window
plumber analyze [flags]

Flags

FlagRequiredDefaultDescription
--gitlab-urlNo*auto-detectGitLab instance URL (e.g., https://gitlab.com)
--projectNo*auto-detectProject path (e.g., group/project)
--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-warningsNofalseTreat configuration warnings (unknown keys) as errors (exit 2)
--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
GITLAB_TOKENYesGitLab API token with read_api + read_repository scopes (from a Maintainer or higher). Use api scope instead if --mr-comment or --badge is enabled.
PLUMBER_NO_UPDATE_CHECKNoSet to any value (e.g., 1) to disable the automatic version check.

Automatic Version Check

When running locally, Plumber checks GitHub for newer releases on every invocation and prints an upgrade notice if one is available. The check runs asynchronously and has a 3-second timeout, so it never slows down the analysis.

The check is automatically skipped when:

  • Running in CI environments (CI or GITLAB_CI environment variables are set)
  • Using a development build (version is dev)

To disable it manually:

Terminal window
export PLUMBER_NO_UPDATE_CHECK=1

Exit Codes

CodeMeaning
0Passed (compliance ≥ threshold)
1Compliance failure (compliance < threshold)
2Runtime error (config error, network failure, missing token, etc.)

plumber config generate

Generate a default .plumber.yaml configuration file.

Terminal window
plumber config generate [flags]
FlagDefaultDescription
--output, -o.plumber.yamlOutput file path
--force, -ffalseOverwrite existing file

Examples:

Terminal window
# Generate default config
plumber config generate
# Custom filename
plumber config generate --output my-plumber.yaml
# Overwrite existing file
plumber config generate --force

plumber config view

Display a clean, human-readable view of the effective configuration without comments.

Terminal window
plumber config view [flags]
FlagDefaultDescription
--config, -c.plumber.yamlPath to configuration file
--no-colorfalseDisable colorized output

Booleans are colorized for quick scanning: true in green, false in red. Color is automatically disabled when piping output.

plumber config view output with colorized booleans

Examples:

Terminal window
# View the default .plumber.yaml
plumber config view
# View a specific config file
plumber config view --config custom-plumber.yaml
# View without colors (for piping or scripts)
plumber config view --no-color

plumber config validate

Validate a configuration file for correctness. Detects unknown control names and sub-keys with typo suggestions using fuzzy matching.

Terminal window
plumber config validate [flags]
FlagDefaultDescription
--config, -c.plumber.yamlPath to configuration file
--fail-warningsfalseTreat configuration warnings as errors (exit 2)

Warnings are printed to stderr so they don’t interfere with scripted output. Use --fail-warnings to exit with code 2 when warnings are found (useful in CI).

Examples:

Terminal window
# Validate the default .plumber.yaml
plumber config validate
# Validate a specific config file
plumber config validate --config custom-plumber.yaml
# Fail on warnings (for CI pipelines)
plumber config validate --fail-warnings

Sample output with typos:

Configuration validation warnings:
- Unknown control in .plumber.yaml: "containerImageMustNotUseForbiddenTag". Did you mean "containerImageMustNotUseForbiddenTags"?
- Unknown key "tag" in control "containerImageMustNotUseForbiddenTags". Did you mean "tags"?
- Unknown key "allowForcePushes" in control "branchMustBeProtected". Did you mean "allowForcePush"?

Usage Examples

Save JSON Output

Terminal window
docker run --rm \
-e GITLAB_TOKEN=glpat-xxxx \
-v $(pwd):/output \
getplumber/plumber:latest analyze \
--gitlab-url https://gitlab.com \
--project mygroup/myproject \
--branch main \
--config /.plumber.yaml \
--threshold 100 \
--output /output/results.json

Self-Hosted GitLab

Terminal window
plumber analyze \
--gitlab-url https://gitlab.example.com \
--project mygroup/myproject \
--branch develop \
--config .plumber.yaml \
--threshold 80

Silent Mode (JSON Only)

Terminal window
plumber analyze \
--gitlab-url https://gitlab.com \
--project mygroup/myproject \
--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

Pipeline Bill of Materials (PBOM) & CycloneDX

Plumber can generate a PBOM — a complete inventory of all dependencies in your CI/CD pipeline (container images, components, templates, includes). Two formats are available:

Native PBOM (detailed, pipeline-specific):

Terminal window
plumber analyze --pbom pbom.json

CycloneDX SBOM (standard format for security tool integration):

Terminal window
plumber analyze --pbom-cyclonedx pipeline-sbom.json

The CycloneDX output follows the CycloneDX 1.5 specification and is compatible with tools like Grype, Trivy, and Dependency-Track. When using the GitLab CI component, the CycloneDX file is automatically uploaded as a GitLab CycloneDX report.

Info

CI/CD components and templates do not have CVEs in public vulnerability databases. The PBOM is primarily an inventory and compliance tool — it tells you what’s in your pipeline, not whether those items have known vulnerabilities. For image vulnerability scanning, use trivy image or grype directly on the images.

Configuration

Plumber uses a .plumber.yaml configuration file to customize checks.

Available Controls

Plumber includes compliance controls covering CI/CD configuration, repository settings, and access management. Each can be enabled/disabled and customized. When a control detects a violation, it creates an Issue (e.g., ISSUE-101) with a direct link to its documentation page.

ControlIssuesDescription
Container images must not use forbidden tagsISSUE-102Flags latest, dev, and other mutable tags. Can enforce digest pinning for all images
Container images must be pinned by digestISSUE-103Ensures images use SHA256 digest pinning
Container images must come from authorized sourcesISSUE-101Ensures images come from trusted registries
Branch must be protectedISSUE-501 ISSUE-505Verifies critical branches have proper protection
Pipeline must not include hardcoded jobsISSUE-401Detects jobs defined directly instead of from includes
Includes must be up to dateISSUE-403Checks if included templates have newer versions
Includes must not use forbidden versionsISSUE-404Prevents mutable version references like main, HEAD
Pipeline must include componentISSUE-408 ISSUE-409Ensures required CI/CD components are included; detects overridden jobs
Pipeline must include templateISSUE-405 ISSUE-406Ensures required templates are included; detects overridden jobs
Pipeline must not enable debug traceISSUE-203Detects CI_DEBUG_TRACE/CI_DEBUG_SERVICES leaking secrets in job logs
Pipeline must not use unsafe variable expansionISSUE-204Detects user-controlled variables in shell re-interpretation contexts (OWASP CICD-SEC-1)
Security jobs must not be weakenedISSUE-410Detects security scanning jobs neutralized by allow_failure, rules: overrides, or when: manual (OWASP CICD-SEC-4)
Pipeline must not execute unverified scriptsISSUE-411Detects curl | bash, wget | sh, and download-then-execute patterns without integrity verification (OWASP CICD-SEC-3)
Unsafe Variable Expansion Detection

Detects user-controlled CI variables (MR title, commit message, branch name) passed to commands that re-interpret their input as shell code. This is OWASP CICD-SEC-1.

GitLab sets CI variables as environment variables. The shell does not re-parse expanded values for command substitution, so normal usage is safe. Only commands that re-interpret their arguments as code are flagged:

Flagged (re-interpretation contexts):

Terminal window
eval "$CI_COMMIT_BRANCH"
sh -c "$CI_MERGE_REQUEST_TITLE"
bash -c "$CI_COMMIT_MESSAGE"
source <(echo "$CI_COMMIT_REF_NAME")
echo "$CI_COMMIT_BRANCH" | xargs sh

Not flagged (safe, the shell doesn’t re-parse env var values):

Terminal window
echo $CI_COMMIT_BRANCH
curl -d "$CI_MERGE_REQUEST_TITLE" https://...
git checkout $CI_COMMIT_REF_NAME
printf '%s' "$CI_COMMIT_MESSAGE"

Allowing specific patterns

Some sh -c or bash -c usages are legitimate (e.g., Helm deploys, Terraform workspaces). Use allowedPatterns (regex) to suppress those findings. Each pattern is matched against the full script line.

pipelineMustNotUseUnsafeVariableExpansion:
enabled: true
dangerousVariables:
- CI_MERGE_REQUEST_TITLE
- CI_COMMIT_MESSAGE
- CI_COMMIT_REF_NAME
- CI_COMMIT_REF_SLUG
- CI_COMMIT_BRANCH
allowedPatterns:
- "helm.*--set.*\\$CI_"
- "terraform workspace select.*\\$CI_"
- "docker build.*--build-arg.*\\$CI_"
- "aws s3 sync.*\\$CI_"
- "make deploy.*\\$CI_"

For example, the script line sh -c "helm upgrade myapp . --set image.tag=$CI_COMMIT_SHA" would normally be flagged, but the pattern helm.*--set.*\\$CI_ allows it.

Info

Escape $ as \\$ and {/} as \\{/\\} in patterns. Only direct variable names are detected. Indirect aliasing (variables: { B: $CI_COMMIT_BRANCH } then sh -c $B) is not tracked.

Security Job Weakening Detection

GitLab lets you override any property of an included job. This means someone can include a security template (SAST, Secret Detection, Container Scanning, Dependency Scanning, DAST, License Scanning) but silently neutralize it. The pipeline still looks compliant, but the scanning is disabled. Maps to OWASP CICD-SEC-4 (Poisoned Pipeline Execution).

This control detects three weakening patterns, each a separate sub-control you can toggle independently:

  • allowFailureMustBeFalse (default: off, opt-in): Detects allow_failure: true. Off by default because GitLab templates ship with this setting.
  • rulesMustNotBeRedefined (default: on): Detects rules: overrides with when: never or when: manual.
  • whenMustNotBeManual (default: on): Detects when: manual set at job level.

Flagged: security jobs weakened

include:
- template: Security/SAST.gitlab-ci.yml
semgrep-sast:
allow_failure: true # failures silently ignored
secret_detection:
rules:
- when: never # job will never run
container_scanning:
when: manual # requires manual trigger

Configuration

Security jobs are identified by matching job names against securityJobPatterns (wildcards supported):

securityJobsMustNotBeWeakened:
enabled: true
securityJobPatterns:
- "*-sast"
- "secret_detection"
- "container_scanning"
- "*_dependency_scanning"
- "gemnasium-*"
- "dast"
- "dast_*"
- "license_scanning"
allowFailureMustBeFalse:
enabled: false
rulesMustNotBeRedefined:
enabled: true
whenMustNotBeManual:
enabled: true
Unverified Script Execution Detection

Detects CI/CD jobs that download and immediately execute scripts from the internet without integrity verification. This is a well-documented supply chain attack vector: an attacker who compromises the remote URL can serve a modified script that exfiltrates secrets. 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, etc.
  • Download-and-execute: curl -o script.sh ... && bash script.sh
  • Download-redirect-execute: curl ... > install.sh; sh install.sh

Lines that include checksum verification (e.g., sha256sum, gpg --verify) between the download and execution are automatically excluded.

Configuration:

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

Add trusted URL patterns to trustedUrls (supports wildcards) to suppress findings for known-good sources.

Issues

Every violation detected by Plumber is identified as an Issue following the pattern ISSUE-XXXX. Each issue links to a dedicated documentation page with:

  • Description of the problem
  • Impact on your security posture
  • How to fix with before/after configuration examples
  • Tips for best practices

Issues are displayed in the CLI output, MR comments, and the summary table. Click any issue to view its full documentation.

IssueTitle
ISSUE-101Untrusted image source
ISSUE-102Forbidden container image tag
ISSUE-103Container image not pinned by digest
ISSUE-201Unprotected variable
ISSUE-202Unmasked variable
ISSUE-203Pipeline enables CI debug trace
ISSUE-204Unsafe variable expansion
ISSUE-301Secret leak in pipeline configuration
ISSUE-401Hardcoded job
ISSUE-402Forbidden override of job
ISSUE-403Outdated template
ISSUE-404Forbidden include version
ISSUE-405Missing required template
ISSUE-406Forbidden override of required template
ISSUE-501Branch protection missing
ISSUE-601Missing security policy source on project
ISSUE-502Merge request approval rule is below the minimum level of approvals required
ISSUE-503Merge request approval settings are not compliant
ISSUE-504No merge request approval rule covering all protected branches
ISSUE-407Invalid pipeline composition
ISSUE-505Branch protection configuration not compliant
ISSUE-506Merge request settings are not compliant
ISSUE-408Missing required component
ISSUE-409Forbidden override of required component
ISSUE-507Members’ role quotas are not respected for projects
ISSUE-508Members’ role quotas are not respected for groups
ISSUE-410Security job weakened
ISSUE-411Unverified script execution

Example Configuration

Tip

See the full configuration reference for all options.

version: "1.0"
controls:
containerImageMustNotUseForbiddenTags:
enabled: true
tags:
- latest
- dev
- main
# When true, ALL images must be pinned by digest (e.g., alpine@sha256:...)
# Takes precedence over tags list — even version tags like alpine:3.19 will be flagged
containerImagesMustBePinnedByDigest: false
containerImageMustComeFromAuthorizedSources:
enabled: true
trustDockerHubOfficialImages: true
trustedUrls:
- $CI_REGISTRY_IMAGE:*
- registry.gitlab.com/security-products/*
branchMustBeProtected:
enabled: true
defaultMustBeProtected: true
namePatterns:
- main
- release/*
allowForcePush: false
minMergeAccessLevel: 30 # Developer
minPushAccessLevel: 40 # Maintainer
pipelineMustNotIncludeHardcodedJobs:
enabled: true
includesMustBeUpToDate:
enabled: true
includesMustNotUseForbiddenVersions:
enabled: true
forbiddenVersions:
- latest
- "~latest"
- main
- master
- HEAD
defaultBranchIsForbiddenVersion: false
pipelineMustIncludeComponent:
enabled: false # Disabled by default - enable and configure for your org
# Expression syntax (use one, not both):
# required: components/sast/sast AND components/secret-detection/secret-detection
# Array syntax (OR of ANDs):
# requiredGroups:
# - ["components/sast/sast", "components/secret-detection/secret-detection"]
# - ["your-org/full-security/full-security"]
pipelineMustIncludeTemplate:
enabled: false # Disabled by default - enable and configure for your org
# Expression syntax (use one, not both):
# required: templates/go/go AND templates/trivy/trivy
# Array syntax (OR of ANDs):
# requiredGroups:
# - ["templates/go/go", "templates/trivy/trivy"]
# - ["templates/full-go-pipeline"]
# Detect debug trace variables that leak secrets in job logs
pipelineMustNotEnableDebugTrace:
enabled: true
forbiddenVariables:
- CI_DEBUG_TRACE
- CI_DEBUG_SERVICES
# Detect user-controlled variables in shell re-interpretation contexts (eval, sh -c, etc.)
# Safe: echo $CI_COMMIT_BRANCH. Dangerous: eval "deploy $CI_COMMIT_BRANCH"
pipelineMustNotUseUnsafeVariableExpansion:
enabled: true
dangerousVariables:
- CI_MERGE_REQUEST_TITLE
- CI_MERGE_REQUEST_DESCRIPTION
- CI_COMMIT_MESSAGE
- CI_COMMIT_TITLE
- CI_COMMIT_TAG_MESSAGE
- CI_COMMIT_REF_NAME
- CI_COMMIT_REF_SLUG
- CI_COMMIT_BRANCH
- CI_MERGE_REQUEST_SOURCE_BRANCH_NAME
- CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_NAME
# Regex patterns to allow specific script lines (escape $ as \\$)
allowedPatterns:
- "helm.*--set.*\\$CI_"
- "terraform workspace select.*\\$CI_"
- "docker build.*--build-arg.*\\$CI_"
# Detect security scanning jobs that have been silently weakened
securityJobsMustNotBeWeakened:
enabled: true
securityJobPatterns:
- "*-sast"
- "secret_detection"
- "container_scanning"
- "*_dependency_scanning"
- "gemnasium-*"
- "dast"
- "dast_*"
- "license_scanning"
allowFailureMustBeFalse:
enabled: false # opt-in (GitLab templates ship with allow_failure: true)
rulesMustNotBeRedefined:
enabled: true
whenMustNotBeManual:
enabled: true
# Detect unverified script downloads and execution (curl|bash, wget|sh)
pipelineMustNotExecuteUnverifiedScripts:
enabled: true
trustedUrls: []
# - https://internal-artifacts.example.com/*

Info

Controls 7 and 8 support two syntax options for defining requirements (use one, not both):

  • Expression syntax (required): A natural boolean expression using AND, OR, and parentheses. AND binds tighter than OR.
  • Array syntax (requiredGroups): A list of groups using “OR of ANDs” logic — outer array = OR, inner array = AND.

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.

Run only specific controls:

Terminal window
# Only check image tags and branch protection
plumber analyze --controls containerImageMustNotUseForbiddenTags,branchMustBeProtected

Skip specific controls:

Terminal window
# Run everything except branch protection
plumber analyze --skip-controls branchMustBeProtected

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

Valid control names
Control Name
branchMustBeProtected
containerImageMustComeFromAuthorizedSources
containerImageMustNotUseForbiddenTags
includesMustBeUpToDate
includesMustNotUseForbiddenVersions
pipelineMustIncludeComponent
pipelineMustIncludeTemplate
pipelineMustNotEnableDebugTrace
pipelineMustNotExecuteUnverifiedScripts
pipelineMustNotIncludeHardcodedJobs
pipelineMustNotUseUnsafeVariableExpansion
securityJobsMustNotBeWeakened

GitLab Integration

Plumber integrates directly with GitLab to provide visual compliance feedback where your team works.

Merge Request Comments

Automatically post compliance summaries on merge requests to catch issues before they’re merged.

Terminal window
plumber analyze --mr-comment

Or via the GitLab CI component:

include:
- component: gitlab.com/getplumber/plumber/plumber@v0.1.29
inputs:
mr_comment: true # Requires api scope on token

Merge request comment showing compliance results

Features:

  • Shows compliance badge with pass/fail status
  • Lists all controls with individual compliance percentages
  • Details specific issues found with job names and image references
  • Automatically updates on each pipeline run (no duplicate comments)

Caution

Token requirement: The api scope is required (not read_api) to create/update MR comments. The --mr-comment flag only works in merge request pipelines (CI_MERGE_REQUEST_IID must be set).

Project Badges

Display a live compliance badge on your project’s overview page.

Terminal window
plumber analyze --badge

Or via the GitLab CI component:

include:
- component: gitlab.com/getplumber/plumber/plumber@v0.1.29
inputs:
badge: true # Requires api scope on token

Project badge showing Plumber compliance percentage

Features:

  • Shows current compliance percentage
  • Green when compliance meets threshold, red when below
  • Only updates on default branch pipelines (not on MRs or feature branches)
  • Badge appears in GitLab’s “Project information” section

Caution

Token requirement: The api scope is required (not read_api) and Maintainer role to manage project badges.

Troubleshooting

IssueSolution
GITLAB_TOKEN environment variable is requiredSet the GITLAB_TOKEN environment variable with a valid GitLab token
401 UnauthorizedToken needs read_api + read_repository scopes, from a Maintainer or higher
403 Forbidden on MR settingsExpected on non-Premium GitLab; continues without that data
403 Forbidden on MR commentToken needs api scope (not read_api) when --mr-comment is enabled
403 Forbidden on badgeToken needs api scope (not read_api) when --badge is enabled
404 Not FoundVerify the project path and GitLab URL are correct
MR comment not posted--mr-comment only works in merge request pipelines (CI_MERGE_REQUEST_IID must be set)
Badge not created/updatedToken needs api scope and Maintainer role (or higher) on the project
Configuration file not foundEnsure the path to .plumber.yaml is correct (use absolute path in Docker)

Info

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