Skip to main content

GitLab

Plumber scans your GitLab CI/CD pipelines and repository configuration for security problems:

  • Unverified remote scripts
  • Weakened security jobs
  • Missing branch protection
  • More…

It turns them into a Plumber Score from A to E that can block pipelines below a threshold you set.

Quick Start

Two ways to scan a GitLab project:

Run locally

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

  2. Authenticate: create a GITLAB_TOKEN with read_api + read_repository scopes (see Authentication).

  3. Run the scan from inside your project, or against a remote one (see Running a scan):

    Terminal window
    plumber analyze
  4. Read your Plumber score: an A–E grade with a per-control breakdown, plus an optional JSON report, PBOM, and CycloneDX SBOM (see Example Output).

Run with the GitLab CI component

  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. Create a GitLab token

    Use a token with read_api + read_repository scopes (or api if you enable mr_comment / badge), as described under Authentication below.

  3. Add the token to your project

    Go to Settings → CI/CD → Variables and add it as GITLAB_TOKEN (masked recommended).

  4. Add the component to your .gitlab-ci.yml

    Info

    • Self-hosted GitLab? You need your own copy of the component; see Hosting on self-hosted GitLab below. The snippet below targets gitlab.com.
    • Why workflow:rules? Without it, pushing to a branch with an open merge request creates two pipelines (a branch pipeline and an MR pipeline), splitting your jobs between them. The block below ensures a single pipeline per push: MR pipeline when an MR exists, branch pipeline otherwise. This is the recommended GitLab pattern. If you already have workflow:rules, keep yours and just add the include.
    • Publish a live score badge. Set score_push: true to publish a public Plumber Score badge for this repo. See Plumber Score.
    workflow:
    rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS # prevents duplicate pipelines
    when: never
    - if: $CI_COMMIT_BRANCH
    - if: $CI_COMMIT_TAG
    include:
    - component: gitlab.com/getplumber/plumber/plumber@<version>
    inputs:
    score_push: false # if "true": publish a public Plumber Score badge

    Get the latest version from the CI/CD Catalog.

  5. Run your pipeline

    Plumber now runs on every pipeline (default branch, tags, and open merge requests) and reports compliance issues.

Hosting on self-hosted GitLab

If you run a self-hosted GitLab instance, you need your own copy of the component since gitlab.com components can’t be accessed from your instance. There are two ways:

Customizing the component

Override any input to fit your needs:

include:
- component: gitlab.com/getplumber/plumber/plumber@v0.3.10
inputs:
# Target (defaults to current project)
server_url: https://gitlab.example.com # Self-hosted GitLab
project_path: other-group/other-project # Analyze a different project
branch: develop # Analyze a specific branch
ci_config_path: $CI_CONFIG_PATH # CI config path (GitLab predefined variable)
# Compliance
threshold: 80 # Minimum % to pass (default: 100)
config_file: configs/my-plumber.yaml # Custom config path
# Output
output_file: plumber-report.json # Export JSON report
pbom_file: plumber-pbom.json # PBOM artifact
pbom_cyclonedx_file: plumber-cyclonedx-sbom.json # CycloneDX SBOM (auto-uploaded as a GitLab report)
print_output: true
# Job behavior
stage: test # Run in a different stage
allow_failure: true # Don't block the pipeline on failure
gitlab_token: $MY_CUSTOM_TOKEN # Different variable name
verbose: true
# Selective execution (mutually exclusive)
controls: containerImageMustNotUseForbiddenTags,branchMustBeProtected
# skip_controls: branchMustBeProtected
# MR feedback (require `api` scope, see GitLab Integration above)
mr_comment: true
badge: true

The controls / skip_controls inputs map to the CLI --controls / --skip-controls flags (valid names). mr_comment and badge are the component equivalents of the CLI --mr-comment / --badge flags shown under GitLab Integration.

All inputs

InputDefaultDescription
server_url$CI_SERVER_URLGitLab instance URL
project_path$CI_PROJECT_PATHProject to analyze
branch$CI_COMMIT_REF_NAMEBranch to analyze
ci_config_path$CI_CONFIG_PATHCI configuration file path to analyze. Defaults to the GitLab predefined variable (.gitlab-ci.yml unless customized in project settings)
gitlab_token$GITLAB_TOKENGitLab API token (read_api + read_repository, or api if mr_comment / badge is enabled)
threshold100Minimum compliance % to pass
config_file(auto-detect)Path to config file (relative to repo root). Auto-detects .plumber.yaml, falls back to default
output_fileplumber-report.jsonPath to write JSON results
pbom_fileplumber-pbom.jsonPath to write the PBOM
pbom_cyclonedx_fileplumber-cyclonedx-sbom.jsonPath to write the CycloneDX SBOM (auto-uploaded as a GitLab report)
print_outputtruePrint text output to stdout
stage.prePipeline stage for the job. .pre runs before all other stages but requires at least one job in a regular stage. If Plumber is the only job, set this to test or another stage
imagegetplumber/plumber:0.1Docker image to use
allow_failurefalseAllow the job to fail without blocking
verbosefalseEnable debug output
mr_commentfalsePost/update a compliance comment on the merge request (requires api scope)
badgefalseCreate/update a Plumber letter-score badge (requires api scope; default branch only)
scorefalseDeprecated no-op. The Plumber score is shown by default now; kept so pipelines that already set it do not break. Use score_point for the full points breakdown
score_pointfalseAdd the full points breakdown to stdout and the MR comment (the score banner is shown by default)
score_pushfalsePublish this repo’s Plumber Score to the hosted badge service (score.getplumber.io). Uses CI-native OIDC (no secret); the component mints the id-token. Publishes on every run; the service keeps only the default branch for the public badge. Warns (never fails) on error. See Plumber Score
score_endpointhttps://score.getplumber.ioScore service base URL. Override only for a self-hosted score service; the OIDC audience follows this value so it always matches the target
controls-Run only listed controls (comma-separated). Cannot be used with skip_controls
skip_controls-Skip listed controls (comma-separated). Cannot be used with controls
fail_warningsfalseTreat configuration warnings (unknown keys) as errors (exit 2)

Component configuration resolution

The component resolves your configuration in priority order:

  1. config_file input set uses your specified path (relative to repo root).
  2. .plumber.yaml in repo root uses your repo’s config file.
  3. No config found uses the default embedded in the container.

To author one, run plumber config generate (see the CLI Reference) or create it manually from the default config. The CycloneDX SBOM the component writes is automatically uploaded as a GitLab CycloneDX report.

Authentication

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

Use api scope instead of read_api if you plan to enable --mr-comment or --badge (see GitLab Integration below).

Running a scan

Terminal window
# Auto-detected from git remote
plumber analyze
# Explicit project
plumber analyze --gitlab-url https://gitlab.com --project mygroup/myproject

Self-Hosted GitLab

Terminal window
plumber analyze --gitlab-url https://gitlab.example.com --project mygroup/myproject

Custom CI Configuration Path

By default Plumber reads the project’s configured CI config path (usually .gitlab-ci.yml). Override it when your pipeline file lives elsewhere:

Terminal window
plumber analyze --ci-config-path .gitlab/ci/main.yml

Examples

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 image tags and branch protection
plumber analyze --controls containerImageMustNotUseForbiddenTags,branchMustBeProtected
# 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.

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

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

Configuration

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

version: "2.0"
gitlab:
controls:
containerImageMustNotUseForbiddenTags:
enabled: true
tags:
- latest
- dev
- main
# When true, ALL images must be pinned by digest. Takes precedence
# over the tags list, so even version tags like alpine:3.19 fail.
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
externalRefsMustNotCollide:
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 controlled variables overridden in .gitlab-ci.yml.
pipelineMustNotOverrideJobVariables:
enabled: true
variables:
- SECURE_ANALYZERS_PREFIX
- SAST_DISABLED
- SAST_EXCLUDED_PATHS
- SECRET_DETECTION_DISABLED
- CONTAINER_SCANNING_DISABLED
- DAST_DISABLED
# Detect unverified script downloads and execution (curl|bash, wget|sh).
pipelineMustNotExecuteUnverifiedScripts:
enabled: true
trustedUrls: []
# - https://internal-artifacts.example.com/*
# Detect Docker-in-Docker services and insecure daemon configuration.
pipelineMustNotUseDockerInDocker:
enabled: true
detectInsecureDaemon: true

Info

pipelineMustIncludeComponent and pipelineMustIncludeTemplate 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.

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.3.10
inputs:
mr_comment: true # Requires api scope on token

Merge request comment showing compliance results

Features:

  • Shows the Plumber letter-score badge (A–E) and a short score line; with score_point, adds the full points breakdown
  • 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 Plumber letter-score 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.3.10
inputs:
badge: true # Requires api scope on token

Plumber score badge on the project overview page

Features:

  • Shows the Plumber letter score (A–E)
  • Colored by grade: A/B green, C yellow, D orange, E red
  • 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.

Reference

The complete, always-current catalogs and command reference:

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 --config points at the real file (use an absolute path in Docker). Create one with plumber config generate or plumber config init
Component not found (self-hosted)You must import or mirror the component to your instance (Hosting on self-hosted GitLab)
Plumber component job not runningThe component’s default stage is .pre, which requires at least one other job in a regular stage. Override with inputs: { stage: test }
Two pipelines on the same pushAdd workflow:rules to prevent duplicate branch + MR pipelines (see Run with the GitLab CI component)
Component job skipped on branchThe component runs only on merge request events, the default branch, and tags
Score badge not publishedscore_push publishes on every CI run, but the public badge only reflects your default branch (the service filters by the OIDC branch claim; MR/tag pipelines publish without touching it). A local run never publishes. The push warns, never fails, when the OIDC id-token is unavailable. See Plumber Score

Info

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