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 MarkAllSkipped(entries []ControlEntry, reason string)
- func MarkOwnCollectionGaps(result *AnalysisResult, entries []ControlEntry)
- func MarkSkippedByFilter(entries []ControlEntry, includeOnly, skip []string)
- func ReEvaluateForConfig(result *AnalysisResult, conf *configuration.Configuration, provider string, ...) (scoped *AnalysisResult, score PlumberScoreResult, ok bool)
- func ScoreBadgeURL(letter string) string
- func ScoreLetterFromPoints(finalPoints float64) string
- func ScoreLetterMeaning(letter string) string
- func ScoreLetterRank(letter string) int
- func StatusFor(e ControlEntry, result *AnalysisResult, findingCount int) string
- type AnalysisResult
- func (r *AnalysisResult) DropNotEvaluableFindings()
- func (r *AnalysisResult) MarkDegradedSnapshotLanes(entries []ControlEntry, run *platform.RunContext)
- func (r *AnalysisResult) MarkFailedCollections(entries []ControlEntry)
- func (r *AnalysisResult) MarkIncludeAttributionUnavailable(entries []ControlEntry)
- func (r *AnalysisResult) MarkMergedConfigUnavailable(entries []ControlEntry, reason string)
- func (r *AnalysisResult) MarkNotEvaluable(controlName, reason string)
- func (r *AnalysisResult) NotEvaluableReason(controlName string) (string, bool)
- type AnalyzedCIConfig
- type AnalyzedWorkflowFile
- type CatalogControl
- type CatalogDocument
- type CatalogIssueType
- 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 ( // obtained for this run, so nothing that reads the merged pipeline can // be evaluated. ReasonResolutionUnavailable = "resolution_unavailable" // available, but not the per-include attribution (which include came // from which project, at which ref and blob) that only the git host's // config-merge API returns. Controls that reason about includes // themselves cannot run on merged YAML alone. ReasonIncludeAttributionUnavailable = "include_attribution_unavailable" // ReasonSnapshotLaneDegraded: the platform reported this control's // snapshot lane as a FAILED collection (degraded_fields), so its absence // is not evidence of an honestly-empty setting. Distinct from an absent // lane the platform vouches for, which a control may legitimately FAIL // against. ReasonSnapshotLaneDegraded = "snapshot_lane_degraded" // ReasonLaneNotServed: the snapshot in hand does not carry this // control's data, and the runner's CI_JOB_TOKEN cannot fetch it either. // // Distinct from ReasonSnapshotLaneDegraded, which says a collection was // attempted and FAILED. This one says the data was never on offer: // either the contract carries no such lane at all // (controlsWithNoPlatformLane), or this deployment's snapshot predates // the lane the contract has since grown (lanesWhoseAbsenceIsNotServed). // Nothing is broken in either case, and telling an operator their // collection failed would send them looking for a fault that is not // there. ReasonLaneNotServed = "lane_not_served" // ReasonIncludeResolutionFailed: at least one include could not be // resolved this run, so both the include list and the per-job // attribution built from it are INCOMPLETE rather than merely absent. // // A dropped include takes its jobs with it: the jobs still appear in the // merged pipeline but nothing attributes them to the include that // contributed them, so they read as project-authored. That direction // fabricates findings, which is why this degrades the same controls a // missing attribution lane does rather than being reported as a warning // beside them. ReasonIncludeResolutionFailed = "include_resolution_failed" // ReasonUpstreamProbeFailed: a check against the include's SOURCE // project could not be completed, so the control has no evidence either // way. // // These probes are fail-safe: only a confirmed result sets the flag they // feed, and a failed probe leaves it at its zero value. That is right for // the flag and wrong for the report, because the zero value reads as a // clean answer - "this ref is not ambiguous" - that nothing established. ReasonUpstreamProbeFailed = "upstream_probe_failed" // not be read, though the merged pipeline was obtained anyway. // // The merged document answers almost every rule, so the run is still // worth having. Two controls compare the pre-merge file against it, and // both fail silently without it rather than loudly: an unread root // yields no hardcoded jobs and no local variables, which reads as // nothing to report. ReasonRawConfigUnavailable = "raw_config_unavailable" // ReasonCollectionFailed: a collection this run makes ITSELF could not // be completed, so the control that reads it has no data. // // Distinct from the snapshot reasons above, which describe a lane the // PLATFORM could not fill. This one applies in every mode, because a // token without the scope for one endpoint is not a platform-mode // condition. ReasonCollectionFailed = "collection_failed" )
Machine-readable reasons a control's data lane supplied nothing. They travel to the platform on the finding, so an operator can tell "we could not check this" apart from "this passed" without reading prose.
const ( StatusPassed = "passed" StatusFailed = "failed" StatusSkipped = "skipped" StatusError = "error" )
Per-control evaluation statuses. `passed` and `failed` mean the control genuinely evaluated (real data, real check); `skipped` means it never ran (disabled in .plumber.yaml or excluded via --controls/--skip-controls); `error` means it could not be trusted to have fully evaluated — an empty findings list in that state is "could not tell", not "compliant".
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.
WARNING: this is a HAND-WRITTEN if-chain, not a derivation. It does NOT stay in sync with GitLabControls/GitHubControls automatically, and no test compares them, so adding a control to either catalog without adding a branch here silently breaks its disable path: the control renders as "skipped" in the terminal table and the legacy JSON, while its findings still survive FilterFindingsByEnabledControls into SARIF, GLSAST, PBOM and the score. For a Critical code that means a 30-point malus and an E on a control the user believes is off. Keep the branch count equal to the union of the two catalogs.
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 MarkAllSkipped ¶ added in v0.4.48
func MarkAllSkipped(entries []ControlEntry, reason string)
MarkAllSkipped marks every entry skipped with the given reason. It backs --no-controls: the catalog still describes what Plumber COULD have checked, but nothing was evaluated, so no entry may report a verdict. Without this, a zero-findings run stamps `status: "passed"` on every control that never ran, which is the false green the flag exists to avoid.
func MarkOwnCollectionGaps ¶ added in v0.4.50
func MarkOwnCollectionGaps(result *AnalysisResult, entries []ControlEntry)
MarkOwnCollectionGaps marks the controls whose OWN check could not be completed, independent of where the run got its configuration.
Every input here comes from a collection this CLI performs itself in BOTH modes: a tag-vs-branch probe against an include's source project, a catalogue lookup for its latest version, the project's own unmerged CI file. None of it is platform-specific.
It used to live inside the platform-gated marker, which meant a plain `plumber analyze` recorded these failures and then read nobody: a rate limit or a 403 on the source project left externalRefsMustNotCollide and includesMustBeUpToDate reporting a clean pass. The failures degrade ONE control each rather than the whole attribution set, and they happen on runs where everything else went fine, which is exactly why nothing else caught them.
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 ReEvaluateForConfig ¶ added in v0.4.50
func ReEvaluateForConfig( result *AnalysisResult, conf *configuration.Configuration, provider string, pc *configuration.PlumberConfig, ) (scoped *AnalysisResult, score PlumberScoreResult, ok bool)
ReEvaluateForConfig re-runs the rule engine over the pipeline this run already collected, under a DIFFERENT control configuration, and returns the findings plus the score they produce.
It exists for platform mode's per-policy evaluation. The platform serves each policy its own control parameters, and a policy's verdict has to come from its own parameters: reporting a finding computed under policy A's config while claiming policy B's effective config is a false positive under B, not merely an imprecise label.
Nothing is re-collected. The IR is the one the run already built, so this costs a rule evaluation and no git-host traffic. A run with no retained pipeline (the GitHub path, or a limited analysis that never built one) returns ok=false and the caller keeps the run's own verdict rather than inventing an empty one.
func ScoreBadgeURL ¶ added in v0.1.83
ScoreBadgeURL builds a Shields.io badge URL showing the Plumber letter score (A–E).
func ScoreLetterFromPoints ¶ added in v0.4.58
ScoreLetterFromPoints is the scoring-v3 letter band (A>=90, B>=71, C>=51, D>=31, else E) over FINAL points. Exported for the platform, which derives an aggregate's letter through this one table.
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").
func StatusFor ¶ added in v0.4.29
func StatusFor(e ControlEntry, result *AnalysisResult, findingCount int) string
StatusFor derives a control's evaluation status for a run.
Order matters: findings trump degradation — when a control found real violations, `failed` is accurate and actionable regardless of whether the run was complete. The `error` state only guards the opposite case: an EMPTY findings list that cannot be trusted, because the control had nothing real to evaluate (missing/invalid CI config) or its data collection failed mid-run. Presenting that as `passed` is the silent false-green this field exists to eliminate (#220, and the "how do I tell passed from not-evaluated" integration feedback on #353).
branchMustBeProtected is the one repo-level control: it evaluates against the provider's protection API, not the CI configuration, so CiMissing/CiValid do not apply to it — only its own collection signals do (a failed protection fetch, or protection details a token scope could not read). Every other control reads the CI configuration and inherits the CI-level signals.
Run-wide Warnings ("could not verify" messages, e.g. a skipped known-CVE lookup) deliberately do NOT flip a control to error in this version: they are surfaced separately in every output and gated by --fail-warnings. Folding them in per-control would require parsing which control each warning string belongs to.
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"`
// ArtifactCommitSHA and ArtifactRef are the resolved analyzed commit and
// its branch/tag, computed once at output time and read by every artifact
// writer so the JSON report, PBOM, SARIF and OCSF all report the same
// commit the same way (#443). ArtifactCommitSHA is the resolved SHA, never
// the literal "HEAD" placeholder (empty when nothing real resolved). They
// are json:"-" because each writer places them in its own schema slot
// (the report's headCommitSha, SARIF's versionControlProvenance, ...),
// never as raw fields on the marshaled result.
ArtifactCommitSHA string `json:"-"`
ArtifactRef string `json:"-"`
// ArtifactRepoURI is the analyzed project's web URL, carried so SARIF's
// versionControlProvenance can name the repository the commit belongs to
// (repositoryUri is required there). Empty when it cannot be derived.
ArtifactRepoURI string `json:"-"`
// AnalyzedCIConfig is the CI configuration this run actually evaluated:
// for GitLab the resolved merged pipeline, for GitHub each scanned
// workflow file (#443). It carries a json tag so it appears in the JSON
// report, and only there: no other artifact marshals the result whole.
AnalyzedCIConfig *AnalyzedCIConfig `json:"analyzedCiConfig,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:"-"`
// VariablesData records the settings-variable collection for the
// cicdVariablesMustBe* controls. Set only after the collection ran;
// nil (never ran) or Known=false (401/403) makes those controls
// report not-evaluable rather than a false pass (see StatusFor).
VariablesData *gitlab.GitlabVariablesAnalysisData `json:"-"`
// SecurityPolicyEvaluable is true when the security policy project linkage
// was read authoritatively (a successful GraphQL read). False when it could
// not be read (auth error, or the field is unavailable on the instance), so
// StatusFor reports projectMustHaveSecurityPolicySource (ISSUE-601) as
// not-evaluable rather than a false pass.
SecurityPolicyEvaluable bool `json:"-"`
// SecurityPolicyTierCaveat is set when the security-policy control ran, the
// linkage was read, and NO policy project is linked — the tier-ambiguous
// case (Ultimate-only feature; a non-Ultimate project cannot link one, an
// Ultimate project may have left it unset). Renderers surface a conditional
// caveat next to ISSUE-601.
SecurityPolicyTierCaveat bool `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:"-"`
// Pipeline is the normalized IR this run evaluated (GitLab path). It is
// retained so a per-policy evaluation can re-run the rules over the same
// collected data under different parameters: the platform serves each
// policy its OWN control config, and a policy's verdict must come from
// its own config rather than from whichever one happened to run first.
// Nil on the GitHub path, which has GitHubPipeline below.
Pipeline *ir.NormalizedPipeline `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"`
// ApprovalRulesTierCaveat is set when an MR approval-rule control ran but
// the GitLab approvals API returned zero rules — the ambiguous case where
// the project is either on GitLab Free (feature unavailable, API returns an
// empty list) or on Premium/Ultimate with no rules configured. The API
// gives no tier signal to tell them apart, so renderers surface a
// Premium/Ultimate caveat next to ISSUE-502/504 rather than presenting the
// result as authoritative.
ApprovalRulesTierCaveat bool `json:"-"`
// MRApprovalSettingsTierCaveat is set when the MR approval-settings control
// ran and the project has no approval protection in effect — the GitLab-Free
// signature (the feature does not exist there and the API 200-defaults every
// protection off, which the operator cannot change). The settings API gives
// no other tier signal, so renderers surface a Premium/Ultimate caveat next
// to ISSUE-503 advising the operator to disable the control if they are on
// Free. Any single protection being active proves a paid tier and clears it.
MRApprovalSettingsTierCaveat bool `json:"-"`
// MRSettingsPremiumCaveatFields lists the Premium/Ultimate MR-setting
// expectations (mergePipelinesEnabled, mergeTrainsEnabled) that read as OFF
// while the config expects them ON. These features require a paid tier, and
// the project payload gives no tier signal, so an OFF read is ambiguous: a
// Free project that cannot enable them, or a paid project that left them off
// (a real misconfiguration). Non-empty => renderers surface a CONDITIONAL
// caveat next to ISSUE-506 (disable the expectation if on Free; enable the
// feature if on a paid tier) rather than asserting the tier either way.
MRSettingsPremiumCaveatFields []string `json:"-"`
// 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"`
// NotEvaluable maps a control name to the machine-readable reason its
// verdict could not be established this run, for controls whose data
// lane supplied nothing while the rest of the run evaluated normally.
//
// It exists because DegradedReasons above is all-or-nothing: any entry
// turns EVERY control into an error. A lane split — where one source is
// unavailable and the others are fine — has to say so per control, or
// the unavailable lane either hides (a silent pass over data nobody
// collected) or wrongly discredits every other control's verdict.
//
// A control listed here reports not_evaluable instead of passing. Real
// findings still win: a control that DID fire is failed, because a
// violation found on partial data is still a violation.
NotEvaluable map[string]string `json:"notEvaluable,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.
func (*AnalysisResult) DropNotEvaluableFindings ¶ added in v0.4.50
func (r *AnalysisResult) DropNotEvaluableFindings()
DropNotEvaluableFindings removes every finding belonging to a control this run marked not evaluable.
A control is marked when its data lane could not feed it, and a lane can fail in two ways: by supplying nothing, or by supplying something WRONG. The second is why the findings go rather than just the status. Without include attribution, jobs a component contributed are indistinguishable from ones the project wrote, so rules keyed on that distinction fire on upstream jobs behaving normally. Keeping those findings would push fabricated violations to the platform and deduct real score points for them.
Reporting nothing for a control that could not be evaluated is the honest outcome, and the not_evaluable status is what carries the fact that it was checked at all.
func (*AnalysisResult) MarkDegradedSnapshotLanes ¶ added in v0.4.50
func (r *AnalysisResult) MarkDegradedSnapshotLanes(entries []ControlEntry, run *platform.RunContext)
MarkDegradedSnapshotLanes flags the controls whose platform snapshot lane failed collection, plus the controls the snapshot has no lane for at all.
This is the distinction the CLI could not previously make. Before the platform served degraded_fields, an absent lane could equally mean "this project genuinely has no branch protections" (a real violation a control SHOULD fail on) or "the collection blew up". Failing on the first and abstaining on the second are both correct; guessing between them is not.
func (*AnalysisResult) MarkFailedCollections ¶ added in v0.4.50
func (r *AnalysisResult) MarkFailedCollections(entries []ControlEntry)
MarkFailedCollections flags the controls whose own collection could not be completed, and drops their findings.
It runs in EVERY mode, unlike the snapshot lane marking. The failure it covers is a permission, not a platform: a token that cannot list a project's protected branches gets an error where it expected a list, and an empty protection list is indistinguishable from a project that protects nothing - which is the exact violation branchMustBeProtected exists to report. Left unmarked, the loudest possible false positive fires on every branch the config names.
The branch NAMES survive such a failure, so this is specifically about the protection detail; a control that reads only the branch list would still be evaluable.
func (*AnalysisResult) MarkIncludeAttributionUnavailable ¶ added in v0.4.50
func (r *AnalysisResult) MarkIncludeAttributionUnavailable(entries []ControlEntry)
MarkIncludeAttributionUnavailable flags every control that reasons about include attribution as not evaluable. Used when the merged configuration came from somewhere that does not carry per-include provenance — today, the platform snapshot or its resolve endpoint, both of which return the merged document only.
func (*AnalysisResult) MarkMergedConfigUnavailable ¶ added in v0.4.50
func (r *AnalysisResult) MarkMergedConfigUnavailable(entries []ControlEntry, reason string)
MarkMergedConfigUnavailable flags every control that reads the merged CI pipeline as not evaluable, with reason.
The set is every GitLab control EXCEPT the ones that read a different source entirely: branchMustBeProtected evaluates the provider's branch protection API, so an unavailable CI configuration says nothing about it. Listing the exceptions rather than enumerating the dependents is deliberate — a control added later reads the merged pipeline unless it says otherwise, and defaulting a new control to not_evaluable when the config is missing is the safe direction.
func (*AnalysisResult) MarkNotEvaluable ¶ added in v0.4.50
func (r *AnalysisResult) MarkNotEvaluable(controlName, reason string)
MarkNotEvaluable records that a control could not be evaluated this run and why. The FIRST reason recorded for a control wins: the earliest lane to come up empty is the most specific explanation, and a later, broader failure should not overwrite it.
func (*AnalysisResult) NotEvaluableReason ¶ added in v0.4.50
func (r *AnalysisResult) NotEvaluableReason(controlName string) (string, bool)
NotEvaluableReason returns the machine-readable reason this control's own data lane could not feed it, and whether it was marked at all.
Use this, not the run-level DegradedReasons, when explaining a StatusError for ONE control. DegradedReasons describes the RUN, so per-control it either says nothing (when empty) or names an unrelated cause: a run degraded by a failed variables fetch would explain an unevaluable include control with the variables failure.
type AnalyzedCIConfig ¶ added in v0.4.52
type AnalyzedCIConfig struct {
// Path is the GitLab CI config path that was analyzed (the project's
// ci_config_path, default ".gitlab-ci.yml"). Empty on the GitHub path.
Path string `json:"path,omitempty"`
// Content is the GitLab configuration that findings were evaluated
// against. Merged reports whether it is the include-merged pipeline
// (true) rather than a single raw file.
Content string `json:"content,omitempty"`
Merged bool `json:"merged,omitempty"`
// Workflows are the GitHub workflow files that were scanned, each with
// its repo-relative path and content. Empty on the GitLab path.
Workflows []AnalyzedWorkflowFile `json:"workflows,omitempty"`
}
AnalyzedCIConfig is the CI configuration a run evaluated, emitted in the JSON report so a consumer knows exactly which input produced the findings (#443). It is provider-shaped: a GitLab run fills Path/Content/Merged (one resolved pipeline), a GitHub run fills Workflows (each scanned file).
type AnalyzedWorkflowFile ¶ added in v0.4.52
AnalyzedWorkflowFile is one scanned GitHub workflow file (#443).
type CatalogControl ¶ added in v0.4.56
type CatalogControl struct {
ID string `json:"id"`
Name string `json:"name"`
DisplayName string `json:"displayName"`
Category string `json:"category"`
Providers []string `json:"providers"`
Description string `json:"description"`
RequiresConfig bool `json:"requiresConfig"`
ConfigSchema *configuration.ControlConfigSchema `json:"configSchema,omitempty"`
IssueCodes []string `json:"issueCodes"`
}
CatalogControl is one control in the exported catalog: the CTRL-XXX immutable id, the stable technical name, display metadata, the welded config schema when the control has a config block, and the ISSUE codes it can emit (#458).
type CatalogDocument ¶ added in v0.4.56
type CatalogDocument struct {
CatalogVersion int `json:"catalogVersion"`
CLIVersion string `json:"cliVersion"`
Controls []CatalogControl `json:"controls"`
IssueTypes []CatalogIssueType `json:"issueTypes"`
}
CatalogDocument is the whole exported catalog in one versioned envelope: what the platform imports through Catalog() and the catalog command serializes as JSON (#458). CatalogVersion identifies the document shape; consumers are expected to be forward-tolerant to added fields.
func Catalog ¶ added in v0.4.56
func Catalog(cliVersion string) CatalogDocument
Catalog assembles the CLI's whole catalog into one document: the single source the platform imports (Go) and the catalog command serializes (JSON) so no consumer maintains a divergent copy (#458). cliVersion is injected by the caller (cmd passes the build version; tests pass a constant) so the document itself stays deterministic.
type CatalogIssueType ¶ added in v0.4.56
type CatalogIssueType struct {
Code string `json:"code"`
Severity string `json:"severity"`
Title string `json:"title"`
Description string `json:"description"`
Remediation string `json:"remediation"`
DocURL string `json:"docUrl"`
ControlName string `json:"controlName"`
}
CatalogIssueType is one issue type in the exported catalog: the ISSUE-XXX immutable id with the CLI's own wording (title, description, remediation) and its owning control, so consumers render the engine's words instead of maintaining a copy (#458).
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 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).
ISSUE codes are immutable identifiers (#458): the platform keys durable state (issues, history, dismissals) on them. A code is never renumbered, never reused for a different meaning, and survives any rename of titles or control names. TestIssueCodesAreUnique pins uniqueness; TestEveryRegoIssueCodeIsRegistered pins that no policy emits an unregistered code.
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-714: Action fetches and runs remote code from a mutable ref, in the open (no integrity check) CodeActionMutableRemoteExec ErrorCode = "ISSUE-714" // ISSUE-715: Action OBFUSCATES a remote code fetch/exec (decode-then-run) CodeActionObfuscatedRemoteExec ErrorCode = "ISSUE-715" // ISSUE-716: Action source could not be verified (fetch failed): not a pass CodeActionRemoteExecUnverified ErrorCode = "ISSUE-716" )
Issue codes for container image controls (1xx)
const ( // ISSUE-201: CI/CD settings variable is not protected (exposed to pipelines on unprotected branches) CodeCicdVariableUnprotected ErrorCode = "ISSUE-201" // ISSUE-202: CI/CD settings variable is not masked (its value prints in job logs) CodeCicdVariableUnmasked ErrorCode = "ISSUE-202" // 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, latent // hygiene (low). The demonstrable-leak escalation is ISSUE-310. 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-310: Persisted checkout credentials are packed into an uploaded // artifact (`.git` in the artifact): the demonstrable ArtiPACKED leak. CodeArtipackedExfiltrated ErrorCode = "ISSUE-310" )
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-422: Workflow has no explicit `name:` field CodeAnonymousDefinition ErrorCode = "ISSUE-422" // 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 (4xx, plus the 9xx repo-hygiene codes)
const ( // ISSUE-501: Branch is not protected CodeBranchUnprotected ErrorCode = "ISSUE-501" // ISSUE-502: A merge-request approval rule covering all protected branches requires fewer approvals than the configured minimum CodeMRApprovalRulesBelowMinimum ErrorCode = "ISSUE-502" // ISSUE-503: The project's merge-request approval settings do not meet the configured expectations CodeMRApprovalSettingsNonCompliant ErrorCode = "ISSUE-503" // ISSUE-504: No merge-request approval rule applies to all protected branches CodeMRApprovalRulesAllBranchesMissing ErrorCode = "ISSUE-504" // ISSUE-505: Branch has non-compliant protection settings CodeBranchNonCompliant ErrorCode = "ISSUE-505" // ISSUE-506: The project's merge-request/merge settings do not meet the configured expectations CodeMRSettingsNonCompliant ErrorCode = "ISSUE-506" // ISSUE-803: Job runs with overly broad permissions (write-all) CodeExcessivePermissions ErrorCode = "ISSUE-803" )
Issue codes for access and authorization controls (5xx)
const ( // ISSUE-601: No (or the wrong) GitLab security policy project is linked CodeSecurityPolicyProjectNotSet ErrorCode = "ISSUE-601" )
Issue codes for security-policy-source controls (6xx)
func CodesForControl ¶ added in v0.4.29
CodesForControl returns every ISSUE code registered to a control name, sorted for deterministic output. Empty for an unknown control name.
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"`
// RawPointsUnclamped is RawPoints before the floor at zero. RawPoints is
// what the gate and the badge are defined on, so it stays clamped; this
// carries the true signed deficit for consumers that store history and
// want to distinguish "barely failing" from "catastrophic".
RawPointsUnclamped float64 `json:"rawPointsUnclamped"`
// 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 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.