Is your feature request related to a problem? Please describe.
Without mandatory MR approvals, an attacker can modify the .gitlab-ci.yml or any script executed by the pipeline (Direct/Indirect PPE), push it, and the pipeline will execute the malicious code before any human review. This is the root cause of two major CI/CD attack vectors:
- Direct PPE: Attacker modifies
.gitlab-ci.yml on a branch → pipeline runs with secrets
- Indirect PPE: Attacker modifies a script called by the pipeline (e.g.,
scripts/run-tests.sh) via a MR → pipeline executes the modified version before approval
The Protection data collector already retrieves MRApprovalRules, MRApprovalSettings, and MRSettings from the GitLab API, but no control currently exploits this data.
Describe the solution you'd like
Add a new control mrApprovalMustBeRequired that verifies:
- MR approvals are required (
approvalsBeforeMerge >= N)
- Authors cannot approve their own MR (
mergeRequestsAuthorApproval == false)
- Approvals reset on new push (
resetApprovalsOnPush == true) — prevents approved MR from being modified after approval
- Committers cannot approve (
mergeRequestsDisableCommittersApproval == true)
Configuration in .plumber.yaml
controls:
mrApprovalMustBeRequired:
enabled: true
# Minimum number of approvals required
minApprovals: 1
# Author must not be able to approve their own MR
preventAuthorApproval: true
# Approvals must reset when new commits are pushed
resetApprovalsOnPush: true
# Committers must not be able to approve
preventCommitterApproval: true
Implementation Hints
These are just ideas. Feel free to change the implementation.
- Data already collected: The
collector/dataCollectionGitlabProtection.go already fetches MRApprovalRules, MRApprovalSettings, and MRSettings via the GitLab API. This data is passed to controls via GitlabProtectionData.
- New control file: Create
control/controlGitlabProtectionMRApproval.go.
- Logic: Compare the project's MR approval settings against the configured requirements. Each sub-check that fails reduces compliance proportionally.
- Compliance calculation: Each enabled sub-check that passes contributes equally. For example, if 4 checks are enabled and 3 pass, compliance = 75%.
- Integration: Wire in
task.go alongside the existing branchMustBeProtected control (they share the same Protection data collector).
Files Touched
control/controlGitlabProtectionMRApproval.go (new control)
control/types.go (add result field to AnalysisResult)
control/task.go (wire the new control, reuse existing Protection data collection)
configuration/plumberconfig.go (add config struct and getter)
.plumber.yaml (add default config section)
cmd/analyze.go (add output formatting)
Why It's Valuable
MR approval enforcement is a foundational security control that prevents both Direct and Indirect PPE attacks. The data is already being collected by plumber but not used — this control would unlock that existing investment with minimal effort. It complements branchMustBeProtected by covering the merge request workflow rather than just branch-level protections.
Is your feature request related to a problem? Please describe.
Without mandatory MR approvals, an attacker can modify the
.gitlab-ci.ymlor any script executed by the pipeline (Direct/Indirect PPE), push it, and the pipeline will execute the malicious code before any human review. This is the root cause of two major CI/CD attack vectors:.gitlab-ci.ymlon a branch → pipeline runs with secretsscripts/run-tests.sh) via a MR → pipeline executes the modified version before approvalThe
Protectiondata collector already retrievesMRApprovalRules,MRApprovalSettings, andMRSettingsfrom the GitLab API, but no control currently exploits this data.Describe the solution you'd like
Add a new control
mrApprovalMustBeRequiredthat verifies:approvalsBeforeMerge >= N)mergeRequestsAuthorApproval == false)resetApprovalsOnPush == true) — prevents approved MR from being modified after approvalmergeRequestsDisableCommittersApproval == true)Configuration in
.plumber.yamlImplementation Hints
These are just ideas. Feel free to change the implementation.
collector/dataCollectionGitlabProtection.goalready fetchesMRApprovalRules,MRApprovalSettings, andMRSettingsvia the GitLab API. This data is passed to controls viaGitlabProtectionData.control/controlGitlabProtectionMRApproval.go.task.goalongside the existingbranchMustBeProtectedcontrol (they share the sameProtectiondata collector).Files Touched
control/controlGitlabProtectionMRApproval.go(new control)control/types.go(add result field toAnalysisResult)control/task.go(wire the new control, reuse existing Protection data collection)configuration/plumberconfig.go(add config struct and getter).plumber.yaml(add default config section)cmd/analyze.go(add output formatting)Why It's Valuable
MR approval enforcement is a foundational security control that prevents both Direct and Indirect PPE attacks. The data is already being collected by plumber but not used — this control would unlock that existing investment with minimal effort. It complements
branchMustBeProtectedby covering the merge request workflow rather than just branch-level protections.