github

package
v0.4.8 Latest Latest
Warning

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

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

Documentation

Index

Constants

View Source
const EnvDisableGitHubAPI = "PLUMBER_DISABLE_GITHUB_API"

EnvDisableGitHubAPI, when set to a truthy value, forces the GitHub metadata client into degraded mode regardless of gh auth state. Set to "1" by the test suite to keep unit tests offline and fast; production code does not read this variable.

View Source
const EnvMetadataToken = "PLUMBER_METADATA_TOKEN"

EnvMetadataToken, when set, supplies the token the metadata client uses to resolve third-party action versions. It takes precedence over the go-gh default chain (gh auth, GH_TOKEN, GITHUB_TOKEN) and is the supported way to resolve actions hosted in an org that gates the Actions GITHUB_TOKEN behind an IP allow list (ISSUE-228); it also carries the 5,000/hr user rate limit rather than the 60/hr anonymous fallback. A token with public-repository read is sufficient.

Variables

View Source
var ErrAuthRequired = fmt.Errorf(
	"GitHub authentication required for upstream-fetch mode (--github-url). " +
		"Set up one of:\n" +
		"  export GH_TOKEN=<token>          # personal token (see README §Step 3 for scope guidance)\n" +
		"  export GITHUB_TOKEN=<token>      # auto-set in GitHub Actions runners\n" +
		"  gh auth login                    # recommended for local dev")

ErrAuthRequired is the actionable error surfaced when go-gh cannot resolve any auth credential. The message points the user at the three supported sources and the README section that documents scopes. Exported so cmd/ and control/ layers can detect the sentinel via errors.Is and short-circuit their normal wrap/log behaviour — a redundant logrus error log on top of cobra's "Error:" prefix on top of "analysis failed:" on top of "github api client:" produces a frame stack instead of the actionable message we want.

Functions

func FetchGitHubBranchProtection

func FetchGitHubBranchProtection(host, owner, repo string, opts BranchFetchOptions) ([]ir.Branch, error)

FetchGitHubBranchProtection resolves branch-protection state for the names the caller asks about, with pagination/cost optimised for the typical "I just want main protected" config. See BranchFetchOptions for how ExactNames and Listing combine.

host is the GitHub API host (empty → api.github.com; non-empty → GHES). Auth is consumed from the same go-gh chain used elsewhere (GH_TOKEN / GH_ENTERPRISE_TOKEN / gh auth login). Without auth, or when the token lacks `repo` / Administration:read scope, the API returns 403/404 — those degrade silently to whatever subset we have already collected (the rego rule then sees fewer branches and may emit fewer findings; quiet is preferable to crash for a partial- data control).

Mapping decisions GitHub → IR shape:

  • branch.protected = true when the API marks the branch as such (the listing endpoint already merges classic Branch Protection and the newer Repository Rulesets, so this flag is correct regardless of which mechanism the repo uses).
  • allowForcePush = api.AllowForcePushes.Enabled.
  • codeOwnerApprovalRequired = api.RequiredPullRequestReviews .RequireCodeOwnerReviews.
  • min*AccessLevel: deliberately left 0 on GitHub. GitLab uses a numeric 0..60 access ladder where 0 = "no one allowed" (strictest). The legacy ISSUE-505 rule treats config min=0 as "always violates", which would false-positive on every GitHub branch that simply requires PR reviews. GitHub has no equivalent ladder; encoding an approximation produced misleading findings. The other ISSUE-505 reasons (allowForcePush, codeOwnerApprovalRequired) still apply.

func FetchGitHubDefaultBranch

func FetchGitHubDefaultBranch(host, owner, repo string) (string, error)

FetchGitHubDefaultBranch resolves the repo's default branch name via the REST API. Returns an empty string with no error when the repo can't be queried (degraded mode), which keeps the `defaultMustBeProtected` rule a silent no-op rather than a noisy crash.

func ScanGitHubWorkflows

func ScanGitHubWorkflows(projectPath, defaultBranch, rootDir, apiHost string, enrichActionMetadata bool) (pipeline *ir.NormalizedPipeline, partialErrors []error, err error)

ScanGitHubWorkflows reads every .yml/.yaml file under <rootDir>/.github/workflows/ and aggregates them into a single NormalizedPipeline. Job names are namespaced by the workflow file base name ("ci/lint", "release/build", ...) so two workflows can expose identically-named jobs without clashing in the IR.

A missing workflows directory is not an error: the returned pipeline simply carries no jobs. Individual unreadable or unparseable files are returned in partialErrors so the caller can surface them without aborting the whole scan.

func ScanGitHubWorkflowsRemote

func ScanGitHubWorkflowsRemote(host, owner, repo, ref string, enrichActionMetadata bool, progressFn ProgressFunc) (*ir.NormalizedPipeline, []error, error)

ScanGitHubWorkflowsRemote fetches `.github/workflows/*.{yml,yaml}` from a GitHub project via the Contents API and runs them through the same parser as the local scanner. Used by `plumber analyze --project owner/repo` (with optional --github-url for GHES) when the user is not inside a local checkout.

host empty → api.github.com. ref empty → repo's default branch. Auth resolves via the same go-gh chain the metadata client uses (GH_TOKEN / GH_ENTERPRISE_TOKEN / GITHUB_TOKEN / gh auth login).

Repo-side artefacts that need a local checkout (Dockerfiles, dependabot.yml, SECURITY.md, Renovate config) are NOT collected in remote mode — controls that depend on them simply see absent inputs and produce no findings. Same degraded-mode contract as missing API auth elsewhere.

func ScanGitHubWorkflowsWithProgress

func ScanGitHubWorkflowsWithProgress(projectPath, defaultBranch, rootDir, apiHost string, enrichActionMetadata bool, progressFn ProgressFunc) (pipeline *ir.NormalizedPipeline, partialErrors []error, err error)

ScanGitHubWorkflowsWithProgress mirrors ScanGitHubWorkflows but notifies the caller through progressFn as it works. The progress total is sized so the bar advances monotonically end-to-end:

step 1                 Scanning workflow files
step 2..(1+N)          Resolving action <n>      (N unique refs)
step 2+N               Scan complete

The last step (policy evaluation) is reported by the caller (RunGitHubAnalysis) using the same total so the bar keeps climbing. progressFn may be nil; callers that don't care about progress should call the plain ScanGitHubWorkflows variant.

func SplitImageRef

func SplitImageRef(ref string) ir.Image

SplitImageRef parses a GitHub Actions image reference into an ir.Image, folding Docker Hub registry-host aliases to docker.io.

func TotalProgressStepsForPipeline

func TotalProgressStepsForPipeline(pipeline *ir.NormalizedPipeline) int

TotalProgressStepsForPipeline returns the grand total the caller (RunGitHubAnalysis / RunGitHubAnalysisRemote) should use when emitting its own progress updates for the post-scan phases, so the bar stays in sync with what the collector already reported.

Layout in slots, both modes:

1                    "Scanning" (local) or "Listing" (remote)
2..(1+N)             per-file fetch ticks (remote only;
                     WorkflowFileCount is 0 in local mode)
(2+N)..(1+N+M)       per-action enrichment ticks (M = unique refs)
(2+N+M)              "Resolving branch protection"
(3+N+M)              "Evaluating policies"
(4+N+M)              "Analysis complete"

Total = N + M + 4. WorkflowFileCount is populated by ScanGitHubWorkflowsRemote; local scans leave it at zero so the formula collapses to M + 4 there.

Types

type BranchFetchOptions

type BranchFetchOptions struct {
	// ExactNames are branch names without glob characters. Each is
	// fetched directly via /repos/{owner}/{repo}/branches/{name}; a
	// 404 is treated as "branch doesn't exist on this repo" and
	// silently skipped (the rego rule then has nothing to flag for
	// that name). Duplicates are deduped.
	ExactNames []string

	// Listing, when true, additionally paginates the /branches
	// endpoint (capped at maxBranchListingPages * 100 entries) so
	// wildcard patterns can match. Off by default because the
	// targeted path covers the typical config and avoids the
	// pagination foot-gun.
	Listing bool

	// OnProgress, when non-nil, is invoked at user-meaningful
	// checkpoints during the fetch: each listing page (with a
	// running branches-seen count) and each per-branch protection-
	// detail call. The caller is expected to forward these to its
	// progress spinner as label updates at the same global slot;
	// the messages are short single-line strings already shaped for
	// terminal display. Used by the CLI to keep the bar's label
	// alive during the otherwise-silent "Resolving branch
	// protection" phase on large repos (grafana/grafana has 772
	// branches across 8 listing pages, ~10s of API time).
	OnProgress func(message string)

	// InScope, when non-nil, gates the slow protection-detail calls
	// during the listing pagination: branches for which InScope
	// returns false are still added to the IR (Protected flag
	// preserved from the listing so ISSUE-501 still has the data it
	// needs) but the classic /protection + Rulesets endpoints are
	// skipped for them. Saves hundreds of API calls on repos where
	// the listing returns many protected branches that do not match
	// any of the user's configured namePatterns (grafana's hundreds
	// of `release-X.Y.Z` branches when the config asks for
	// `release/*`, for example). The rego rule applies the same
	// scope check at evaluation time; this just avoids paying for
	// data the rule is going to discard.
	InScope func(name string) bool
}

BranchFetchOptions controls which branches FetchGitHubBranchProtection reaches out for. The split between targeted and listing modes is deliberate: a single `?per_page=100` page on a busy repo (think grafana/grafana with thousands of `dependabot/*` and `release-*` branches) does not necessarily contain `main` — the alphabetical page-1 falls through long before we reach the default branch — so a naive listing silently produces "0 branches to protect" findings on every realistic config. Targeted /branches/{name} bypasses that problem entirely; listing is only used when the user has at least one wildcard pattern (e.g. `release/*`) we cannot enumerate ahead of time.

type GitHubMetadata

type GitHubMetadata struct {
	RepoArchived bool
	RefExists    bool
	// RefKnownAbsent is true ONLY when the upstream API definitively
	// answered that the ref does not exist (a 404 / 422 on a readable
	// repo). It stays false when the ref could not be verified (private
	// repo, rate limit, network error) so impostor-commit (ISSUE-707)
	// never flags a valid SHA it merely failed to reach.
	RefKnownAbsent   bool
	RefKind          string
	TagSha           string
	LatestTag        string
	LatestReleaseSha string
	RefIsAmbiguous   bool
	Advisories       []string
	StargazersCount  int
}

GitHubMetadata is the facts the API-backed policies need to know about a single `owner/repo@ref` action reference.

  • RepoArchived: the GitHub repo hosting the action is archived.
  • RefExists: the ref (tag / branch / commit SHA) resolves.
  • RefKind: "tag", "branch", "commit", "unknown".
  • TagSha: when RefKind=="tag", the commit SHA the tag currently points at.
  • LatestTag: the repo's newest release tag, "" when the API returns no releases.
  • LatestReleaseSha: the SHA that tag resolves to upstream.
  • RefIsAmbiguous: the ref resolves as BOTH a tag and a branch (ref-confusion).
  • Advisories: security advisory identifiers from the GitHub Advisory Database whose affected version range covers this ref, if any.

Zero value (all fields empty / false) is explicitly "unknown" — it is also what the policies see when the API call failed. They should treat zero value as "I don't know" and stay silent.

type GitHubMetadataClient

type GitHubMetadataClient struct {
	// contains filtered or unexported fields
}

GitHubMetadataClient resolves `owner/repo@ref` references against the real GitHub REST API (via github.com/cli/go-gh which reuses the installed `gh` CLI's stored credentials) and caches every answer so the collector never hits the API twice for the same key. Safe for concurrent use.

When `gh` is not authenticated — or go-gh cannot find a token — the client operates in degraded mode: every lookup returns an empty GitHubMetadata and Available() returns false. Policies are expected to key their deny rules on the positive evidence the client surfaces, so the degraded-mode output is a zero-finding run rather than a crash.

func NewGitHubMetadataClient

func NewGitHubMetadataClient() *GitHubMetadataClient

NewGitHubMetadataClient builds a client using the gh-CLI auth store. Returns a usable client even when authentication is missing — see Available() to check. Honors the PLUMBER_DISABLE_GITHUB_API env var which short-circuits the client into degraded mode regardless of auth state.

Targets api.github.com by default. For GitHub Enterprise Server instances, use NewGitHubMetadataClientForHost with the GHES API host (e.g. "ghes.example.com" or "ghes.example.com/api/v3").

func NewGitHubMetadataClientForHost

func NewGitHubMetadataClientForHost(host string) *GitHubMetadataClient

NewGitHubMetadataClientForHost is the GHES-aware constructor. When host is empty the client targets api.github.com via the default go-gh resolution chain (gh auth, GH_TOKEN, GITHUB_TOKEN). When host is non-empty the client is bound to that host — pair with a GH_TOKEN (or GH_ENTERPRISE_TOKEN) that has access to the GHES instance.

func (*GitHubMetadataClient) Available

func (c *GitHubMetadataClient) Available() bool

Available reports whether the client has a usable gh auth token.

func (*GitHubMetadataClient) DegradedChecks

func (c *GitHubMetadataClient) DegradedChecks() []string

DegradedChecks returns the accumulated "could not verify" messages, one per action ref whose known-CVE check was skipped. Returns nil when none.

func (*GitHubMetadataClient) Resolve

func (c *GitHubMetadataClient) Resolve(ownerRepoRef string) GitHubMetadata

Resolve looks up "owner/repo@ref" and returns what the API told us. Never returns an error — all failures degrade to "unknown" (zero-valued GitHubMetadata). Repeated calls for the same key return the cached value.

func (*GitHubMetadataClient) ResolveTagSha

func (c *GitHubMetadataClient) ResolveTagSha(ownerRepo, tag string) string

ResolveTagSha exposes the tag → SHA lookup publicly so the ref-version-mismatch enrichment can query the commented tag without going through the full Resolve() probe chain.

type ProgressFunc

type ProgressFunc func(step, total int, message string)

ProgressFunc is the signature callers use to observe the progress of long-running collector operations — currently the GitHub API enrichment phase.

type RuleParameters

type RuleParameters struct {
	// pull_request rule
	RequireCodeOwnerReview bool `json:"require_code_owner_review,omitempty"`
}

RuleParameters is the union of parameter shapes across rule types we care about. JSON unmarshal populates only the fields present in the source — extras are silently ignored.

Jump to

Keyboard shortcuts

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