Documentation
¶
Index ¶
- Constants
- func AggregateIssueCodeCounts(result *AnalysisResult) map[ErrorCode]int
- func ApplyGitHubFindingCounts(stats *GitHubAnalysisStats, findings []opaengine.Finding)
- func ControlPassesFilter(name string, includeOnly, skip []string) bool
- func CriticalIssueCodesSorted(result *AnalysisResult) []string
- func DisabledControlNames(c *configuration.ControlsConfig) map[string]bool
- func FilterFindingsByEnabledControls(findings []opaengine.Finding, provider string, c *configuration.ControlsConfig, ...) []opaengine.Finding
- func FindingsByControl(findings []opaengine.Finding) map[string][]opaengine.Finding
- func GitHubControlCompliance(_ string, _ *GitHubAnalysisStats, findings int) float64
- func IsRegoFileBenchedForProvider(content []byte, provider string) bool
- func ManageMergeRequestComment(projectID int, mrIID int, result *AnalysisResult, ...) error
- func ManageProjectBadge(projectID int, conf *configuration.Configuration, ps *PlumberScoreResult) error
- func MarkSkippedByFilter(entries []ControlEntry, includeOnly, skip []string)
- func ScoreBadgeURL(letter string) string
- func ScoreLetterMeaning(letter string) string
- func ScoreLetterRank(letter string) int
- type AnalysisResult
- type CodeLoss
- type ControlEntry
- type ErrorCode
- type ErrorCodeInfo
- type GitHubAnalysisStats
- type IssueSeverity
- type PipelineImageMetricsSummary
- type PipelineOriginMetricsSummary
- type PlumberScoreResult
- type SeverityCounts
- type SeverityLoss
- type StatLine
Constants ¶
const ( // MRCommentIdentifier is an invisible HTML comment used to find the Plumber // comment in the merge request notes so it can be updated on subsequent // runs. The historical wording is kept on purpose: changing it would stop // matching comments posted by older versions and create duplicates. MRCommentIdentifier = "<!-- Plumber Compliance Comment -->" )
const PlumberScoreDocURL = "https://github.com/getplumber/plumber/blob/main/docs/scoring.md"
PlumberScoreDocURL is the canonical user-facing explanation of the Plumber letter score.
const PlumberScoreProfileID = "scoring-v3"
PlumberScoreProfileID identifies the scoring rules version (see docs/scoring.md).
Variables ¶
This section is empty.
Functions ¶
func AggregateIssueCodeCounts ¶ added in v0.2.22
func AggregateIssueCodeCounts(result *AnalysisResult) map[ErrorCode]int
AggregateIssueCodeCounts walks analysis issues and counts occurrences per ErrorCode. This is the input expected by ComputePlumberScore in scoring-v3 (per-code caps).
func ApplyGitHubFindingCounts ¶ added in v0.3.26
func ApplyGitHubFindingCounts(stats *GitHubAnalysisStats, findings []opaengine.Finding)
ApplyGitHubFindingCounts fills per-control finding counters on stats after Rego evaluation (denominators come from AggregateGitHubStats).
func ControlPassesFilter ¶ added in v0.3.0
ControlPassesFilter applies the --controls / --skip-controls semantics for one control name. When includeOnly is non-empty, only listed controls pass; skip removes controls from the survivor set. The two flags are mutually exclusive at the CLI level (cmd/analyze_gitlab.go), but this helper handles either or both for callers that don't enforce that.
func CriticalIssueCodesSorted ¶ added in v0.1.83
func CriticalIssueCodesSorted(result *AnalysisResult) []string
CriticalIssueCodesSorted returns unique Critical-level issue codes present in the analysis, sorted.
func DisabledControlNames ¶ added in v0.3.0
func DisabledControlNames(c *configuration.ControlsConfig) map[string]bool
DisabledControlNames returns the set of control names treated as skipped for the supplied ControlsConfig — both explicit disables (cfg present with IsEnabled() == false) and absent entries (cfg == nil). Matching v0.2.x exactly: at tag v0.2.22, every legacy control wrapper in `control/controlGitlab*.go` sets `p.Enabled = false` when its section is missing from .plumber.yaml, so the Rego port drops those findings via FilterFindingsByEnabledControls. Issue #158 was specifically about v0.3.0-beta.2 keeping absent-control Rego findings in the score; this function feeds the filter to close that path. For securityJobsMustNotBeWeakened the "skipped" call also covers the case where every sub-check is explicitly off — see isSecurityJobsWeakenedSkipped. Pass the right provider's ControlsConfig (use pc.ControlsFor("gitlab") or pc.ControlsFor("github")). DisabledControlNames returns the set of control names that are disabled (nil or enabled:false) in the given ControlsConfig. It derives the list from the same spec tables used by GitLabControls/GitHubControls so the two sources of truth stay in sync automatically.
func FilterFindingsByEnabledControls ¶ added in v0.3.0
func FilterFindingsByEnabledControls(findings []opaengine.Finding, provider string, c *configuration.ControlsConfig, includeOnly, skip []string) []opaengine.Finding
FilterFindingsByEnabledControls drops findings whose ControlName is either (a) currently benched for the given provider — see IsBenched in registry.go — or (b) treated as skipped for the supplied ControlsConfig (explicitly disabled OR absent from the user's config; see DisabledControlNames). Findings whose code is unknown or has no ControlName are kept (defensive: better surfaced than silently swallowed). When the supplied ControlsConfig is nil no skipped-controls claim is made and all findings pass through; callers that want strict skipping must pass a non-nil config. Pass the provider name ("gitlab" or "github") and the matching ControlsConfig (use pc.ControlsFor(provider)).
func FindingsByControl ¶ added in v0.3.0
FindingsByControl groups Rego findings by their declared ControlName (from the issue-code registry). Findings whose code has no registry entry land under the "" key so the caller can still surface them if they want. The map values preserve the input order so downstream tables read deterministically.
func GitHubControlCompliance ¶ added in v0.3.65
func GitHubControlCompliance(_ string, _ *GitHubAnalysisStats, findings int) float64
GitHubControlCompliance returns the binary compliance percentage for a single GitHub control: 100 when no findings, 0 otherwise. The stats parameter is reserved for future per-control percentage overrides.
func IsRegoFileBenchedForProvider ¶ added in v0.3.0
IsRegoFileBenchedForProvider returns true when every ISSUE-XXX referenced in content maps to a control name that is currently benched for the given provider (see configuration.IsBenched). When true, the engine should skip loading the file entirely — the policy never executes, no cycles wasted.
Returns false (i.e. "load this file") in any of:
- the file references no ISSUE codes (helper modules, placeholders);
- the file references at least one code mapping to a control that is NOT benched for this provider;
- any ISSUE code is unknown to errorCodeRegistry (defensive: an unknown code is treated as not-bench so the rule still runs and surfaces — better noisy than silently dropped).
The decision is made entirely from existing data: errorCodeRegistry (issue code → control name) and configuration.benchedControls ({provider, control} → benched). No separate file→package mapping lives anywhere; the rego file's own issue-code references are the link.
func ManageMergeRequestComment ¶ added in v0.1.42
func ManageMergeRequestComment( projectID int, mrIID int, result *AnalysisResult, pc *configuration.PlumberConfig, passed bool, gateLine string, conf *configuration.Configuration, score *PlumberScoreResult, scoreMode bool, scorePointMode bool, ) error
ManageMergeRequestComment creates or updates the Plumber comment on the given merge request. projectID and gitlabURL come from the already-resolved configuration/result; only mrIID is CI-specific. passed is the run's gate verdict and gateLine its human-readable rendering.
func ManageProjectBadge ¶ added in v0.1.42
func ManageProjectBadge( projectID int, conf *configuration.Configuration, ps *PlumberScoreResult, ) error
ManageProjectBadge creates or updates the Plumber badge on the project. The badge shows the Plumber letter score (A–E, see ScoreBadgeURL) and links to the score documentation. No-op when no score is available.
func MarkSkippedByFilter ¶ added in v0.3.0
func MarkSkippedByFilter(entries []ControlEntry, includeOnly, skip []string)
MarkSkippedByFilter mutates entries in place, setting Skipped=true for any control filtered out by --controls / --skip-controls. Called from the renderer so the compliance table shows filtered controls as "skipped" instead of pretending they ran with 100 % compliance. Already-skipped entries (disabled in .plumber.yaml) stay skipped.
func ScoreBadgeURL ¶ added in v0.1.83
ScoreBadgeURL builds a Shields.io badge URL showing the Plumber letter score (A–E).
func ScoreLetterMeaning ¶ added in v0.2.5
ScoreLetterMeaning returns a short human-readable description of what a letter score implies about the pipeline. It is used by CLI banners, merge request comments, and documentation so wording stays consistent.
func ScoreLetterRank ¶ added in v0.4.0
ScoreLetterRank orders letter scores for gate comparisons: A=5 … E=1, 0 for anything unknown, so "at least a B" is Rank(got) >= Rank("B").
Types ¶
type AnalysisResult ¶
type AnalysisResult struct {
// Project information
ProjectPath string `json:"projectPath"`
ProjectID int `json:"projectId"`
DefaultBranch string `json:"defaultBranch"`
// AnalyzeBranch is the branch the analysis actually ran against
// (--branch or the project's default). May differ from DefaultBranch.
AnalyzeBranch string `json:"analyzeBranch,omitempty"`
// HeadCommitSha is the head SHA of the analyzed branch, when known.
// Used to build stable remote source links so that, even if the
// branch moves later, links in the artifact still point at the
// exact code that produced the finding. Empty when the SHA could
// not be resolved (e.g. local-only runs without a fetched HEAD).
HeadCommitSha string `json:"headCommitSha,omitempty"`
// CI configuration status
CiValid bool `json:"ciValid"`
CiMissing bool `json:"ciMissing"`
CiErrors []string `json:"ciErrors,omitempty"` // Specific CI config errors from GitLab
CIConfigSource string `json:"ciConfigSource"` // "local" or "remote"
// Pipeline origin data
PipelineOriginMetrics *PipelineOriginMetricsSummary `json:"pipelineOriginMetrics,omitempty"`
// Pipeline image data
PipelineImageMetrics *PipelineImageMetricsSummary `json:"pipelineImageMetrics,omitempty"`
// Findings from the Rego/OPA rule engine. Single source of truth
// for compliance results since all legacy Go controls were retired.
Findings []opaengine.Finding `json:"findings,omitempty"`
// Raw collected data (not included in JSON output, used for PBOM generation
// and for the per-control aggregated stats block printed under each
// control header in the terminal output).
PipelineImageData *gitlab.GitlabPipelineImageData `json:"-"`
PipelineOriginData *gitlab.GitlabPipelineOriginData `json:"-"`
ProtectionData *gitlab.GitlabProtectionAnalysisData `json:"-"`
// GitHubStats holds per-control denominators computed from the
// GitHub IR after a GitHub analysis. Used by the GitHub renderer
// to produce per-control stats blocks ("Total Images: 19,
// Pinned By Digest: 1, …") and per-control compliance
// percentages, matching the GitLab output structure. Nil on the
// GitLab path.
GitHubStats *GitHubAnalysisStats `json:"-"`
// GitHubPipeline is the normalized IR produced by the GitHub
// collector, retained on the result so legacy JSON / PBOM /
// CycloneDX builders can read images, action references, and
// per-branch protection details without re-running the gitlab.
// Nil on the GitLab path.
GitHubPipeline *ir.NormalizedPipeline `json:"-"`
// Warnings holds non-fatal "could not verify" messages from the run,
// e.g. a known-CVE check skipped because an action's pinned commit
// could not be resolved to a version (tag list blocked by an org IP
// allow list, rate limit, or network). Surfaced in the terminal,
// JSON, SARIF and GLSAST output, and gated by --fail-warnings (exit
// 3) so a degraded check is visible instead of silently passing.
Warnings []string `json:"warnings,omitempty"`
// DataCollectionDegraded is set when a collection or enrichment step
// failed mid-run, so the analysis ran on incomplete data: a GitLab
// merged-CI fetch that timed out (empty pipeline), or a GitHub run
// where some workflow files or the branch-protection fetch could not
// be retrieved. When true the renderer withholds the letter-score
// banner and marks the un-collected controls "not evaluated" instead
// of presenting missing data as 100% compliant (#220). Distinct from
// CiMissing, which is the legitimate "this project has no CI config"
// state and is not degraded.
DataCollectionDegraded bool `json:"dataCollectionDegraded,omitempty"`
// DegradedReasons lists the human-readable collection/enrichment
// failures behind DataCollectionDegraded (e.g. "3 workflow file(s)
// could not be fetched", "branch protection could not be fetched").
// Surfaced as a caveat in the terminal. Empty when not degraded.
DegradedReasons []string `json:"degradedReasons,omitempty"`
}
AnalysisResult holds the complete result of a pipeline analysis
func RunAnalysis ¶
func RunAnalysis(conf *configuration.Configuration) (*AnalysisResult, error)
RunAnalysis executes the complete pipeline analysis for a GitLab project
func RunGitHubAnalysis ¶ added in v0.3.0
func RunGitHubAnalysis(conf *configuration.Configuration) (*AnalysisResult, error)
RunGitHubAnalysis is the GitHub counterpart of RunAnalysis. It scans .github/workflows/*.{yml,yaml} under conf.GitRepoRoot, evaluates the embedded Rego policies against the resulting IR, and returns an AnalysisResult whose only populated fields are the project metadata and Findings. No legacy Go control fields are set — GitHub support is Rego-only by design.
func RunGitHubAnalysisRemote ¶ added in v0.3.0
func RunGitHubAnalysisRemote(conf *configuration.Configuration, owner, repo, ref string) (*AnalysisResult, error)
RunGitHubAnalysisRemote is the upstream-fetch counterpart of RunGitHubAnalysis. Instead of walking conf.GitRepoRoot, it fetches `.github/workflows/*.{yml,yaml}` from the provided owner/repo via the GitHub Contents API and runs the same Rego pipeline against the resulting IR. Used by `plumber analyze --github-url X --project owner/repo` when the user does not have a local clone.
Auth is mandatory in remote mode (GH_TOKEN / GH_ENTERPRISE_TOKEN / GITHUB_TOKEN / gh auth login) — without it the Contents API rate-limits aggressively and returns 403 on private repos. Repo- side artefacts that need a local checkout (Dockerfiles, dependabot.yml, SECURITY.md) are not collected; controls that depend on them simply produce no findings.
type CodeLoss ¶ added in v0.2.22
type CodeLoss struct {
Code ErrorCode `json:"code"`
Severity IssueSeverity `json:"severity"`
Count int `json:"count"`
Weight float64 `json:"weight"`
Cap float64 `json:"cap,omitempty"` // omitted when infinite (critical)
UncappedLoss float64 `json:"uncappedLoss"`
CappedLoss float64 `json:"cappedLoss"`
}
CodeLoss is the points lost for a single issue code after weight, log growth, and per-severity cap. scoring-v3 caps loss per (code), so distinct types at the same severity each contribute their own bucket.
type ControlEntry ¶ added in v0.3.0
type ControlEntry struct {
DisplayName string
ControlName string
Skipped bool
// SkipReason is the human-friendly explanation rendered next to
// the "(skipped)" header and in the Status: SKIPPED (…) line. Left
// empty for the default "disabled in configuration" case; callers
// that flip Skipped for a different reason (e.g. a required data
// source was unavailable) set this so the operator sees why.
SkipReason string
Compliance float64
}
ControlEntry is the canonical per-control view consumed by the analyze renderer, the MR comment builder and any future output path. Compliance is derived from the Rego Findings list (binary: 100 when no finding matches the ControlName, 0 when at least one does); Skipped reflects whether the user disabled the control in .plumber.yaml. DisplayName is the user-facing title.
func ApplyFindings ¶ added in v0.3.0
func ApplyFindings(entries []ControlEntry, findingsByControl map[string]int) []ControlEntry
ApplyFindings fills in Compliance for each catalog entry based on whether any finding matches its ControlName. The rule is binary: 100 when the control fires no finding (or is skipped), 0 otherwise.
func GitHubControls ¶ added in v0.3.0
func GitHubControls(pc *configuration.PlumberConfig) []ControlEntry
GitHubControls returns the catalog of GitHub Actions controls in their canonical display order — the same shape GitLabControls uses. Every non-benched GitHub control is returned. Skip semantics mirror GitLabControls and v0.2.x's GitLab legacy code: absent from `github.controls.*` OR `enabled: false` (or empty entry) → Skipped=true. Benched controls remain invisible because their findings never reach the catalog.
func GitLabControls ¶ added in v0.3.0
func GitLabControls(pc *configuration.PlumberConfig) []ControlEntry
type ErrorCode ¶ added in v0.1.67
type ErrorCode string
ErrorCode represents a unique Plumber issue code (ISSUE-XXX format).
const ( CodeImageUnauthorizedSource ErrorCode = "ISSUE-101" // ISSUE-102: Container image uses a forbidden tag (e.g., latest, dev) CodeImageForbiddenTag ErrorCode = "ISSUE-102" // ISSUE-103: Container image is not pinned by digest CodeImageNotPinnedByDigest ErrorCode = "ISSUE-103" // ISSUE-701: Third-party GitHub Action reference is not pinned by commit SHA CodeActionUnpinned ErrorCode = "ISSUE-701" // ISSUE-704: Container registry password is hard-coded in the workflow CodeContainerHardcodedCredentials ErrorCode = "ISSUE-704" // ISSUE-705: Release/publish workflow primes a build cache from attacker-controlled artifacts CodeCachePoisoning ErrorCode = "ISSUE-705" // ISSUE-702: Action is hosted in an archived GitHub repository CodeActionArchivedRepo ErrorCode = "ISSUE-702" // ISSUE-707: Pinned commit SHA does not exist in the action's upstream repository CodeImpostorCommit ErrorCode = "ISSUE-707" // ISSUE-708: `# vX.Y.Z` comment does not match the SHA the ref resolves to CodeRefVersionMismatch ErrorCode = "ISSUE-708" // ISSUE-709: Action pinned by SHA is stale vs the latest upstream release CodeStaleActionRef ErrorCode = "ISSUE-709" // ISSUE-703: Action version carries a published security advisory CodeKnownVulnerableAction ErrorCode = "ISSUE-703" // ISSUE-711: Third-party action duplicates a runner built-in (gh CLI, etc.) CodeSuperfluousAction ErrorCode = "ISSUE-711" // ISSUE-706: Dockerfile FROM reference is not pinned by digest CodeDockerfileUnpinnedBase ErrorCode = "ISSUE-706" // ISSUE-712: Release / publish workflow produces unsigned artefacts CodeReleaseWorkflowUnsigned ErrorCode = "ISSUE-712" CodeActionUnauthorizedSource ErrorCode = "ISSUE-713" )
Issue codes for container image controls (1xx)
const ( // ISSUE-203: Pipeline enables CI debug trace (CI_DEBUG_TRACE or CI_DEBUG_SERVICES) CodeDebugTraceEnabled ErrorCode = "ISSUE-203" // ISSUE-204: Unsafe variable expansion in shell re-interpretation context (eval, sh -c, etc.) CodeUnsafeVariableExpansion ErrorCode = "ISSUE-204" // ISSUE-205: A variable that should only be set in CI/CD Settings is overridden in the pipeline config CodeJobVariableOverridden ErrorCode = "ISSUE-205" // ISSUE-207: Workflow inlines user-controlled template expressions into a run: script CodeTemplateInjection ErrorCode = "ISSUE-207" // ISSUE-208: Workflow re-enables deprecated GitHub Actions workflow commands CodeInsecureCommands ErrorCode = "ISSUE-208" // ISSUE-209: Workflow writes untrusted content to $GITHUB_ENV or $GITHUB_PATH CodeGitHubEnvInjection ErrorCode = "ISSUE-209" // ISSUE-210: Workflow gates behaviour on a spoofable actor/bot check CodeBotConditions ErrorCode = "ISSUE-210" // ISSUE-211: Workflow `if:` condition is logically unsound (always true/false, tautology) CodeUnsoundCondition ErrorCode = "ISSUE-211" // ISSUE-212: Workflow misuses the `contains()` built-in (argument order, type) CodeUnsoundContains ErrorCode = "ISSUE-212" // ISSUE-215: Workflow expands a `vars.*` template into a shell script CodeTemplateInjectionVars ErrorCode = "ISSUE-215" // ISSUE-213: Workflow exports the whole `github` context via toJson(github) CodeUnsafeGithubContextDump ErrorCode = "ISSUE-213" // ISSUE-214: Workflow installs a package without pinning a version / lockfile CodeUnpinnedPackageInstall ErrorCode = "ISSUE-214" )
Issue codes for CI/CD variable controls (2xx)
const ( // ISSUE-302: Reusable workflow called with `secrets: inherit` CodeSecretsInherit ErrorCode = "ISSUE-302" // ISSUE-303: Secret dereferenced via fromJSON bypasses log redaction CodeUnredactedSecrets ErrorCode = "ISSUE-303" // ISSUE-801: Workflow grants no explicit permissions, relying on the repo default CodeUndocumentedPermissions ErrorCode = "ISSUE-801" // ISSUE-305: Secret used without an environment gate CodeSecretsOutsideEnv ErrorCode = "ISSUE-305" // ISSUE-306: GitHub App token issued with revocation disabled CodeGitHubAppSkipRevoke ErrorCode = "ISSUE-306" // ISSUE-307: Checkout persists credentials in .git/config (artipacked) CodeArtipacked ErrorCode = "ISSUE-307" // ISSUE-308: Workflow reads a secret via a dynamic index (secrets[expr]) CodeSecretsDynamicIndex ErrorCode = "ISSUE-308" // ISSUE-309: Workflow exfiltrates the entire secrets context via toJson(secrets). // Moved from ISSUE-301 in the 301/309 swap so the leaked-secrets rule could // take the slot already used by the downstream jobs platform. CodeOverprovisionedSecrets ErrorCode = "ISSUE-309" )
Issue codes for secret and credential handling controls (3xx)
const ( // ISSUE-401: Job is hardcoded (not sourced from include/component) CodeJobHardcoded ErrorCode = "ISSUE-401" // ISSUE-402: External CI ref collides with both a tag and a branch upstream // (GitLab include/component @ref, GitHub action/reusable-workflow @ref) CodeRefConfusion ErrorCode = "ISSUE-402" // ISSUE-403: Include uses an outdated version CodeIncludeOutdated ErrorCode = "ISSUE-403" // ISSUE-404: Include uses a forbidden version CodeIncludeForbiddenVersion ErrorCode = "ISSUE-404" // ISSUE-405: Required template is missing from the pipeline CodeTemplateMissing ErrorCode = "ISSUE-405" // ISSUE-406: Required template jobs are overridden CodeTemplateOverridden ErrorCode = "ISSUE-406" // ISSUE-408: Required component is missing from the pipeline CodeComponentMissing ErrorCode = "ISSUE-408" // ISSUE-409: Required component jobs are overridden CodeComponentOverridden ErrorCode = "ISSUE-409" // ISSUE-410: Security job is weakened (allow_failure, rules override, when: manual) CodeSecurityJobWeakened ErrorCode = "ISSUE-410" // ISSUE-411: Pipeline executes unverified scripts (curl|bash, base64|bash, download-then-exec, |sh) CodeUnverifiedScriptExecution ErrorCode = "ISSUE-411" // ISSUE-412: CI/CD job uses a Docker-in-Docker (dind) service CodeDockerInDockerUsage ErrorCode = "ISSUE-412" // ISSUE-413: CI/CD job uses Docker-in-Docker with insecure daemon configuration CodeDockerInDockerInsecure ErrorCode = "ISSUE-413" // ISSUE-802: Job reaches a dangerous trigger (workflow_run, issue_comment, // pull_request_review*, discussion*, gollum, fork) AND checks out fork // content. pull_request_target is owned by ISSUE-804. CodeDangerousTriggers ErrorCode = "ISSUE-802" // ISSUE-804: pull_request_target workflow explicitly checks out the PR head (tj-actions pattern) CodePullRequestTargetWithHeadCheckout ErrorCode = "ISSUE-804" // ISSUE-417: Required action or reusable workflow is not referenced anywhere in the project's workflows CodeRequiredActionMissing ErrorCode = "ISSUE-417" )
Issue codes for pipeline composition controls (4xx)
const ( // ISSUE-601: Workflow has no explicit `name:` field CodeAnonymousDefinition ErrorCode = "ISSUE-601" // ISSUE-418: Workflow has no `concurrency:` block at either level CodeMissingConcurrency ErrorCode = "ISSUE-418" // ISSUE-419: Workflow uses a misfeature pattern (shell: cmd, inline pip install curl|sh, …) CodeWorkflowMisfeature ErrorCode = "ISSUE-419" // ISSUE-420: Workflow script contains obfuscation (zero-width / non-ASCII unicode, bidi) CodeWorkflowObfuscation ErrorCode = "ISSUE-420" // ISSUE-421: PyPI / npm publish relies on a static token instead of OIDC trusted publishing CodeUseTrustedPublishing ErrorCode = "ISSUE-421" // ISSUE-901: dependabot.yml re-enables insecure external code execution CodeDependabotInsecureExec ErrorCode = "ISSUE-901" // ISSUE-902: dependabot.yml update ecosystem has no cooldown window CodeDependabotMissingCooldown ErrorCode = "ISSUE-902" // ISSUE-903: Repository has workflows but no dependency update tool configured CodeDependencyUpdateToolMissing ErrorCode = "ISSUE-903" // ISSUE-904: Repository has workflows but none runs a SAST scanner CodeSASTWorkflowMissing ErrorCode = "ISSUE-904" // ISSUE-905: Repository has workflows but no SECURITY.md policy file CodeSecurityPolicyMissing ErrorCode = "ISSUE-905" )
Issue codes for workflow-hygiene controls (6xx)
const ( // ISSUE-501: Branch is not protected CodeBranchUnprotected ErrorCode = "ISSUE-501" // ISSUE-505: Branch has non-compliant protection settings CodeBranchNonCompliant ErrorCode = "ISSUE-505" // ISSUE-803: Job runs with overly broad permissions (write-all) CodeExcessivePermissions ErrorCode = "ISSUE-803" )
Issue codes for access and authorization controls (5xx)
type ErrorCodeInfo ¶ added in v0.1.67
type ErrorCodeInfo struct {
// Code is the unique issue code (e.g., ISSUE-102).
Code ErrorCode `json:"code"`
// Severity reflects potential impact (see documentation); used for Plumber Score.
Severity IssueSeverity `json:"severity"`
// Title is a short human-readable title used by SARIF / GLSAST / JSON
// renderers when no provider-specific override is set. Acts as the
// default; the catalog DisplayName covers the terminal renderer's
// per-provider naming separately.
Title string `json:"title"`
// Description explains what the issue is. Same fallback semantics as
// Title: rendered as-is when no provider override is set.
Description string `json:"description"`
// Remediation provides guidance on how to fix the issue.
Remediation string `json:"remediation"`
// DocURL is a direct link to the documentation for this issue.
DocURL string `json:"docUrl"`
// ControlName is the .plumber.yaml control key this code belongs to.
ControlName string `json:"controlName"`
// TitleByProvider lets cross-provider controls render different titles
// in SARIF / JSON / GLSAST depending on which provider produced the
// finding (mirrors the per-provider DisplayName already supported by
// the catalog). Keyed by "gitlab" / "github". An empty map (the common
// case) means TitleFor() falls back to Title for every provider.
TitleByProvider map[string]string `json:"titleByProvider,omitempty"`
// DescriptionByProvider works the same way for the longer description.
DescriptionByProvider map[string]string `json:"descriptionByProvider,omitempty"`
}
ErrorCodeInfo provides metadata about an issue code.
func AllCodes ¶ added in v0.1.67
func AllCodes() []ErrorCodeInfo
AllCodes returns all registered issue codes sorted by code.
func LookupCode ¶ added in v0.1.67
func LookupCode(code ErrorCode) *ErrorCodeInfo
LookupCode returns the ErrorCodeInfo for a given issue code, or nil if not found.
func (ErrorCodeInfo) DescriptionFor ¶ added in v0.3.42
func (i ErrorCodeInfo) DescriptionFor(provider string) string
DescriptionFor mirrors TitleFor for the description.
func (ErrorCodeInfo) TitleFor ¶ added in v0.3.42
func (i ErrorCodeInfo) TitleFor(provider string) string
TitleFor returns the title to render for a finding emitted under the given provider ("gitlab" / "github"). Falls back to Title if no override is registered for the provider, so codes that don't need per-provider divergence stay a single Title declaration.
type GitHubAnalysisStats ¶ added in v0.3.0
type GitHubAnalysisStats struct {
// Actions pinning (ISSUE-701).
ActionRefsTotal int
ActionRefsUnpinned int
ActionRefsExempt int
// Actions supply-chain (ISSUE-702, ISSUE-703, ISSUE-402). Counted
// across every `uses:` entry that has API metadata, regardless of
// the pin-by-SHA trusted-owner exemption — the rules themselves do
// not exempt. In practice trusted-owner refs have nil metadata
// (enrichment skips them) and won't add to either count.
ActionRefsArchived int
ActionRefsVulnerable int
ActionRefsAmbiguous int
// ActionRefsAbsentUpstream (ISSUE-707) counts SHA-pinned refs the
// upstream repo confirmed do not exist. Guarded on isShaPinned so it
// stays aligned with the impostor-commit rego, which only fires on a
// 40-char SHA.
ActionRefsAbsentUpstream int
// Container images (ISSUE-102 / ISSUE-103).
ImagesTotal int
ImagesPinnedByDigest int
ImagesUsingForbidden int
// Docker-in-Docker (ISSUE-412 / ISSUE-413).
JobsTotal int
JobsWithDinD int
JobsWithInsecureDaemon int
// Excessive permissions (ISSUE-803). Counts jobs whose effective
// `permissions:` is the literal `write-all` shortcut (workflow-
// level grants are propagated per-job by the collector, so the
// per-job count is the right denominator either way).
JobsWithWriteAll int
// Debug trace (ISSUE-203, GitHub side). VariableBindingsTotal is
// the env-var denominator displayed by the stats block; mirrors
// the GitLab side's "Variables Checked". DebugTraceFound counts
// (job, var) pairs where the variable name matches a configured
// forbidden entry case-insensitively and the value is truthy.
VariableBindingsTotal int
DebugTraceFound int
// Reusable workflow secrets (ISSUE-302).
ReusableCalls int
ReusableCallsSecretsInherit int
// Security jobs (ISSUE-410).
SecurityJobsTotal int
SecurityJobsWeakened int
// Workflow content scanned for template injection (ISSUE-207).
ScriptLinesTotal int
// Unverified script execution (ISSUE-411).
UnverifiedScriptsFound int
// Workflows + properties (ISSUE-802, ISSUE-801).
WorkflowsTotal int
WorkflowsWithDangerousTrigger int
WorkflowsMissingPermissions int
// Branch protection (ISSUE-501 / ISSUE-505).
BranchesTotal int
BranchesProtected int
BranchesMatched int // matched a configured namePattern
// BranchesProtectionDetailsUnknown counts in-scope branches whose
// protection-detail fetch did not yield authoritative data
// (typically: GitHub /branches/{name}/protection 403/404 because
// the token lacks Administration:Read). The branch_non_compliant
// rule (ISSUE-505) abstains on such branches; this counter exists
// so the renderer can surface a "we couldn't fully evaluate this"
// caveat instead of a misleading 100% compliant.
BranchesProtectionDetailsUnknown int
}
GitHubAnalysisStats holds per-control aggregations computed by AggregateGitHubStats from the normalized pipeline IR. Each field corresponds to a denominator/numerator pair used by the renderer to display "(X.X% compliant)" headers and stats blocks like the GitLab side. All fields are pre-aggregated counts — the renderer does not walk the IR again.
func AggregateGitHubStats ¶ added in v0.3.0
func AggregateGitHubStats(pipeline *ir.NormalizedPipeline, pc *configuration.PlumberConfig) *GitHubAnalysisStats
AggregateGitHubStats walks the normalized pipeline IR once and produces the per-control denominators the GitHub renderer needs to emit "(X.X% compliant)" headers and stats blocks (Total Images: 19, Pinned By Digest: 1, …) — matching the GitLab output style.
All counts are derived from the IR; no second collection pass is needed. Configuration knobs (trustedOwners, forbiddenTags, security-job-pattern list) are read from pc when present, falling back to sensible defaults so the function works on a bare config.
type IssueSeverity ¶ added in v0.1.83
type IssueSeverity string
IssueSeverity is the documented severity for an issue code (aligned with getplumber.io issue docs).
const ( SeverityCritical IssueSeverity = "critical" SeverityHigh IssueSeverity = "high" SeverityMedium IssueSeverity = "medium" SeverityLow IssueSeverity = "low" )
func SeverityForCode ¶ added in v0.1.83
func SeverityForCode(code ErrorCode) IssueSeverity
SeverityForCode returns the documented severity for a code, or medium if unknown.
type PipelineImageMetricsSummary ¶
type PipelineImageMetricsSummary struct {
Total uint `json:"total"`
}
PipelineImageMetricsSummary is a simplified version of image metrics for output
type PipelineOriginMetricsSummary ¶
type PipelineOriginMetricsSummary struct {
JobTotal uint `json:"jobTotal"`
JobHardcoded uint `json:"jobHardcoded"`
OriginTotal uint `json:"originTotal"`
OriginComponent uint `json:"originComponent"`
OriginLocal uint `json:"originLocal"`
OriginProject uint `json:"originProject"`
OriginRemote uint `json:"originRemote"`
OriginTemplate uint `json:"originTemplate"`
OriginGitLabCatalog uint `json:"originGitLabCatalog"`
OriginOutdated uint `json:"originOutdated"`
}
PipelineOriginMetricsSummary is a simplified version of origin metrics for output
type PlumberScoreResult ¶ added in v0.1.83
type PlumberScoreResult struct {
ProfileID string `json:"profileId"`
Counts SeverityCounts `json:"counts"`
// RawPoints is 100 minus summed capped per-code losses (before Critical malus).
RawPoints float64 `json:"rawPoints"`
// FinalPoints applies Critical category malus (max points in E band when any Critical exists).
FinalPoints float64 `json:"finalPoints"`
// Score is the letter A–E from final points (what people mean by "how did we score?").
Score string `json:"score"`
CriticalMalusApplied bool `json:"criticalMalusApplied"`
CriticalMalusMax float64 `json:"criticalMalusMax,omitempty"` // max points when malus applies (30)
// Losses is a per-severity rollup of capped per-code losses.
Losses []SeverityLoss `json:"losses"`
// CodeLosses is the per-code breakdown that drives the score in scoring-v3.
CodeLosses []CodeLoss `json:"codeLosses"`
}
PlumberScoreResult is the official result: letter Score (A–E) derived from numeric Points (0–100).
func ComputePlumberScore ¶ added in v0.1.83
func ComputePlumberScore(codeCounts map[ErrorCode]int) PlumberScoreResult
ComputePlumberScore applies the scoring-v3 rules (see docs/scoring.md).
For each issue code with count n > 0:
loss = w × (1 + 0.5·log2(n)) (capped at the per-severity cap)
Each ErrorCode is treated as its own bucket: distinct codes at the same severity each consume their own per-code cap, so accumulating different types of issues keeps reducing the score even after one code is capped.
Raw points are 100 minus the sum of capped per-code losses. When at least one Critical issue is present, final points are capped at 30 (Critical malus), forcing the letter score into the E band. The A–E letter is read from final points using the thresholds in scoreLetterFromPoints.
type SeverityCounts ¶ added in v0.1.83
type SeverityCounts struct {
Critical int `json:"critical"`
High int `json:"high"`
Medium int `json:"medium"`
Low int `json:"low"`
}
SeverityCounts is the number of detected issues per documented severity bucket.
func AggregateSeverityCounts ¶ added in v0.1.83
func AggregateSeverityCounts(result *AnalysisResult) SeverityCounts
AggregateSeverityCounts walks analysis issues and counts occurrences per severity. Kept for callers that only need totals per severity (banners, summary tables).
func SeverityCountsFromIssueCodes ¶ added in v0.1.83
func SeverityCountsFromIssueCodes(codes []ErrorCode) SeverityCounts
SeverityCountsFromIssueCodes tallies severities for individual findings (one code per finding).
type SeverityLoss ¶ added in v0.1.83
type SeverityLoss struct {
Severity IssueSeverity `json:"severity"`
Count int `json:"count"`
CappedLoss float64 `json:"cappedLoss"`
}
SeverityLoss is a rollup per severity bucket: sum of capped losses across all codes in that severity. In scoring-v3 the cap is applied per code, so this rollup can exceed any single per-code cap.