control

package
v0.3.0-beta.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 13, 2026 License: MPL-2.0 Imports: 16 Imported by: 0

Documentation

Index

Constants

View Source
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 -->"
)
View Source
const PlumberScoreDocURL = "https://github.com/getplumber/plumber/blob/main/docs/scoring.md"

PlumberScoreDocURL is the canonical user-facing explanation of the Plumber letter score.

View Source
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

func ComplianceBadgeURL(compliance, threshold float64) string

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

func ControlPassesFilter(name string, includeOnly, skip []string) bool

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

func FindingsByControl(findings []opaengine.Finding) map[string][]opaengine.Finding

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

func IsRegoFileBenchedForProvider(content []byte, provider string) bool

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

func ScoreBadgeURL(letter string) string

ScoreBadgeURL builds a Shields.io badge URL showing the Plumber letter score (A–E).

func ScoreLetterMeaning added in v0.2.5

func ScoreLetterMeaning(letter string) string

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

type ControlEntry struct {
	DisplayName string
	ControlName string
	Skipped     bool
	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

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 (
	// ISSUE-101: Container image comes from an unauthorized registry
	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)

func (ErrorCode) DocURL added in v0.1.67

func (c ErrorCode) DocURL() string

DocURL returns the documentation URL for a given issue code.

func (ErrorCode) String added in v0.1.67

func (c ErrorCode) String() string

String returns the string representation of an issue code.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL