Help us shape the future of Go! Take the 2026 Go Developer Survey

github

package
v0.7.9 Latest Latest
Warning

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

Go to latest
Published: Oct 19, 2026 License: MPL-2.0 Imports: 30 Imported by: 0

Documentation

Overview

Package github is the GitHub provider: the workflow parser over .github/workflows, the action metadata client that resolves `uses:` refs against the GitHub API, branch protection and ruleset reads, and the intermediate-representation builder the Rego policies evaluate.

Index

Constants

View Source
const (
	// MaxNestedContainers bounds the API cost of the closure (R3, R7, R8):
	// it counts the NEW unique containers (composite actions, reusable
	// workflows) fetched in the run, never the direct references of the
	// workflows. One container costs one definition fetch plus, once its
	// steps are entries, the metadata resolve and the mutable-exec scan the
	// enrichment already pays per unique reference: about 15 calls with
	// ISSUE-714 on, about 7 with it off, so the worst case stays near 1,500
	// calls against the 5,000 per hour authenticated budget. A reference the
	// budget cut off is never enriched, so it costs nothing. A named
	// constant, not a .plumber.yaml key (R7): a knob would let a user
	// silence the degradation.
	MaxNestedContainers = 100

	// MaxNestedEntriesPerJob bounds the entries one job's closure appends.
	// The container budget bounds the fetches, not the paths: two paths to
	// one container are two entries (R5), so a lattice of a hundred
	// containers could otherwise multiply without end. Past it the rest is
	// listed with reason budget and the run degrades like a container
	// overflow (R3).
	MaxNestedEntriesPerJob = 10 * MaxNestedContainers

	// MaxNestedJobs bounds the jobs the called workflows of other
	// repositories append in the run. A workflow costs one fetch however
	// many calls reach it, and every call appends its own copy of the
	// callee's jobs (two calls are two sets of nested jobs), so a diamond of
	// calls would double the jobs at every level for a fixed fetch cost.
	// Past it the calls left are listed with reason budget and the run
	// degrades like a container overflow (R3).
	MaxNestedJobs = 1000
)

The bounds of the closure live here only. The control package names the one that tripped in the degraded reason (spec section 7).

View Source
const EnvDisableGitHubAPI = "PLUMBER_DISABLE_GITHUB_API"

EnvDisableGitHubAPI is the variable that, when set to a truthy value, forces every GitHub API consumer that honors it (the metadata client, the default-branch lookup seam in control) into degraded mode regardless of gh auth state. Set to "1" by the test suites to keep unit tests offline and fast.

View Source
const EnvMetadataToken = "PLUMBER_METADATA_TOKEN"

EnvMetadataToken is the variable that, 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 APIDisabled added in v0.7.2

func APIDisabled() bool

APIDisabled reports the offline switch both read paths honour (R6).

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 FetchGitHubDefaultTokenWrite added in v0.7.1

func FetchGitHubDefaultTokenWrite(host, owner, repo, defaultBranch string, declaresContentsWrite bool, eligible func(workflowPath string) bool) (write, source string)

FetchGitHubDefaultTokenWrite resolves whether the repository's real default GITHUB_TOKEN (the one a job with no permissions block at all, its own or its workflow's, actually runs with) can write, and how that was decided.

eligible reports whether a workflow (by its repo-relative path, the form the runs listing's own "path" field and the workflow-states listing share) is one where EVERY job declares no permissions of its own and calls no reusable workflow: the only workflows whose run can safely stand in for the repository default, since GitHub answers a run's first job (per_page=1) whichever job that happens to be. defaultBranch is the repository's own default branch (pipeline.DefaultBranch); empty means no run is ever read (nothing to compare a run's own branch against). declaresContentsWrite is true when some job elsewhere in the pipeline already declares a contents write or write-all: in that case a bot-authored release proves only that SOME token in this repository can write, not that the undeclared default one can, so the effect route is skipped outright. The lane tries, in order:

  1. the log route: the latest defaultTokenRunsPerPage completed runs, kept to the ones that ran on a push, a schedule or a manual dispatch of the repository's own default branch, in the repository itself (never a fork), triggered by neither actor nor triggering actor as defaultTokenExcludedActor, and whose workflow is eligible; for the first such run with a fetchable first job, that job's own log is read for the "GITHUB_TOKEN Permissions" block GitHub Actions prints at the top. A job fetch failure moves on to the next eligible run; a log fetch or parse failure (or an absent block) abandons the log route entirely rather than trying another run, and falls through to the effect route below.
  2. the effect route (skipped when declaresContentsWrite): a release authored by github-actions[bot], a write the bot demonstrably made, proof enough that its token could write, with no permissions block in sight either.

Neither route ever returns an error: every failure (network, auth, an unparsable response) degrades to the next route, and finally to write="unknown", source="unknown" when nothing settled it. Nothing here ever logs the job's log body or an Authorization header value.

func FetchGitHubEnvironments added in v0.7.1

func FetchGitHubEnvironments(host, owner, repo string) ([]ir.Environment, bool, error)

FetchGitHubEnvironments resolves the repository's deployment environments and the reviewer rules each one carries, via GET /repos/{owner}/{repo}/environments. Pagination follows the response's `Link: <...>; rel="next"` header rather than counting pages, so a host that answers fewer or more entries per page than requested is still walked correctly.

The bool return is known: a repository's environments are an optional fact a claim about a protected job can be capped by, never one that fails collection. known is false, with a nil error, when the endpoint answers 401/403 (no access), 404 (environments not supported, or the repository itself is not visible) or when the request never reaches a response at all (DNS, connection refused, timeout). known is also false on any other failure, but that error is returned too, for the caller to log; it is never meant to propagate as a collection failure.

func FetchGitHubRepoIdentity added in v0.6.0

func FetchGitHubRepoIdentity(host, owner, repo string) (visibility, fullName string)

FetchGitHubRepoIdentity answers, from the one repos/{owner}/{repo} lookup, the repository's visibility and its canonical owner/repo. The visibility is public, private or internal as GitHub sees it, normalized to the three situation-fact values, or unknown when the API cannot answer (no token, no access, not found, any other error); it never fails the run, the situation facts treat unknown like any other missing fact. GitHub Enterprise's "internal" repos report private: false alongside visibility: "internal", so the visibility field, when present, takes precedence over the private boolean; hosts that omit visibility fall back to private. The canonical name: the API follows renames, so a repository asked for under a former name or another case answers with its current full_name. The full name is empty whenever the lookup fails or answers no owner/repo shaped name, and the caller then keeps the path it was given.

func FetchGitHubWorkflowStates added in v0.7.1

func FetchGitHubWorkflowStates(host, owner, repo string) (map[string]string, bool, error)

FetchGitHubWorkflowStates resolves the enabled/disabled state of every workflow file in the repository via GET /repos/{owner}/{repo}/actions/workflows, mapping each workflow's path to its state. Pagination follows the response's `Link: <...>; rel="next"` header rather than counting pages, same contract as FetchGitHubEnvironments.

The bool return is known: a repository's workflow states are an optional fact a job's "cannot run" claim can be capped by, never one that fails collection. known is false, with a nil error, when the endpoint answers 401/403 (no access), 404 (the repository itself is not visible) or when the request never reaches a response at all (DNS, connection refused, timeout). known is also false on any other failure, but that error is returned too, for the caller to log; it is never meant to propagate as a collection failure.

func GitHubRepoVisible added in v0.4.29

func GitHubRepoVisible(host, owner, repo string) (bool, error)

GitHubRepoVisible reports whether repos/{owner}/{repo} is readable with the current credentials. It exists to disambiguate a zero-branch protection fetch: the per-branch endpoints swallow 404s ("branch absent" is legitimate on a real repo), so a repo that is nonexistent, renamed, or invisible to the token yields zero branches with no error anywhere. Every visible GitHub repo answers its metadata endpoint, so false here means the repo cannot be seen at all. Unlike FetchGitHubDefaultBranch this deliberately does NOT fold 404/401 into a silent success -- telling the difference is its whole job.

func ScanActionSource added in v0.4.11

func ScanActionSource(files map[string]string) *ir.MutableRemoteExec

ScanActionSource inspects an action's fetched source files (path → content) and returns the strongest mutable-remote-exec signal, or nil when nothing mutable is found. Tiers, strongest first:

obfuscated — decode-then-exec / eval(atob): hiding is the finding (critical)
exec       — a script fetched from a moving ref and run, no checksum (high)
data       — a mutable manifest, OR an exec that DOES verify a checksum
             (content-pinned, materially safer) — recorded, not surfaced

Files are scanned in sorted order so the reported hit is deterministic.

func ScanGitHubWorkflowsRemote

func ScanGitHubWorkflowsRemote(host, owner, repo, ref string, enrichActionMetadata, scanMutableExec 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, scanMutableExec bool, progressFn ProgressFunc) (pipeline *ir.NormalizedPipeline, partialErrors []error, err error)

ScanGitHubWorkflowsWithProgress 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. An individual unreadable file is returned in partialErrors so the caller can surface it without aborting the whole scan; a file that reads fine but does not parse as a workflow is returned on the pipeline's InvalidWorkflows instead (#517).

The caller is notified through progressFn as the scan 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 simply pass nil.

func ScanLocalSelfAction added in v0.4.11

func ScanLocalSelfAction(rootDir string) *ir.MutableRemoteExec

ScanLocalSelfAction checks whether the scanned repository is itself a GitHub Action (root action.yml/action.yaml) whose own source fetches mutable remote code, reading the action definition and entrypoint from the local checkout. Returns nil when the repo is not an action or nothing mutable is found. This is the producer-side check: scanning an action's own repo must flag the action it publishes.

func ScanRemoteSelfAction added in v0.4.11

func ScanRemoteSelfAction(owner, repo, ref string) *ir.MutableRemoteExec

ScanRemoteSelfAction is the remote counterpart of ScanLocalSelfAction: it fetches the scanned repo's own root action.yml + entrypoint over raw content and scans them. Used on the --project (upstream-fetch) path.

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
	// RefCommitSha is set only when the pinned ref is the SHA of an
	// annotated tag OBJECT: it carries the commit that tag object
	// dereferences to. Empty for every other ref shape, including a
	// plain commit pin (there the ref already IS the commit).
	RefCommitSha     string
	TagSha           string
	LatestTag        string
	LatestReleaseSha string
	RefIsAmbiguous   bool
	Advisories       []string
	// AdvisorySeverities maps each id of Advisories to its severity
	// (low, medium, high or critical), read off the same listing; an id
	// whose severity the listing did not give is absent.
	AdvisorySeverities map[string]string
	StargazersCount    int
	// RedirectedOwner is the owner the repository's current full_name
	// names, when it differs from the `uses` owner (case insensitive):
	// GitHub keeps resolving the old owner/repo path after a rename or
	// an organisation transfer, so this is who can actually change the
	// code. Empty when the names agree or the repository could not be
	// read.
	RedirectedOwner string
}

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 NewGitHubMetadataClientForHost

func NewGitHubMetadataClientForHost(host string) *GitHubMetadataClient

NewGitHubMetadataClientForHost 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.

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 — for GitHub Enterprise Server pass the GHES API host (e.g. "ghes.example.com" or "ghes.example.com/api/v3") paired 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

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.

func (*GitHubMetadataClient) UnavailableReason added in v0.4.50

func (c *GitHubMetadataClient) UnavailableReason() string

UnavailableReason explains why metadata enrichment did not run at all, or "" when it did.

Without this the disable cause was recorded and never read: no token, a disabled API, or a client-construction error returned from enrichment before a single action was touched, leaving every metadata-driven control reporting passed over an empty result.

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