Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 32 additions & 12 deletions .plumber.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ controls:
containerImageMustNotUseForbiddenTags:
# Set to false to disable this control
enabled: true

# Tags considered "forbidden" - images using these will be flagged
tags:
- latest
Expand Down Expand Up @@ -54,21 +54,21 @@ controls:
containerImageMustComeFromAuthorizedSources:
# Set to false to disable this control
enabled: true

# Trust official Docker Hub images (e.g., nginx, alpine, python)
# These are images without a username prefix on Docker Hub
trustDockerHubOfficialImages: true

# Trusted registry URLs and patterns (supports wildcards)
# Images matching these patterns will be considered trusted
trustedUrls:
# Docker Hub official images (if trustDockerHubOfficialImages is false)
# - docker.io/library/*

# Common CI/CD tool images
- docker.io/docker:*
- gcr.io/kaniko-project/*

# GitLab registry patterns
- $CI_REGISTRY_IMAGE:*
- $CI_REGISTRY_IMAGE/*
Expand All @@ -79,7 +79,7 @@ controls:

# Security products
- registry.gitlab.com/security-products/*

# Add your organization's trusted registries below:
# - your-registry.example.com/*
# - ghcr.io/your-org/*
Expand All @@ -95,10 +95,10 @@ controls:
branchMustBeProtected:
# Set to false to disable this control
enabled: true

# Require the default branch to be protected
defaultMustBeProtected: true

# Branch name patterns that must be protected (supports wildcards)
namePatterns:
- main
Expand All @@ -109,16 +109,16 @@ controls:
# Add your organization's protected branch patterns below:
# - develop
# - staging

# When false, force push must be disabled on protected branches
allowForcePush: false

# Require code owner approval for changes
codeOwnerApprovalRequired: false

# Minimum access level required to merge (0=No one, 30=Developer, 40=Maintainer)
minMergeAccessLevel: 30

# Minimum access level required to push (0=No one, 30=Developer, 40=Maintainer)
minPushAccessLevel: 40

Expand Down Expand Up @@ -243,3 +243,23 @@ controls:
enabled: false
# required: templates/go/go AND templates/trivy/trivy AND templates/iso27001/iso27001
requiredGroups: []

# ===========================================
# Pipeline must not enable debug trace
# ===========================================
# Detects CI/CD pipelines that set CI_DEBUG_TRACE or CI_DEBUG_SERVICES
# to "true" in global or job-level variables.
#
# When CI_DEBUG_TRACE is enabled, GitLab prints ALL environment variables
# in the job logs, including masked secrets like CI_JOB_TOKEN and any
# custom CI/CD variables. This is a critical security risk.
#
# Best practice: Never enable CI_DEBUG_TRACE in committed CI configuration
pipelineMustNotEnableDebugTrace:
# Set to false to disable this control
enabled: true

# CI/CD variable names that must not be set to "true"
forbiddenVariables:
- CI_DEBUG_TRACE
- CI_DEBUG_SERVICES
23 changes: 20 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Plumber is a compliance scanner for GitLab. It reads your `.gitlab-ci.yml` and r
- Outdated includes/templates
- Forbidden version patterns (e.g., `main`, `HEAD`)
- Missing required components or templates
- Debug trace variables (`CI_DEBUG_TRACE`) leaking secrets in job logs

**How does it work?** Plumber connects to your GitLab instance via API, analyzes your pipeline configuration, and reports any issues it finds. You define what's allowed in a config file (`.plumber.yaml`), and Plumber tells you if your project complies. When running locally from your git repo, Plumber uses your **local `.gitlab-ci.yml`** allowing you to validate changes before pushing.

Expand Down Expand Up @@ -273,7 +274,7 @@ This creates `.plumber.yaml` with sensible [defaults](./.plumber.yaml). Customiz

### Available Controls

Plumber includes 8 compliance controls. Each can be enabled/disabled and customized in [.plumber.yaml](.plumber.yaml):
Plumber includes 9 compliance controls. Each can be enabled/disabled and customized in [.plumber.yaml](.plumber.yaml):

<details>
<summary><b>1. Container images must not use forbidden tags</b></summary>
Expand Down Expand Up @@ -450,6 +451,21 @@ pipelineMustIncludeTemplate:

</details>

<details>
<summary><b>9. Pipeline must not enable debug trace</b></summary>

Detects CI/CD pipelines that set `CI_DEBUG_TRACE` or `CI_DEBUG_SERVICES` to `"true"` in global or job-level variables. When enabled, GitLab prints ALL environment variables in job logs, including masked secrets like `CI_JOB_TOKEN`.

```yaml
pipelineMustNotEnableDebugTrace:
enabled: true
forbiddenVariables:
- CI_DEBUG_TRACE
- CI_DEBUG_SERVICES
```

</details>

### 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.
Expand Down Expand Up @@ -491,6 +507,7 @@ Controls not selected are reported as **skipped** in the output. The `--controls
| `includesMustNotUseForbiddenVersions` |
| `pipelineMustIncludeComponent` |
| `pipelineMustIncludeTemplate` |
| `pipelineMustNotEnableDebugTrace` |
| `pipelineMustNotIncludeHardcodedJobs` |

</details>
Expand Down Expand Up @@ -629,10 +646,10 @@ brew install plumber
To install a specific version:

```bash
brew install getplumber/plumber/plumber@0.1.49
brew install getplumber/plumber/plumber@0.1.51
```

> **Note:** Versioned formulas are keg-only. Use the full path for example `/usr/local/opt/plumber@0.1.49/bin/plumber` or run `brew link plumber@0.1.49` to add it to your PATH.
> **Note:** Versioned formulas are keg-only. Use the full path for example `/usr/local/opt/plumber@0.1.51/bin/plumber` or run `brew link plumber@0.1.51` to add it to your PATH.

### Mise

Expand Down
38 changes: 38 additions & 0 deletions cmd/analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,11 @@ func runAnalyze(cmd *cobra.Command, args []string) error {
controlCount++
}

if result.DebugTraceResult != nil && !result.DebugTraceResult.Skipped {
complianceSum += result.DebugTraceResult.Compliance
controlCount++
}

// Calculate average compliance
// If no controls ran (e.g., data collection failed), compliance is 0% - we can't verify anything
var compliance float64 = 0
Expand Down Expand Up @@ -934,6 +939,39 @@ func outputText(result *control.AnalysisResult, threshold, compliance float64, c
fmt.Println()
}

// Control 9: Pipeline must not enable debug trace
if result.DebugTraceResult != nil {
ctrl := controlSummary{
name: "Pipeline must not enable debug trace",
compliance: result.DebugTraceResult.Compliance,
issues: len(result.DebugTraceResult.Issues),
skipped: result.DebugTraceResult.Skipped,
}
controls = append(controls, ctrl)

printControlHeader("Pipeline must not enable debug trace", result.DebugTraceResult.Compliance, result.DebugTraceResult.Skipped)

if result.DebugTraceResult.Skipped {
fmt.Printf(" %sStatus: SKIPPED (disabled in configuration)%s\n", colorDim, colorReset)
} else {
fmt.Printf(" Variables Checked: %d\n", result.DebugTraceResult.Metrics.TotalVariablesChecked)
fmt.Printf(" Forbidden Found: %d\n", result.DebugTraceResult.Metrics.ForbiddenFound)

if len(result.DebugTraceResult.Issues) > 0 {
fmt.Printf("\n %sForbidden Debug Variables Found:%s\n", colorYellow, colorReset)
for _, issue := range result.DebugTraceResult.Issues {
location := issue.Location
if location == "global" {
fmt.Printf(" %s•%s %s = \"%s\" (global variables)\n", colorYellow, colorReset, issue.VariableName, issue.Value)
} else {
fmt.Printf(" %s•%s %s = \"%s\" (job '%s')\n", colorYellow, colorReset, issue.VariableName, issue.Value, location)
}
}
}
}
fmt.Println()
}

// Summary Section
printSectionHeader("Summary")
fmt.Println()
Expand Down
34 changes: 34 additions & 0 deletions configuration/plumberconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ var validControlSchema = map[string][]string{
"pipelineMustIncludeTemplate": {
"enabled", "required", "requiredGroups",
},
"pipelineMustNotEnableDebugTrace": {
"enabled", "forbiddenVariables",
},
}

// validControlKeys returns the list of known control names.
Expand Down Expand Up @@ -90,6 +93,9 @@ type ControlsConfig struct {

// PipelineMustIncludeTemplate control configuration
PipelineMustIncludeTemplate *RequiredTemplatesControlConfig `yaml:"pipelineMustIncludeTemplate,omitempty"`

// PipelineMustNotEnableDebugTrace control configuration
PipelineMustNotEnableDebugTrace *DebugTraceControlConfig `yaml:"pipelineMustNotEnableDebugTrace,omitempty"`
}

// ImageForbiddenTagsControlConfig configuration for the forbidden image tags control
Expand Down Expand Up @@ -220,6 +226,16 @@ func (c *RequiredComponentsControlConfig) GetResolvedRequiredGroups() ([][]strin
return c.RequiredGroups, nil
}

// DebugTraceControlConfig configuration for the debug trace detection control
type DebugTraceControlConfig struct {
// Enabled controls whether this check runs
Enabled *bool `yaml:"enabled,omitempty"`

// ForbiddenVariables is a list of CI/CD variable names that must not be set to "true"
// Defaults: CI_DEBUG_TRACE, CI_DEBUG_SERVICES
ForbiddenVariables []string `yaml:"forbiddenVariables,omitempty"`
}

// RequiredTemplatesControlConfig configuration for the required templates control
type RequiredTemplatesControlConfig struct {
// Enabled controls whether this check runs
Expand Down Expand Up @@ -466,6 +482,24 @@ func (c *PlumberConfig) GetPipelineMustIncludeTemplateConfig() *RequiredTemplate
return c.Controls.PipelineMustIncludeTemplate
}

// GetPipelineMustNotEnableDebugTraceConfig returns the control configuration
// Returns nil if not configured
func (c *PlumberConfig) GetPipelineMustNotEnableDebugTraceConfig() *DebugTraceControlConfig {
if c == nil {
return nil
}
return c.Controls.PipelineMustNotEnableDebugTrace
}

// IsEnabled returns whether the control is enabled
// Returns false if not properly configured
func (c *DebugTraceControlConfig) IsEnabled() bool {
if c == nil || c.Enabled == nil {
return false
}
return *c.Enabled
}

// IsEnabled returns whether the control is enabled
// Returns false if not properly configured
func (c *RequiredTemplatesControlConfig) IsEnabled() bool {
Expand Down
1 change: 1 addition & 0 deletions configuration/plumberconfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,7 @@ func TestValidControlNames(t *testing.T) {
"includesMustNotUseForbiddenVersions",
"pipelineMustIncludeComponent",
"pipelineMustIncludeTemplate",
"pipelineMustNotEnableDebugTrace",
"pipelineMustNotIncludeHardcodedJobs",
}

Expand Down
Loading