GitLab Component
The Plumber GitLab Component lets you add compliance scanning directly to your GitLab CI/CD pipelines. It checks 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
Quick Start (GitLab.com)
Info
These instructions are for projects hosted on gitlab.com. For self-hosted GitLab instances, see Self-Hosted GitLab below.
Create a GitLab token
In GitLab, go to User Settings → Access Tokens (or create one here) and create a Personal Access Token with
read_api+read_repositoryscopes. 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.
Using
mr_commentorbadge? The token needs theapiscope (instead ofread_api) to create/update merge request comments or project badges.Add the token to your project
Go to your project’s Settings → CI/CD → Variables and add the token as
GITLAB_TOKEN(masked recommended).Add to your pipeline
Add this to your
.gitlab-ci.yml:workflow:rules:- if: $CI_PIPELINE_SOURCE == "merge_request_event"- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS # prevents duplicate pipelineswhen: never- if: $CI_COMMIT_BRANCH- if: $CI_COMMIT_TAGinclude:- component: gitlab.com/getplumber/plumber/plumber@v0.1.29# inputs:# stage: .pre | by default runs in .pre which only runs if there is at least another CI job in another stageGet the latest version from the CI/CD Catalog.
Info
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
workflow:rulesblock ensures a single pipeline per push: MR pipeline when an MR exists, branch pipeline otherwise. This is the recommended GitLab pattern. If you already haveworkflow:rulesin your.gitlab-ci.yml, keep yours and just add theinclude.Run your pipeline
Plumber will now run on every pipeline (default branch, tags, and open merge requests) and report compliance issues.
Tip
Everything is customizable — GitLab URL, branch, threshold, and more. See Customize below.
Self-Hosted GitLab
If you’re running a self-hosted GitLab instance, you’ll need to host your own copy of the component since gitlab.com components can’t be accessed from your instance. There are two ways:
Import the upstream repository directly into your GitLab instance.
Import the repository
Go to New Project → Import project → Repository by URL and use this URL:
https://gitlab.com/getplumber/plumber.git. Choose a group and project name (e.g.,infrastructure/plumber).Enable the CI/CD Catalog
In your imported project, go to Settings → General and make sure the project has a description (required for CI/CD Catalog). Then expand Visibility, project features, permissions, toggle CI/CD Catalog resource to enabled, and click Save changes.
Publish a release
The imported project comes with upstream tags. The preferred method is to run a pipeline on an existing tag to trigger the release:
- Go to CI/CD → Pipelines → Run pipeline
- Select an imported tag (e.g.,
v0.1.29) from the branch/tag dropdown - Click Run pipeline: this creates a release for that tag in the CI/CD Catalog
Alternatively, create a new tag manually (go to Code → Tags → New tag), but this might conflict later when you want to fetch remote tags.
Create a GitLab token
In the project you want to scan, go to User Settings → Access Tokens and create a Personal Access Token with
read_api+read_repositoryscopes (orapiif usingmr_commentorbadge). Project Access Tokens also work: create one inside your project under Settings → Access Tokens with the same scopes and at least Maintainer role. Then go to the project’s Settings → CI/CD → Variables and add the token asGITLAB_TOKEN(masked recommended).The token must belong to a user (or project bot) with Maintainer role (or higher) on the project.
Use the component in your pipelines
workflow:rules:- if: $CI_PIPELINE_SOURCE == "merge_request_event"- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS # prevents duplicate pipelineswhen: never- if: $CI_COMMIT_BRANCH- if: $CI_COMMIT_TAGinclude:- component: gitlab.example.com/infrastructure/plumber/plumber@v0.1.29# inputs:# stage: .pre | by default runs in .pre which only runs if there is at least another CI job in another stageTo update: re-import or manually pull upstream changes.
Fork the project on gitlab.com first, then set up a pull mirror on your self-hosted instance. This way, whenever you fetch upstream changes in your fork, your self-hosted mirror stays in sync automatically.
Fork on gitlab.com
Go to getplumber/plumber on gitlab.com, click Fork, and create a fork under your gitlab.com namespace (e.g.,
your-org/plumber).Create a mirrored project on your self-hosted instance
On your self-hosted GitLab, go to New Project → Import project → Repository by URL and use this URL:
https://gitlab.com/your-org/plumber.git. Choose a group and project name (e.g.,infrastructure/plumber).Set up pull mirroring
In your self-hosted project, go to Settings → Repository → Mirroring repositories. Add the mirror URL
https://gitlab.com/your-org/plumber.git, set the direction to Pull, and add a gitlab.com token withread_repositoryscope if the fork is private. Click Mirror repository.Info
Pull mirroring syncs automatically (every 30 minutes on GitLab Premium, or manually on other tiers). When upstream releases a new version, sync your fork on gitlab.com first, then your self-hosted mirror picks it up.
Enable the CI/CD Catalog
Go to Settings → General and make sure the project has a description (required for CI/CD Catalog). Then expand Visibility, project features, permissions, toggle CI/CD Catalog resource to enabled, and click Save changes.
Publish a release
The mirrored project comes with upstream tags. The preferred method is to run a pipeline on an existing tag to trigger the release:
- Go to CI/CD → Pipelines → Run pipeline
- Select an imported tag (e.g.,
v0.1.29) from the branch/tag dropdown - Click Run pipeline: this creates a release for that tag in the CI/CD Catalog
Alternatively, create a new tag manually via Code → Tags → New tag.
Create a GitLab token
In the project you want to scan, go to User Settings → Access Tokens and create a Personal Access Token with
read_api+read_repositoryscopes (orapiif usingmr_commentorbadge). Project Access Tokens also work: create one inside your project under Settings → Access Tokens with the same scopes and at least Maintainer role. Then go to the project’s Settings → CI/CD → Variables and add the token asGITLAB_TOKEN(masked recommended).The token must belong to a user (or project bot) with Maintainer role (or higher) on the project.
Use the component in your pipelines
workflow:rules:- if: $CI_PIPELINE_SOURCE == "merge_request_event"- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS # prevents duplicate pipelineswhen: never- if: $CI_COMMIT_BRANCH- if: $CI_COMMIT_TAGinclude:- component: gitlab.example.com/infrastructure/plumber/plumber@v0.1.29# inputs:# stage: .pre | by default runs in .pre which only runs if there is at least another CI job in another stage
Compliance Controls
Plumber includes 12 compliance controls. Each can be enabled/disabled and customized in your .plumber.yaml:
| Control | Description |
|---|---|
| Container images must not use forbidden tags | Flags latest, dev, and other mutable tags. Can enforce digest pinning for all images |
| Container images must come from authorized sources | Ensures images come from trusted registries |
| Branch must be protected | Verifies critical branches have proper protection |
| Pipeline must not include hardcoded jobs | Detects jobs defined directly instead of from includes |
| Includes must be up to date | Checks if included templates have newer versions |
| Includes must not use forbidden versions | Prevents mutable version references like main, HEAD |
| Pipeline must include component | Ensures required CI/CD components are included; detects overridden jobs |
| Pipeline must include template | Ensures required templates are included; detects overridden jobs |
| Pipeline must not enable debug trace | Detects CI_DEBUG_TRACE/CI_DEBUG_SERVICES leaking secrets in job logs |
| Pipeline must not use unsafe variable expansion | Detects user-controlled variables in shell re-interpretation contexts (OWASP CICD-SEC-1) |
| Security jobs must not be weakened | Detects security scanning jobs neutralized by allow_failure, rules: overrides, or when: manual (OWASP CICD-SEC-4) |
| Pipeline must not execute unverified scripts | Detects 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 (OWASP CICD-SEC-1). Normal usage like echo $CI_COMMIT_BRANCH is safe; only re-interpretation contexts like eval, sh -c, and bash -c are flagged.
Some legitimate usages (Helm deploys, Terraform, etc.) can be allowed via allowedPatterns:
pipelineMustNotUseUnsafeVariableExpansion: enabled: true dangerousVariables: - CI_MERGE_REQUEST_TITLE - CI_COMMIT_MESSAGE - CI_COMMIT_REF_NAME - CI_COMMIT_BRANCH allowedPatterns: - "helm.*--set.*\\$CI_" - "terraform workspace select.*\\$CI_" - "docker build.*--build-arg.*\\$CI_"See the CLI documentation for the full list of flagged vs. safe patterns.
Security Job Weakening Detection
Detects security scanning jobs (SAST, Secret Detection, Container Scanning, Dependency Scanning, DAST, License Scanning) that have been silently neutralized through local overrides in .gitlab-ci.yml. Maps to OWASP CICD-SEC-4 (Poisoned Pipeline Execution).
Three independent sub-controls:
allowFailureMustBeFalse(default: off): Detectsallow_failure: true. Opt-in because GitLab templates ship with this.rulesMustNotBeRedefined(default: on): Detectsrules:overrides withwhen: neverorwhen: manual.whenMustNotBeManual(default: on): Detectswhen: manualat job level.
securityJobsMustNotBeWeakened: enabled: true securityJobPatterns: - "*-sast" - "secret_detection" - "container_scanning" - "*_dependency_scanning" - "dast" - "dast_*" - "license_scanning" allowFailureMustBeFalse: enabled: false rulesMustNotBeRedefined: enabled: true whenMustNotBeManual: enabled: trueSee the CLI documentation for full details and examples.
Unverified Script Execution Detection
Detects CI/CD jobs that download and immediately execute scripts from the internet without integrity verification. Patterns like curl | bash, wget | sh, or download-then-execute sequences are flagged. Lines that include checksum verification (e.g., sha256sum, gpg --verify) between download and execution are automatically excluded.
Maps to OWASP CICD-SEC-3 (Dependency Chain Abuse) and CICD-SEC-8 (Ungoverned Usage of 3rd Party Services).
pipelineMustNotExecuteUnverifiedScripts: enabled: true trustedUrls: [] # - https://internal-artifacts.example.com/*Add trusted URL patterns to trustedUrls (supports wildcards) to suppress findings for known-good sources. See the CLI documentation for full details.
Tip
See the full configuration reference for all control options.
Selective Control Execution
Run or skip specific controls using their names from the table above. Useful for targeted CI checks or skipping controls that aren’t relevant.
# Run only image-related controlsinclude: - component: gitlab.com/getplumber/plumber/plumber@v0.1.29 inputs: controls: containerImageMustNotUseForbiddenTags,containerImageMustComeFromAuthorizedSources# Run everything except branch protectioninclude: - component: gitlab.com/getplumber/plumber/plumber@v0.1.29 inputs: skip_controls: branchMustBeProtectedControls not selected are reported as skipped in the output. The controls and skip_controls inputs are mutually exclusive.
Customize
Override any input to fit your needs:
include: - component: gitlab.com/getplumber/plumber/plumber@v0.1.29 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
# 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 GitLab report) print_output: true # Print to stdout
# Job behavior stage: test # Run in a different stage allow_failure: true # Don't block pipeline on failure gitlab_token: $MY_CUSTOM_TOKEN # Different variable name verbose: true # Enable debug outputAll Inputs
| Input | Default | Description |
|---|---|---|
server_url | $CI_SERVER_URL | GitLab instance URL |
project_path | $CI_PROJECT_PATH | Project to analyze |
branch | $CI_COMMIT_REF_NAME | Branch to analyze |
gitlab_token | $GITLAB_TOKEN | GitLab API token (requires read_api + read_repository scopes, or api scope if mr_comment or badge is enabled) |
threshold | 100 | Minimum compliance % to pass |
config_file | (auto-detect) | Path to config file (relative to repo root). Auto-detects .plumber.yaml in repo, falls back to default |
output_file | plumber-report.json | Path to write JSON results |
pbom_file | plumber-pbom.json | Path to write PBOM (Pipeline Bill of Materials) output |
pbom_cyclonedx_file | plumber-cyclonedx-sbom.json | Path to write CycloneDX SBOM (auto-uploaded as GitLab report) |
print_output | true | Print text output to stdout |
stage | .pre | Pipeline 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 in your pipeline, set this to test or another stage |
image | getplumber/plumber:0.1 | Docker image to use |
allow_failure | false | Allow job to fail without blocking |
verbose | false | Enable debug output for troubleshooting |
mr_comment | false | Post/update a compliance comment on the merge request (requires api scope) |
badge | false | Create/update a Plumber compliance badge on the project (requires api scope; only runs on default branch) |
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_warnings | false | Treat configuration warnings (unknown keys) as errors (exit 2) |
Configuration
Plumber works out of the box with sensible defaults embedded in the image.
The component automatically detects your configuration using this priority:
config_fileinput set → Uses your specified path (relative to repo root).plumber.yamlin repo root → Uses your repo’s config file- No config found → Uses the default configuration embedded in the container
(Optional) Create a Configuration File
Option A: If you have the CLI installed (via Homebrew, Mise, or binary):
plumber config generateThis generates a default config file that you can customize.
Option B: Create manually based on the default config.
Example Output
Tip
The component automatically generates and uploads a JSON compliance report as a job artifact. It can also produce a PBOM (Pipeline Bill of Materials) and a CycloneDX SBOM — the CycloneDX file is automatically uploaded as a GitLab CycloneDX report for use with security tools. Configure via the pbom_file and pbom_cyclonedx_file inputs.
The output is color-coded in your CI/CD job logs for easy scanning — green for passing controls, red for failures.

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.
include: - component: gitlab.com/getplumber/plumber/plumber@v0.1.29 inputs: mr_comment: true # Requires api scope on token
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. This feature 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.
include: - component: gitlab.com/getplumber/plumber/plumber@v0.1.29 inputs: badge: true # Requires api scope on token
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
| Issue | Solution |
|---|---|
GITLAB_TOKEN environment variable is required | Add GITLAB_TOKEN in Settings → CI/CD → Variables |
401 Unauthorized | Token needs read_api + read_repository scopes, from a Maintainer or higher |
403 Forbidden on MR settings | Expected on non-Premium GitLab; continues without that data |
403 Forbidden on MR comment | Token needs api scope (not read_api) when mr_comment: true |
403 Forbidden on badge | Token needs api scope (not read_api) when badge: true |
| MR comment not posted | mr_comment only works in merge request pipelines (CI_MERGE_REQUEST_IID must be set) |
| Badge not created/updated | Token needs api scope and Maintainer role (or higher) on the project |
| Component not found | For self-hosted GitLab, you must import or mirror the component to your instance |
| Plumber job not running | The 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 push | Add workflow:rules to your .gitlab-ci.yml to prevent duplicate branch + MR pipelines (see Quick Start) |
| Plumber job skipped on branch | The component only runs on merge request events, the default branch, and tags. Open an MR or push to the default branch to trigger it |