Documentation
¶
Index ¶
- Constants
- func AggregateIssueCodeCounts(result *AnalysisResult) map[ErrorCode]int
- func ComplianceBadgeURL(compliance, threshold float64) string
- 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 IsRegoFileBenchedForProvider(content []byte, provider string) bool
- func ManageMergeRequestComment(projectID int, mrIID int, result *AnalysisResult, ...) error
- func ManageProjectBadge(projectID int, compliance float64, threshold float64, ...) error
- func MarkSkippedByFilter(entries []ControlEntry, includeOnly, skip []string)
- func ScoreBadgeURL(letter string) string
- func ScoreLetterMeaning(letter string) string
- type AnalysisResult
- type CodeLoss
- type ControlEntry
- type ErrorCode
- type ErrorCodeInfo
- type GitHubAnalysisStats
- type IssueSeverity
- type PipelineImageMetricsSummary
- type PipelineOriginMetricsSummary
- type PlumberScoreResult
- type SeverityCounts
- type SeverityLoss
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. 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 ComplianceBadgeURL ¶ added in v0.1.42
ComplianceBadgeURL builds a Shields.io badge URL for the given compliance %. Color is green if compliance meets threshold, red otherwise. Exported so it can be used by the project badge feature.
func ControlPassesFilter ¶
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.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 ¶
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")).
func FilterFindingsByEnabledControls ¶
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 ¶
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 IsRegoFileBenchedForProvider ¶
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, compliance float64, threshold float64, conf *configuration.Configuration, score *PlumberScoreResult, scoreMode bool, scorePointMode bool, ) error
ManageMergeRequestComment creates or updates the Plumber compliance comment on the given merge request. projectID and gitlabURL come from the already- resolved configuration/result; only mrIID is CI-specific.
func ManageProjectBadge ¶ added in v0.1.42
func ManageProjectBadge( projectID int, compliance float64, threshold float64, conf *configuration.Configuration, ps *PlumberScoreResult, useLetterScore bool, ) error
ManageProjectBadge creates or updates the Plumber compliance badge on the project. The badge shows the compliance percentage with green (passed) or red (failed) color. When useLetterScore is true and ps is non-nil, the badge shows letter score (A–E) instead (see ScoreBadgeURL).
func MarkSkippedByFilter ¶
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.
Types ¶
type AnalysisResult ¶
type AnalysisResult struct {
// Project information
ProjectPath string `json:"projectPath"`
ProjectID int `json:"projectId"`
DefaultBranch string `json:"defaultBranch"`
// 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
// (see docs/REFACTOR_MULTI_PROVIDER.md §8 Phase A).
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 *collector.GitlabPipelineImageData `json:"-"`
PipelineOriginData *collector.GitlabPipelineOriginData `json:"-"`
ProtectionData *collector.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 collector.
// Nil on the GitLab path.
GitHubPipeline *ir.NormalizedPipeline `json:"-"`
}
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 ¶
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 (see docs/REFACTOR_MULTI_PROVIDER.md §4).
func RunGitHubAnalysisRemote ¶
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 ¶
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 ¶
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 ¶
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 ¶
func GitLabControls(pc *configuration.PlumberConfig) []ControlEntry
GitLabControls returns the catalog of GitLab compliance controls in their canonical display order. Every known GitLab control is returned so the compliance table and the legacy JSON `*Result` blocks render every row a v0.2.x user expects:
- Absent from `gitlab.controls.*` → `Skipped: true`. v0.2.x's legacy control wrappers (`control/controlGitlab*.go` at tag v0.2.22) uniformly set `p.Enabled = false` and short-circuit `Run()` with `result.Skipped = true` when the per-control section is missing from `.plumber.yaml`. The Rego port follows the same contract: findings for absent controls are dropped by FilterFindingsByEnabledControls so the row stays at 100% and the score ignores them.
- Present with `enabled: true` → `Skipped: false`, runs.
- Present with `enabled: false` (or an empty entry — IsEnabled returns false when the toggle is unset on a non-nil cfg) → `Skipped: true`.
The caller typically fills in the findings-derived compliance by looking up FindingsByControl.
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-104: Third-party GitHub Action reference is not pinned by commit SHA CodeActionUnpinned ErrorCode = "ISSUE-104" // ISSUE-105: Container registry password is hard-coded in the workflow CodeContainerHardcodedCredentials ErrorCode = "ISSUE-105" // ISSUE-106: Release/publish workflow primes a build cache from attacker-controlled artifacts CodeCachePoisoning ErrorCode = "ISSUE-106" // ISSUE-108: Action is hosted in an archived GitHub repository CodeActionArchivedRepo ErrorCode = "ISSUE-108" // ISSUE-109: Pinned commit SHA does not exist in the action's upstream repository CodeImpostorCommit ErrorCode = "ISSUE-109" // ISSUE-110: `# vX.Y.Z` comment does not match the SHA the ref resolves to CodeRefVersionMismatch ErrorCode = "ISSUE-110" // ISSUE-111: Action pinned by SHA is stale vs the latest upstream release CodeStaleActionRef ErrorCode = "ISSUE-111" // ISSUE-113: Symbolic ref collides with both a tag and a branch upstream CodeRefConfusion ErrorCode = "ISSUE-113" // ISSUE-114: Action version carries a published security advisory CodeKnownVulnerableAction ErrorCode = "ISSUE-114" // ISSUE-115: Third-party action duplicates a runner built-in (gh CLI, etc.) CodeSuperfluousAction ErrorCode = "ISSUE-115" // ISSUE-107: Dockerfile FROM reference is not pinned by digest CodeDockerfileUnpinnedBase ErrorCode = "ISSUE-107" // ISSUE-112: Release / publish workflow produces unsigned artefacts CodeReleaseWorkflowUnsigned ErrorCode = "ISSUE-112" )
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-206: Workflow inlines user-controlled template expressions into a run: script CodeTemplateInjection ErrorCode = "ISSUE-206" // 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-301: Workflow exfiltrates the entire secrets context via toJson(secrets) CodeOverprovisionedSecrets ErrorCode = "ISSUE-301" // 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-304: Workflow grants no explicit permissions, relying on the repo default CodeUndocumentedPermissions ErrorCode = "ISSUE-304" // 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 codes for secret and credential handling controls (3xx)
const ( // ISSUE-401: Job is hardcoded (not sourced from include/component) CodeJobHardcoded ErrorCode = "ISSUE-401" // 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 downloads and executes a script without integrity verification (curl|bash, wget|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-414: Workflow subscribes to a dangerous trigger (pull_request_target, workflow_run) CodeDangerousTriggers ErrorCode = "ISSUE-414" // ISSUE-415: pull_request_target workflow explicitly checks out the PR head (tj-actions pattern) CodePullRequestTargetWithHeadCheckout ErrorCode = "ISSUE-415" )
Issue codes for pipeline composition controls (4xx)
const ( // ISSUE-601: Workflow has no explicit `name:` field CodeAnonymousDefinition ErrorCode = "ISSUE-601" // ISSUE-602: Workflow has no `concurrency:` block at either level CodeMissingConcurrency ErrorCode = "ISSUE-602" // ISSUE-603: Workflow uses a misfeature pattern (shell: cmd, inline pip install curl|sh, …) CodeWorkflowMisfeature ErrorCode = "ISSUE-603" // ISSUE-604: Workflow script contains obfuscation (zero-width / non-ASCII unicode, bidi) CodeWorkflowObfuscation ErrorCode = "ISSUE-604" // ISSUE-605: PyPI / npm publish relies on a static token instead of OIDC trusted publishing CodeUseTrustedPublishing ErrorCode = "ISSUE-605" // ISSUE-606: dependabot.yml re-enables insecure external code execution CodeDependabotInsecureExec ErrorCode = "ISSUE-606" // ISSUE-607: dependabot.yml update ecosystem has no cooldown window CodeDependabotMissingCooldown ErrorCode = "ISSUE-607" // ISSUE-608: Repository has workflows but no dependency update tool configured CodeDependencyUpdateToolMissing ErrorCode = "ISSUE-608" // ISSUE-609: Repository has workflows but none runs a SAST scanner CodeSASTWorkflowMissing ErrorCode = "ISSUE-609" // ISSUE-610: Repository has workflows but no SECURITY.md policy file CodeSecurityPolicyMissing ErrorCode = "ISSUE-610" )
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-509: Job runs with overly broad permissions (write-all) CodeExcessivePermissions ErrorCode = "ISSUE-509" )
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.
Title string `json:"title"`
// Description explains what the issue is.
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"`
}
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.
type GitHubAnalysisStats ¶
type GitHubAnalysisStats struct {
// Actions pinning (ISSUE-104).
ActionRefsTotal int
ActionRefsUnpinned int
ActionRefsExempt 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
// Reusable workflow secrets (ISSUE-302).
ReusableCalls int
ReusableCallsSecretsInherit int
// Security jobs (ISSUE-410).
SecurityJobsTotal int
SecurityJobsWeakened int
// Workflow content scanned for template injection (ISSUE-206).
ScriptLinesTotal int
// Workflows + properties (ISSUE-414, ISSUE-304).
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 ¶
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.